@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.
@@ -8,6 +8,15 @@ export interface ConfirmationRequest {
8
8
  */
9
9
  message?: string;
10
10
  }
11
+ /** Options for {@link requestConfirmation}. */
12
+ export interface ConfirmationOptions {
13
+ /**
14
+ * Aborting this signal resolves the card as declined (buttons disabled,
15
+ * `data-resolved="declined"`) — the hook a Stop control uses to dismiss a
16
+ * pending confirmation when the user cancels the whole run.
17
+ */
18
+ signal?: AbortSignal;
19
+ }
11
20
  /**
12
21
  * Append an inline confirmation card to ``host`` (the chat message list) and
13
22
  * resolve when the user decides.
@@ -18,5 +27,5 @@ export interface ConfirmationRequest {
18
27
  * cancel. The card stays in the transcript as a resolved record (buttons
19
28
  * disabled, `data-resolved` set) rather than vanishing.
20
29
  */
21
- export declare function requestConfirmation(host: Node & ParentNode, request: ConfirmationRequest): Promise<boolean>;
30
+ export declare function requestConfirmation(host: Node & ParentNode, request: ConfirmationRequest, options?: ConfirmationOptions): Promise<boolean>;
22
31
  //# sourceMappingURL=confirmation_card.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"confirmation_card.d.ts","sourceRoot":"","sources":["../../src/ui/confirmation_card.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAWD;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,IAAI,GAAG,UAAU,EACvB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,OAAO,CAAC,CAqClB"}
1
+ {"version":3,"file":"confirmation_card.d.ts","sourceRoot":"","sources":["../../src/ui/confirmation_card.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAWD,+CAA+C;AAC/C,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,IAAI,GAAG,UAAU,EACvB,OAAO,EAAE,mBAAmB,EAC5B,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,OAAO,CAAC,CAgDlB"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 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"}
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/ui/styles.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,MAAM,4veAqnBlB,CAAC"}
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/ui/styles.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,MAAM,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.1",
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",
@@ -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
- this.#threadId = this.conversationStore.threadId();
474
- this.#send.disabled = false;
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
- const decision = requestConfirmation(this.#messages, request);
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.#send.disabled = true;
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.#send.disabled = false;
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.#send.disabled = false;
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
@@ -53,11 +53,19 @@ export interface AgUiClientHandlers {
53
53
  onToolResult(toolCallId: string, content: string): void;
54
54
  onRunEnd(): void;
55
55
  onError(message: string): void;
56
+ /**
57
+ * Fired when the user cancelled the run ({@link AgUiClient.cancel}) — the
58
+ * deliberate-stop sibling of `onError`. Any partial assistant text already
59
+ * streamed stays valid; the host should keep the bubble and show a muted
60
+ * "stopped" affordance rather than an error. `onSettled` still follows.
61
+ */
62
+ onCancelled(): void;
56
63
  /**
57
64
  * Fired exactly once when the whole interaction settles — after the run loop
58
65
  * ends for any reason (a server-only round, frontend-tool rounds exhausted,
59
- * or an error). The terminal guarantee that the UI returns to rest (pending
60
- * indicator cleared, input re-enabled) no matter how the run finished.
66
+ * a cancellation, or an error). The terminal guarantee that the UI returns
67
+ * to rest (pending indicator cleared, input re-enabled) no matter how the
68
+ * run finished.
61
69
  */
62
70
  onSettled(): void;
63
71
  }
@@ -100,6 +108,9 @@ export class AgUiClient {
100
108
  readonly #getContext: () => Context[];
101
109
  readonly #executeTool: ExecuteTool | null;
102
110
  readonly #onPersist: (messages: readonly Message[]) => void;
111
+ // Set by cancel(); reset at the top of each #run(). Checked by the loop so
112
+ // a cancel between frontend-tool rounds doesn't start another round.
113
+ #cancelled = false;
103
114
 
104
115
  constructor(config: AgUiClientConfig) {
105
116
  this.#agent = config.agent;
@@ -148,24 +159,64 @@ export class AgUiClient {
148
159
  this.#onPersist(this.#agent.messages);
149
160
  }
150
161
 
162
+ /**
163
+ * Cancel the in-flight run (AG-UI has no server cancel route — aborting the
164
+ * streaming request is the protocol's cancel; the server observes the
165
+ * disconnect). Safe to call with no run in flight. Partial text already
166
+ * streamed stays in history; {@link AgUiClientHandlers.onCancelled} fires
167
+ * instead of `onError`, and `onSettled` still follows.
168
+ */
169
+ cancel(): void {
170
+ this.#cancelled = true;
171
+ this.#agent.abortRun();
172
+ }
173
+
151
174
  async #run(): Promise<void> {
175
+ this.#cancelled = false;
152
176
  try {
153
177
  await this.#runLoop();
178
+ // `@ag-ui/client` filters abort errors inside `runAgent` (it resolves
179
+ // rather than rejects on a cancelled fetch), so a cancelled run usually
180
+ // ends here, not in the catch.
181
+ if (this.#cancelled) {
182
+ this.#onCancelled();
183
+ }
154
184
  } catch (error) {
155
- this.#handlers.onError(error instanceof Error ? error.message : String(error));
185
+ if (this.#cancelled || isAbortError(error)) {
186
+ this.#onCancelled();
187
+ } else {
188
+ this.#handlers.onError(error instanceof Error ? error.message : String(error));
189
+ }
156
190
  } finally {
157
191
  this.#handlers.onSettled();
158
192
  }
159
193
  }
160
194
 
195
+ #onCancelled(): void {
196
+ // The truncated exchange (including any partial assistant text the agent
197
+ // already applied) survives a reload.
198
+ this.#onPersist(this.#agent.messages);
199
+ this.#handlers.onCancelled();
200
+ }
201
+
161
202
  async #runLoop(): Promise<void> {
162
203
  for (let round = 0; round < MAX_TOOL_ROUNDS; round += 1) {
204
+ // A cancel during the previous round's frontend-tool execution lands
205
+ // here: the running handler completed, but no further round starts.
206
+ if (this.#cancelled) {
207
+ return;
208
+ }
163
209
  const pending: AgUiToolCall[] = [];
164
210
  await this.#agent.runAgent(
165
211
  { tools: this.#getTools(), context: this.#getContext() },
166
212
  this.#buildSubscriber(pending),
167
213
  );
168
214
  this.#onPersist(this.#agent.messages);
215
+ // Cancelled mid-stream: the user said stop — don't execute the tool
216
+ // calls collected before the abort.
217
+ if (this.#cancelled) {
218
+ return;
219
+ }
169
220
  if (this.#executeTool === null || pending.length === 0) {
170
221
  return;
171
222
  }
@@ -228,3 +279,12 @@ export class AgUiClient {
228
279
  };
229
280
  }
230
281
  }
282
+
283
+ /**
284
+ * Whether a rejection came from aborting the run's fetch. Belt-and-suspenders
285
+ * with the `#cancelled` flag: some `@ag-ui/client` versions re-throw the
286
+ * `AbortError` instead of filtering it.
287
+ */
288
+ function isAbortError(error: unknown): boolean {
289
+ return error instanceof Error && error.name === "AbortError";
290
+ }