@artooi/ag-ui-web-component 0.3.1 → 0.5.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 +59 -1
- package/README.md +25 -1
- package/dist/ag-ui-web-component.bundle.js +251 -54
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- 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/conversation_store.d.ts +37 -8
- package/dist/core/conversation_store.d.ts.map +1 -1
- package/dist/core/remote_conversation_store.d.ts +35 -0
- package/dist/core/remote_conversation_store.d.ts.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +740 -13
- package/dist/index.js.map +4 -4
- package/dist/ui/confirmation_card.d.ts +10 -1
- package/dist/ui/confirmation_card.d.ts.map +1 -1
- package/dist/ui/relative_time.d.ts +11 -0
- package/dist/ui/relative_time.d.ts.map +1 -0
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/thread_drawer.d.ts +33 -0
- package/dist/ui/thread_drawer.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/core/ag_ui_chat.ts +163 -8
- package/src/core/agui_client.ts +63 -3
- package/src/core/conversation_store.ts +148 -9
- package/src/core/remote_conversation_store.ts +147 -0
- package/src/index.ts +7 -1
- package/src/ui/confirmation_card.ts +22 -0
- package/src/ui/relative_time.ts +28 -0
- package/src/ui/styles.ts +197 -0
- package/src/ui/thread_drawer.ts +200 -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
|
+
* A compact relative timestamp for a thread row — e.g. `"just now"`, `"5m ago"`,
|
|
3
|
+
* `"3h ago"`, `"2d ago"`, `"4w ago"`.
|
|
4
|
+
*
|
|
5
|
+
* `now` is injectable so callers (and tests) can pin the reference point; it
|
|
6
|
+
* defaults to the current time. A timestamp in the future (clock skew) reads as
|
|
7
|
+
* `"just now"`. Kept locale-independent on purpose so the rendered label is
|
|
8
|
+
* stable across environments.
|
|
9
|
+
*/
|
|
10
|
+
export declare function relativeTime(timestamp: number, now?: number): string;
|
|
11
|
+
//# sourceMappingURL=relative_time.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relative_time.d.ts","sourceRoot":"","sources":["../../src/ui/relative_time.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,GAAE,MAAmB,GAAG,MAAM,CAkBhF"}
|
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\n/* Chat-history drawer \u2014 a slide-over within the chat panel. */\n.drawer {\n position: absolute;\n inset: 0;\n z-index: 5;\n display: flex;\n}\n\n.drawer[hidden] {\n display: none;\n}\n\n.drawer-backdrop {\n position: absolute;\n inset: 0;\n background: rgba(20, 20, 50, 0.32);\n}\n\n.drawer-panel {\n position: relative;\n display: flex;\n flex-direction: column;\n width: min(300px, 85%);\n height: 100%;\n background: var(--ag-ui-bg);\n border-right: 1px solid var(--ag-ui-border);\n box-shadow: var(--ag-ui-shadow);\n overflow: hidden;\n}\n\n.drawer-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--ag-ui-space);\n padding: var(--ag-ui-pad);\n border-bottom: 1px solid var(--ag-ui-border);\n}\n\n.drawer-title {\n font-weight: 600;\n}\n\n.drawer-new {\n border: 1px solid var(--ag-ui-border);\n border-radius: var(--ag-ui-radius);\n background: var(--ag-ui-bg);\n color: var(--ag-ui-accent);\n padding: 4px 10px;\n font: inherit;\n font-size: 0.85em;\n cursor: pointer;\n}\n\n.drawer-list {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n}\n\n.drawer-empty {\n padding: var(--ag-ui-pad);\n font-size: 0.9em;\n color: var(--ag-ui-muted);\n}\n\n.drawer-row {\n display: flex;\n align-items: stretch;\n border-bottom: 1px solid var(--ag-ui-border);\n}\n\n.drawer-row--active {\n background: var(--ag-ui-assistant-bg);\n}\n\n.drawer-row-select {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 8px 12px;\n border: none;\n background: none;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.drawer-row-title {\n font-weight: 600;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n.drawer-row-time {\n font-size: 0.72em;\n color: var(--ag-ui-muted);\n}\n\n.drawer-row-preview {\n font-size: 0.8em;\n color: var(--ag-ui-muted);\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n.drawer-row-actions {\n display: flex;\n align-items: center;\n gap: 2px;\n padding: 0 6px;\n}\n\n.drawer-row-rename,\n.drawer-row-delete {\n border: none;\n background: none;\n color: var(--ag-ui-muted);\n font-size: 0.9em;\n padding: 4px;\n cursor: pointer;\n}\n\n.drawer-rename-input {\n flex: 1;\n min-width: 0;\n margin: 6px 10px;\n padding: 4px 8px;\n border: 1px solid var(--ag-ui-accent);\n border-radius: 6px;\n background: var(--ag-ui-input-bg);\n color: var(--ag-ui-fg);\n font: inherit;\n}\n\n.drawer-confirm {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n font-size: 0.85em;\n}\n\n.drawer-confirm-label {\n color: var(--ag-ui-danger);\n}\n\n.drawer-confirm-yes {\n border: none;\n border-radius: 6px;\n background: var(--ag-ui-danger);\n color: #ffffff;\n padding: 3px 10px;\n font: inherit;\n cursor: pointer;\n}\n\n.drawer-confirm-no {\n border: 1px solid var(--ag-ui-border);\n border-radius: 6px;\n background: none;\n color: inherit;\n padding: 3px 10px;\n font: inherit;\n cursor: pointer;\n}\n\n/* Embedded placement: an inline, flush side panel rather than a dimmed,\n floating slide-over. */\n:host([placement=\"embedded\"]) .drawer-backdrop {\n background: none;\n}\n\n:host([placement=\"embedded\"]) .drawer-panel {\n width: 100%;\n border-right: none;\n box-shadow: none;\n}\n";
|
|
2
2
|
//# sourceMappingURL=styles.d.ts.map
|
package/dist/ui/styles.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/ui/styles.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,MAAM,
|
|
1
|
+
{"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/ui/styles.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,MAAM,o7lBA0zBlB,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ThreadMeta } from "../core/conversation_store.js";
|
|
2
|
+
/** Actions the host ({@link AgUiChat}) wires to the drawer's rows. */
|
|
3
|
+
export interface ThreadDrawerCallbacks {
|
|
4
|
+
/** A row was picked — load that thread and make it active. */
|
|
5
|
+
readonly onSelect: (threadId: string) => void;
|
|
6
|
+
/** The "New chat" action — start a fresh thread. */
|
|
7
|
+
readonly onNew: () => void;
|
|
8
|
+
/** A row was renamed to `title`. */
|
|
9
|
+
readonly onRename: (threadId: string, title: string) => void;
|
|
10
|
+
/** A row was deleted (after the inline confirm). */
|
|
11
|
+
readonly onDelete: (threadId: string) => void;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The chat-history drawer: a slide-over listing the user's threads (title,
|
|
15
|
+
* relative time, preview), with select / new / rename / delete actions and an
|
|
16
|
+
* empty state. Pure DOM in the spirit of {@link SkillsMenu} — the host appends
|
|
17
|
+
* {@link element}, toggles it, feeds rows via {@link setThreads}, and acts on
|
|
18
|
+
* the callbacks. The drawer is a *view*: it does not mutate the store; after a
|
|
19
|
+
* callback the host updates the store and calls {@link setThreads} to refresh.
|
|
20
|
+
*/
|
|
21
|
+
export declare class ThreadDrawer {
|
|
22
|
+
#private;
|
|
23
|
+
/** The drawer root (backdrop + panel). Append to the chat shell; hidden until opened. */
|
|
24
|
+
readonly element: HTMLDivElement;
|
|
25
|
+
constructor(callbacks: ThreadDrawerCallbacks);
|
|
26
|
+
isOpen(): boolean;
|
|
27
|
+
open(): void;
|
|
28
|
+
close(): void;
|
|
29
|
+
toggle(): void;
|
|
30
|
+
/** Render the rows (or the empty state), highlighting the active thread. */
|
|
31
|
+
setThreads(threads: readonly ThreadMeta[], activeId: string): void;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=thread_drawer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thread_drawer.d.ts","sourceRoot":"","sources":["../../src/ui/thread_drawer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAGhE,sEAAsE;AACtE,MAAM,WAAW,qBAAqB;IACpC,8DAA8D;IAC9D,QAAQ,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,oDAAoD;IACpD,QAAQ,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC;IAC3B,oCAAoC;IACpC,QAAQ,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7D,oDAAoD;IACpD,QAAQ,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/C;AAED;;;;;;;GAOG;AACH,qBAAa,YAAY;;IACvB,yFAAyF;IACzF,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;gBAOrB,SAAS,EAAE,qBAAqB;IAsC5C,MAAM,IAAI,OAAO;IAIjB,IAAI,IAAI,IAAI;IAIZ,KAAK,IAAI,IAAI;IAIb,MAAM,IAAI,IAAI;IAId,4EAA4E;IAC5E,UAAU,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;CAgHnE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@artooi/ag-ui-web-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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
|
@@ -24,6 +24,7 @@ import { renderMarkdown } from "../ui/render_markdown.js";
|
|
|
24
24
|
import { wrapWords } from "../ui/reveal_words.js";
|
|
25
25
|
import { SkillsMenu } from "../ui/skills_menu.js";
|
|
26
26
|
import { STYLES } from "../ui/styles.js";
|
|
27
|
+
import { ThreadDrawer } from "../ui/thread_drawer.js";
|
|
27
28
|
import { ToolCallCard, type ToolDisplayMode } from "../ui/tool_call_card.js";
|
|
28
29
|
import {
|
|
29
30
|
AgUiClient,
|
|
@@ -37,6 +38,7 @@ import {
|
|
|
37
38
|
SessionStorageStore,
|
|
38
39
|
} from "./conversation_store.js";
|
|
39
40
|
import { type AgentFactory, createHttpAgent } from "./create_http_agent.js";
|
|
41
|
+
import { RemoteConversationStore } from "./remote_conversation_store.js";
|
|
40
42
|
|
|
41
43
|
/** The role a rendered chat message takes. */
|
|
42
44
|
export type MessageRole = (typeof MESSAGE_ROLE)[keyof typeof MESSAGE_ROLE];
|
|
@@ -192,9 +194,17 @@ export class AgUiChat extends HTMLElement {
|
|
|
192
194
|
readonly #send: HTMLButtonElement;
|
|
193
195
|
readonly #title: HTMLSpanElement;
|
|
194
196
|
readonly #skillsMenu: SkillsMenu;
|
|
197
|
+
readonly #drawer: ThreadDrawer;
|
|
195
198
|
readonly #skillHint: HTMLDivElement;
|
|
196
199
|
|
|
197
200
|
#client: AgUiClient | null = null;
|
|
201
|
+
// Whether an interaction is in flight (first onRunStart → onSettled). Drives
|
|
202
|
+
// the Send⇄Stop button: `agent.isRunning` is false between frontend-tool
|
|
203
|
+
// rounds, but the user must still be able to stop there.
|
|
204
|
+
#running = false;
|
|
205
|
+
// Aborting this dismisses (declines) an open confirmation card when the run
|
|
206
|
+
// is cancelled while the card awaits a decision. One controller per card.
|
|
207
|
+
#confirmAbort: AbortController | null = null;
|
|
198
208
|
#streamingBubble: HTMLDivElement | null = null;
|
|
199
209
|
// Text deltas applied to the current streaming bubble. >1 ⇒ the message
|
|
200
210
|
// revealed progressively as it streamed, so the word reveal must not re-animate
|
|
@@ -218,6 +228,22 @@ export class AgUiChat extends HTMLElement {
|
|
|
218
228
|
this.#title = document.createElement("span");
|
|
219
229
|
this.#skillHint = document.createElement("div");
|
|
220
230
|
this.#skillsMenu = new SkillsMenu((skill) => this.#applySkill(skill));
|
|
231
|
+
this.#drawer = new ThreadDrawer({
|
|
232
|
+
onSelect: (threadId) => {
|
|
233
|
+
void this.#switchThread(threadId);
|
|
234
|
+
},
|
|
235
|
+
onNew: () => {
|
|
236
|
+
this.newChat();
|
|
237
|
+
void this.#refreshDrawer();
|
|
238
|
+
},
|
|
239
|
+
onRename: (threadId, title) => {
|
|
240
|
+
this.conversationStore.renameThread(threadId, title);
|
|
241
|
+
void this.#refreshDrawer();
|
|
242
|
+
},
|
|
243
|
+
onDelete: (threadId) => {
|
|
244
|
+
this.#deleteThread(threadId);
|
|
245
|
+
},
|
|
246
|
+
});
|
|
221
247
|
}
|
|
222
248
|
|
|
223
249
|
/** Attributes whose late changes must reflect in already-rendered chrome. */
|
|
@@ -330,10 +356,28 @@ export class AgUiChat extends HTMLElement {
|
|
|
330
356
|
}
|
|
331
357
|
this.#initSkills();
|
|
332
358
|
void this.#fetchToolCatalog();
|
|
359
|
+
this.#wireThreadStore();
|
|
333
360
|
this.#threadId = this.conversationStore.threadId();
|
|
334
361
|
void this.#rehydrate();
|
|
335
362
|
}
|
|
336
363
|
|
|
364
|
+
/**
|
|
365
|
+
* When `data-threads-url` is set, route thread enumeration / load / rename /
|
|
366
|
+
* delete through that server endpoint (wrapping the current store as the
|
|
367
|
+
* client-only fallback), so the history drawer shows durable, cross-device
|
|
368
|
+
* threads. Without it, the client store's per-tab threads are used.
|
|
369
|
+
*/
|
|
370
|
+
#wireThreadStore(): void {
|
|
371
|
+
const url = this.getAttribute("data-threads-url");
|
|
372
|
+
if (url !== null) {
|
|
373
|
+
this.conversationStore = new RemoteConversationStore(
|
|
374
|
+
url,
|
|
375
|
+
() => this.headers,
|
|
376
|
+
this.conversationStore,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
337
381
|
/** Fetch the server tool-label catalog from `data-tools-url`, if set. */
|
|
338
382
|
async #fetchToolCatalog(): Promise<void> {
|
|
339
383
|
const url = this.getAttribute("data-tools-url");
|
|
@@ -462,7 +506,17 @@ export class AgUiChat extends HTMLElement {
|
|
|
462
506
|
* in-memory run state, clear the transcript, and mint a new thread id.
|
|
463
507
|
*/
|
|
464
508
|
newChat(): void {
|
|
509
|
+
// Stop any in-flight run first — discarding the client mid-run would
|
|
510
|
+
// leave the old agent streaming into a cleared transcript.
|
|
511
|
+
this.#cancelRun();
|
|
465
512
|
this.conversationStore.clear(this.#threadId);
|
|
513
|
+
this.#resetState();
|
|
514
|
+
this.#threadId = this.conversationStore.threadId();
|
|
515
|
+
this.#setRunning(false);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** Drop the in-memory run + transcript, leaving the thread id untouched. */
|
|
519
|
+
#resetState(): void {
|
|
466
520
|
this.#client = null;
|
|
467
521
|
this.#streamingBubble = null;
|
|
468
522
|
this.#hidePending();
|
|
@@ -470,8 +524,39 @@ export class AgUiChat extends HTMLElement {
|
|
|
470
524
|
this.#serverSettled.clear();
|
|
471
525
|
this.#initialMessages = [];
|
|
472
526
|
this.#messages.replaceChildren();
|
|
473
|
-
|
|
474
|
-
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** Switch the active conversation to an existing thread and replay it. */
|
|
530
|
+
async #switchThread(threadId: string): Promise<void> {
|
|
531
|
+
if (threadId === this.#threadId) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
this.#cancelRun();
|
|
535
|
+
this.#resetState();
|
|
536
|
+
this.conversationStore.setActiveThread(threadId);
|
|
537
|
+
this.#threadId = threadId;
|
|
538
|
+
this.#setRunning(false);
|
|
539
|
+
await this.#rehydrate();
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/** Delete a thread; if it was the active one, fall back to a fresh chat. */
|
|
543
|
+
#deleteThread(threadId: string): void {
|
|
544
|
+
const wasActive = threadId === this.#threadId;
|
|
545
|
+
if (wasActive) {
|
|
546
|
+
this.#cancelRun();
|
|
547
|
+
}
|
|
548
|
+
this.conversationStore.clear(threadId);
|
|
549
|
+
if (wasActive) {
|
|
550
|
+
this.#resetState();
|
|
551
|
+
this.#threadId = this.conversationStore.threadId();
|
|
552
|
+
this.#setRunning(false);
|
|
553
|
+
}
|
|
554
|
+
void this.#refreshDrawer();
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** Reload the drawer's thread list, marking the active thread. */
|
|
558
|
+
async #refreshDrawer(): Promise<void> {
|
|
559
|
+
this.#drawer.setThreads(await this.conversationStore.listThreads(), this.#threadId);
|
|
475
560
|
}
|
|
476
561
|
|
|
477
562
|
/**
|
|
@@ -603,6 +688,17 @@ export class AgUiChat extends HTMLElement {
|
|
|
603
688
|
const controls = document.createElement("div");
|
|
604
689
|
controls.className = "header-controls";
|
|
605
690
|
|
|
691
|
+
const history = document.createElement("button");
|
|
692
|
+
history.type = "button";
|
|
693
|
+
history.className = "header-btn header-btn--history";
|
|
694
|
+
history.title = "Chat history";
|
|
695
|
+
history.setAttribute("aria-label", "Chat history");
|
|
696
|
+
history.textContent = "☰";
|
|
697
|
+
history.addEventListener("click", () => {
|
|
698
|
+
void this.#refreshDrawer();
|
|
699
|
+
this.#drawer.open();
|
|
700
|
+
});
|
|
701
|
+
|
|
606
702
|
const newChat = document.createElement("button");
|
|
607
703
|
newChat.type = "button";
|
|
608
704
|
newChat.className = "header-btn header-btn--new";
|
|
@@ -619,7 +715,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
619
715
|
collapse.textContent = "—";
|
|
620
716
|
collapse.addEventListener("click", () => this.toggleCollapsed());
|
|
621
717
|
|
|
622
|
-
controls.append(newChat, collapse);
|
|
718
|
+
controls.append(history, newChat, collapse);
|
|
623
719
|
header.append(title, controls);
|
|
624
720
|
|
|
625
721
|
this.#messages.className = "messages";
|
|
@@ -641,7 +737,15 @@ export class AgUiChat extends HTMLElement {
|
|
|
641
737
|
this.#send.className = "send";
|
|
642
738
|
this.#send.type = "button";
|
|
643
739
|
this.#send.textContent = "Send";
|
|
740
|
+
this.#send.setAttribute("aria-label", "Send");
|
|
741
|
+
this.#send.dataset["state"] = "idle";
|
|
644
742
|
this.#send.addEventListener("click", () => {
|
|
743
|
+
// One button, two states: Send while idle, Stop while a run is in
|
|
744
|
+
// flight (no layout change).
|
|
745
|
+
if (this.#running) {
|
|
746
|
+
this.#cancelRun();
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
645
749
|
void this.#submit();
|
|
646
750
|
});
|
|
647
751
|
|
|
@@ -658,6 +762,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
658
762
|
this.#skillsMenu.chips,
|
|
659
763
|
this.#skillHint,
|
|
660
764
|
inputRow,
|
|
765
|
+
this.#drawer.element,
|
|
661
766
|
);
|
|
662
767
|
this.#root.append(style, this.#chat);
|
|
663
768
|
}
|
|
@@ -674,12 +779,39 @@ export class AgUiChat extends HTMLElement {
|
|
|
674
779
|
event.preventDefault();
|
|
675
780
|
return;
|
|
676
781
|
}
|
|
782
|
+
// Escape-to-cancel — only reachable when the palette is closed (it
|
|
783
|
+
// consumed the key above otherwise), so the two Escapes don't clash.
|
|
784
|
+
if (event.key === "Escape" && this.#running) {
|
|
785
|
+
event.preventDefault();
|
|
786
|
+
this.#cancelRun();
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
677
789
|
if (event.key === "Enter" && !event.shiftKey) {
|
|
678
790
|
event.preventDefault();
|
|
679
791
|
void this.#submit();
|
|
680
792
|
}
|
|
681
793
|
}
|
|
682
794
|
|
|
795
|
+
/**
|
|
796
|
+
* Stop the in-flight run: decline any confirmation card awaiting a decision
|
|
797
|
+
* (the loop is suspended on it), then cancel the client run — the abort
|
|
798
|
+
* closes the streaming request, which is AG-UI's cancel (the server
|
|
799
|
+
* observes the disconnect).
|
|
800
|
+
*/
|
|
801
|
+
#cancelRun(): void {
|
|
802
|
+
this.#confirmAbort?.abort();
|
|
803
|
+
this.#client?.cancel();
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/** Swap the composer button between Send (idle) and Stop (running). */
|
|
807
|
+
#setRunning(running: boolean): void {
|
|
808
|
+
this.#running = running;
|
|
809
|
+
const label = running ? "Stop" : "Send";
|
|
810
|
+
this.#send.textContent = label;
|
|
811
|
+
this.#send.setAttribute("aria-label", label);
|
|
812
|
+
this.#send.dataset["state"] = running ? "running" : "idle";
|
|
813
|
+
}
|
|
814
|
+
|
|
683
815
|
async #submit(): Promise<void> {
|
|
684
816
|
const content = this.#input.value.trim();
|
|
685
817
|
if (content === "") {
|
|
@@ -762,9 +894,15 @@ export class AgUiChat extends HTMLElement {
|
|
|
762
894
|
if (typeof confirmText === "string") {
|
|
763
895
|
request.message = confirmText;
|
|
764
896
|
}
|
|
765
|
-
|
|
897
|
+
// The run loop is suspended on this card; a Stop while it's open aborts
|
|
898
|
+
// the controller, resolving the decision as declined.
|
|
899
|
+
this.#confirmAbort = new AbortController();
|
|
900
|
+
const decision = requestConfirmation(this.#messages, request, {
|
|
901
|
+
signal: this.#confirmAbort.signal,
|
|
902
|
+
});
|
|
766
903
|
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
767
904
|
const accepted = await decision;
|
|
905
|
+
this.#confirmAbort = null;
|
|
768
906
|
if (!accepted) {
|
|
769
907
|
const message = "User declined the action.";
|
|
770
908
|
card.settle(TOOL_CALL_STATUS.DECLINED, message);
|
|
@@ -806,7 +944,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
806
944
|
#handlers(): AgUiClientHandlers {
|
|
807
945
|
return {
|
|
808
946
|
onRunStart: () => {
|
|
809
|
-
this.#
|
|
947
|
+
this.#setRunning(true);
|
|
810
948
|
this.#showPending();
|
|
811
949
|
},
|
|
812
950
|
onTextDelta: (buffer) => {
|
|
@@ -838,25 +976,42 @@ export class AgUiChat extends HTMLElement {
|
|
|
838
976
|
this.#serverSettled.add(toolCallId);
|
|
839
977
|
},
|
|
840
978
|
onRunEnd: () => {
|
|
979
|
+
// Per-round end; the button stays on Stop until the whole interaction
|
|
980
|
+
// settles — the user must be able to cancel between tool rounds.
|
|
841
981
|
this.#hidePending();
|
|
842
|
-
this.#send.disabled = false;
|
|
843
982
|
this.#streamingBubble = null;
|
|
844
983
|
},
|
|
845
984
|
onError: (message) => {
|
|
846
985
|
this.#hidePending();
|
|
847
986
|
this.#revealWords(this.appendMessage(MESSAGE_ROLE.ASSISTANT, `⚠️ ${message}`));
|
|
848
|
-
this.#
|
|
987
|
+
this.#streamingBubble = null;
|
|
988
|
+
},
|
|
989
|
+
onCancelled: () => {
|
|
990
|
+
// Deliberate stop, not a failure: keep whatever partial text already
|
|
991
|
+
// streamed and add a muted note instead of an error bubble.
|
|
992
|
+
this.#hidePending();
|
|
993
|
+
this.#appendStoppedNote();
|
|
849
994
|
this.#streamingBubble = null;
|
|
850
995
|
},
|
|
851
996
|
onSettled: () => {
|
|
852
997
|
// Terminal guarantee: whatever path ended the run, return to rest.
|
|
853
998
|
this.#hidePending();
|
|
854
|
-
this.#
|
|
999
|
+
this.#setRunning(false);
|
|
855
1000
|
this.#streamingBubble = null;
|
|
856
1001
|
},
|
|
857
1002
|
};
|
|
858
1003
|
}
|
|
859
1004
|
|
|
1005
|
+
/** A muted "⏹ Stopped" line in the transcript (distinct from the ⚠️ error bubble). */
|
|
1006
|
+
#appendStoppedNote(): void {
|
|
1007
|
+
const note = document.createElement("div");
|
|
1008
|
+
note.className = "stopped-note";
|
|
1009
|
+
note.setAttribute("role", "status");
|
|
1010
|
+
note.textContent = "⏹ Stopped";
|
|
1011
|
+
this.#messages.appendChild(note);
|
|
1012
|
+
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
860
1015
|
/**
|
|
861
1016
|
* Show a "thinking" indicator while the agent is being awaited — both the
|
|
862
1017
|
* 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
|
+
}
|