@artooi/ag-ui-web-component 0.2.0 → 0.2.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"route_map.d.ts","sourceRoot":"","sources":["../../src/tools/route_map.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D,gEAAgE;AAChE,MAAM,WAAW,KAAK;IACpB,yDAAyD;IACzD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sCAAsC;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,kDAAkD;IAClD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,4DAA4D;IAC5D,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,qDAAqD;AACrD,MAAM,MAAM,QAAQ,GAAG,SAAS,KAAK,EAAE,CAAC;AAExC,yEAAyE;AACzE,MAAM,WAAW,eAAgB,SAAQ,KAAK;IAC5C,8EAA8E;IAC9E,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAmDD;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,MAAM,QAAQ,EAC3B,WAAW,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,IAAI,GACjD,UAAU,EAAE,CA4Cd"}
1
+ {"version":3,"file":"route_map.d.ts","sourceRoot":"","sources":["../../src/tools/route_map.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D,gEAAgE;AAChE,MAAM,WAAW,KAAK;IACpB,yDAAyD;IACzD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sCAAsC;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,kDAAkD;IAClD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,4DAA4D;IAC5D,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,qDAAqD;AACrD,MAAM,MAAM,QAAQ,GAAG,SAAS,KAAK,EAAE,CAAC;AAExC,yEAAyE;AACzE,MAAM,WAAW,eAAgB,SAAQ,KAAK;IAC5C,8EAA8E;IAC9E,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAmDD;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,MAAM,QAAQ,EAC3B,WAAW,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,IAAI,GACjD,UAAU,EAAE,CA6Cd"}
@@ -1 +1 @@
1
- {"version":3,"file":"state_hook.d.ts","sourceRoot":"","sources":["../../src/tools/state_hook.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,uCAAuC;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC;IAC7B,0EAA0E;IAC1E,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC;IAC5D,uEAAuE;IACvE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC3C;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,SAAS,GAAG,UAAU,EAAE,CAmBlE"}
1
+ {"version":3,"file":"state_hook.d.ts","sourceRoot":"","sources":["../../src/tools/state_hook.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,uCAAuC;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC;IAC7B,0EAA0E;IAC1E,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC;IAC5D,uEAAuE;IACvE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC3C;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,SAAS,GAAG,UAAU,EAAE,CA4BlE"}
@@ -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:host([data-text-animation=\"fade\"]) .message--assistant {\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/* Inline confirmation card \u2014 lives in the transcript, no focus-stealing overlay. */\n.confirm {\n align-self: stretch;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 12px;\n background: var(--ag-ui-bg);\n border: 1px solid var(--ag-ui-accent);\n border-radius: 10px;\n}\n\n.confirm[data-resolved] {\n opacity: 0.7;\n border-color: var(--ag-ui-border);\n}\n\n.confirm-body {\n font-weight: 600;\n}\n\n.confirm-args {\n margin: 0;\n padding: 8px 10px;\n max-height: 140px;\n overflow: auto;\n font-size: 12px;\n font-family: ui-monospace, \"SF Mono\", Menlo, monospace;\n background: var(--ag-ui-assistant-bg);\n border-radius: 8px;\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n.confirm-actions {\n display: flex;\n gap: 8px;\n justify-content: flex-end;\n}\n\n.confirm-btn {\n border: 1px solid var(--ag-ui-border);\n border-radius: 8px;\n padding: 8px 14px;\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n background: var(--ag-ui-bg);\n color: var(--ag-ui-fg);\n}\n\n.confirm-btn:disabled {\n cursor: default;\n opacity: 0.6;\n}\n\n.confirm-btn--confirm {\n border-color: var(--ag-ui-accent);\n background: var(--ag-ui-accent);\n color: #ffffff;\n}\n\n/* Skills \u2014 chips row + the /-command palette, above the input. */\n.skill-chips {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n padding: 10px 12px;\n}\n\n.skill-chip {\n border: 1px solid var(--ag-ui-border);\n border-radius: 999px;\n padding: 4px 12px;\n font: inherit;\n font-size: 0.9em;\n cursor: pointer;\n background: var(--ag-ui-assistant-bg);\n color: var(--ag-ui-fg);\n}\n\n.skill-chip:hover {\n border-color: var(--ag-ui-accent);\n}\n\n.skill-palette {\n margin: 8px 12px 0;\n display: flex;\n flex-direction: column;\n max-height: 220px;\n overflow: auto;\n background: var(--ag-ui-bg);\n border: 1px solid var(--ag-ui-border);\n border-radius: 8px;\n box-shadow: 0 8px 24px rgba(20, 20, 50, 0.16);\n}\n\n.skill-item {\n display: flex;\n flex-direction: column;\n gap: 2px;\n align-items: flex-start;\n padding: 8px 12px;\n border: none;\n background: none;\n font: inherit;\n text-align: left;\n cursor: pointer;\n color: var(--ag-ui-fg);\n}\n\n.skill-item[aria-selected=\"true\"] {\n background: var(--ag-ui-assistant-bg);\n}\n\n.skill-item-title {\n font-weight: 600;\n}\n\n.skill-item-desc {\n font-size: 0.85em;\n color: var(--ag-ui-muted);\n}\n\n.skill-hint {\n margin: 8px 12px 0;\n font-size: 0.85em;\n color: var(--ag-ui-danger);\n}\n";
2
2
  //# sourceMappingURL=styles.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/ui/styles.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,MAAM,k/dAknBlB,CAAC"}
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/ui/styles.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,MAAM,4veAqnBlB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@artooi/ag-ui-web-component",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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",
@@ -149,9 +149,25 @@ export class AgUiChat extends HTMLElement {
149
149
  */
150
150
  skillContext: () => Record<string, unknown> = () => ({});
151
151
 
152
+ /**
153
+ * Friendly display labels for tool-call cards, keyed by tool name (e.g.
154
+ * `{ list_projects: "Search projects" }`). Used as a fallback when a tool has
155
+ * no `x-summary` in its own schema — chiefly **server-side tools** (drf-mcp,
156
+ * `@tool` registry), whose schema never reaches the browser (AG-UI streams
157
+ * only the tool-call name). Client tools should prefer `x-summary` on their
158
+ * schema; this map is the seam for everything else.
159
+ */
160
+ toolSummaries: Record<string, string> = {};
161
+
152
162
  readonly #toolRegistry = new ClientToolRegistry();
153
163
  /** Tool-call cards awaiting execution, keyed by call id. */
154
164
  readonly #toolCards = new Map<string, ToolCallCard>();
165
+ /**
166
+ * Call ids whose card was already settled from a streamed server-side result
167
+ * (`TOOL_CALL_RESULT`), so the post-run executeTool sweep doesn't overwrite
168
+ * the real output with the generic "executed on the server" fallback.
169
+ */
170
+ readonly #serverSettled = new Set<string>();
155
171
  readonly #root: ShadowRoot;
156
172
  readonly #chat: HTMLDivElement;
157
173
  readonly #messages: HTMLDivElement;
@@ -163,6 +179,10 @@ export class AgUiChat extends HTMLElement {
163
179
 
164
180
  #client: AgUiClient | null = null;
165
181
  #streamingBubble: HTMLDivElement | null = null;
182
+ // Text deltas applied to the current streaming bubble. >1 ⇒ the message
183
+ // revealed progressively as it streamed, so the word reveal must not re-animate
184
+ // it; ≤1 ⇒ it arrived at once and the word reveal is appropriate.
185
+ #streamDeltas = 0;
166
186
  #pending: HTMLDivElement | null = null;
167
187
  #threadId = "";
168
188
  #initialMessages: readonly Message[] = [];
@@ -233,7 +253,12 @@ export class AgUiChat extends HTMLElement {
233
253
  description:
234
254
  "Read the current page's structure (fields, buttons, route). Call after " +
235
255
  "acting to observe the result within the same turn.",
236
- parameters: { type: "object", properties: {}, required: [] },
256
+ parameters: {
257
+ type: "object",
258
+ properties: {},
259
+ required: [],
260
+ [X_SUMMARY_KEY]: "Read the page",
261
+ },
237
262
  handler: () => getPageMap(),
238
263
  },
239
264
  ];
@@ -410,6 +435,7 @@ export class AgUiChat extends HTMLElement {
410
435
  this.#streamingBubble = null;
411
436
  this.#hidePending();
412
437
  this.#toolCards.clear();
438
+ this.#serverSettled.clear();
413
439
  this.#initialMessages = [];
414
440
  this.#messages.replaceChildren();
415
441
  this.#threadId = this.conversationStore.threadId();
@@ -435,18 +461,62 @@ export class AgUiChat extends HTMLElement {
435
461
  }
436
462
  }
437
463
 
438
- /** Render a restored message as a chat bubble (text turns only). */
464
+ /**
465
+ * Replay a restored message: text bubbles *and* tool activity. An assistant
466
+ * turn may carry `toolCalls` (rendered as cards) and/or text; a `tool` turn
467
+ * carries a result that settles the matching card. So a refreshed page shows
468
+ * the full transcript — tool calls and their results — not just the prose.
469
+ */
439
470
  #renderHistoricMessage(message: Message): void {
440
- if (typeof message.content !== "string" || message.content === "") {
471
+ const text = typeof message.content === "string" ? message.content : "";
472
+ if (message.role === MESSAGE_ROLE.USER) {
473
+ if (text !== "") {
474
+ this.appendMessage(MESSAGE_ROLE.USER, text);
475
+ }
441
476
  return;
442
477
  }
443
- if (message.role === MESSAGE_ROLE.USER) {
444
- this.appendMessage(MESSAGE_ROLE.USER, message.content);
445
- } else if (message.role === MESSAGE_ROLE.ASSISTANT) {
446
- this.#revealWords(this.appendMessage(MESSAGE_ROLE.ASSISTANT, message.content));
478
+ if (message.role === MESSAGE_ROLE.ASSISTANT) {
479
+ if (text !== "") {
480
+ // Restored history must appear statically — entrance animations
481
+ // (fade / word) are for freshly-arriving messages. On reload the whole
482
+ // transcript mounts at once, so animating every bubble's text in
483
+ // parallel looks wrong. Mark it so the fade CSS skips it, and don't
484
+ // wrap words.
485
+ this.appendMessage(MESSAGE_ROLE.ASSISTANT, text).classList.add("message--restored");
486
+ }
487
+ const toolCalls = message.toolCalls;
488
+ if (toolCalls !== undefined) {
489
+ for (const call of toolCalls) {
490
+ this.#cardFor({
491
+ id: call.id,
492
+ name: call.function.name,
493
+ args: this.#parseArgs(call.function.arguments),
494
+ });
495
+ }
496
+ }
497
+ return;
498
+ }
499
+ if (message.role === "tool") {
500
+ const card = this.#toolCards.get(message.toolCallId);
501
+ if (card !== undefined) {
502
+ card.settle(TOOL_CALL_STATUS.DONE, message.content);
503
+ }
447
504
  }
448
505
  }
449
506
 
507
+ /** Parse a tool call's JSON `arguments` string from history into an object. */
508
+ #parseArgs(raw: string): Record<string, unknown> {
509
+ try {
510
+ const parsed: unknown = JSON.parse(raw);
511
+ if (typeof parsed === "object" && parsed !== null) {
512
+ return parsed as Record<string, unknown>;
513
+ }
514
+ } catch {
515
+ // Malformed history — fall back to empty args rather than failing replay.
516
+ }
517
+ return {};
518
+ }
519
+
450
520
  /**
451
521
  * Word-by-word reveal for the `word` text-animation mode, applied to a
452
522
  * completed assistant bubble. `fade` is pure CSS (no JS); `none` is a no-op.
@@ -638,9 +708,16 @@ export class AgUiChat extends HTMLElement {
638
708
  this.#toolCards.delete(call.id);
639
709
  const tool = this.#resolveTool(call.name);
640
710
  if (tool === null) {
641
- // A server-side tool the server already executed — not ours to re-run.
642
- card.settle(TOOL_CALL_STATUS.DONE, "Executed on the server.");
643
- this.#showPending();
711
+ // Not a client tool. A server-side tool's real output arrives via
712
+ // `onToolResult` (TOOL_CALL_RESULT) and already settled the card — only
713
+ // fall back when it didn't. When no result ever arrived, the call wasn't
714
+ // executed by either side (no handler, no server result), so say so
715
+ // honestly rather than claiming server execution. We do NOT show the
716
+ // pending indicator: nothing here triggers another client round, so it
717
+ // would hang after the run ended.
718
+ if (!this.#serverSettled.has(call.id)) {
719
+ card.settle(TOOL_CALL_STATUS.DONE, "No result returned.");
720
+ }
644
721
  return null;
645
722
  }
646
723
  if (await this.#needsConfirmation(call, tool)) {
@@ -699,15 +776,31 @@ export class AgUiChat extends HTMLElement {
699
776
  onTextDelta: (buffer) => {
700
777
  this.#hidePending();
701
778
  this.#streamInto(buffer);
779
+ this.#streamDeltas += 1;
702
780
  },
703
781
  onTextEnd: (buffer) => {
704
- this.#revealWords(this.#streamInto(buffer));
782
+ const bubble = this.#streamInto(buffer);
783
+ // Only reveal word-by-word when the message arrived at once. If it
784
+ // streamed across multiple deltas it already revealed progressively, so
785
+ // wrapping it now would re-animate the whole message — the awkward
786
+ // "finished response replays one word at a time" bug.
787
+ if (this.#streamDeltas <= 1) {
788
+ this.#revealWords(bubble);
789
+ }
705
790
  this.#streamingBubble = null;
706
791
  },
707
792
  onToolCall: (call) => {
708
793
  this.#hidePending();
709
794
  this.#cardFor(call);
710
795
  },
796
+ onToolResult: (toolCallId, content) => {
797
+ const card = this.#toolCards.get(toolCallId);
798
+ if (card === undefined) {
799
+ return;
800
+ }
801
+ card.settle(TOOL_CALL_STATUS.DONE, content);
802
+ this.#serverSettled.add(toolCallId);
803
+ },
711
804
  onRunEnd: () => {
712
805
  this.#hidePending();
713
806
  this.#send.disabled = false;
@@ -719,6 +812,12 @@ export class AgUiChat extends HTMLElement {
719
812
  this.#send.disabled = false;
720
813
  this.#streamingBubble = null;
721
814
  },
815
+ onSettled: () => {
816
+ // Terminal guarantee: whatever path ended the run, return to rest.
817
+ this.#hidePending();
818
+ this.#send.disabled = false;
819
+ this.#streamingBubble = null;
820
+ },
722
821
  };
723
822
  }
724
823
 
@@ -754,6 +853,7 @@ export class AgUiChat extends HTMLElement {
754
853
  #streamInto(buffer: string): HTMLDivElement {
755
854
  if (this.#streamingBubble === null) {
756
855
  this.#streamingBubble = this.appendMessage(MESSAGE_ROLE.ASSISTANT, "");
856
+ this.#streamDeltas = 0;
757
857
  }
758
858
  this.#streamingBubble.innerHTML = renderMarkdown(buffer);
759
859
  this.#messages.scrollTop = this.#messages.scrollHeight;
@@ -771,13 +871,11 @@ export class AgUiChat extends HTMLElement {
771
871
  if (existing !== undefined) {
772
872
  return existing;
773
873
  }
774
- const summary = this.#resolveTool(call.name)?.parameters[X_SUMMARY_KEY];
775
- const card = new ToolCallCard(
776
- call.name,
777
- call.args,
778
- this.toolDisplay,
779
- typeof summary === "string" ? summary : undefined,
780
- );
874
+ // Prefer the tool's own `x-summary`; fall back to the `toolSummaries` map
875
+ // for server-side tools whose schema never reached the browser.
876
+ const labelled = this.#resolveTool(call.name)?.parameters[X_SUMMARY_KEY];
877
+ const summary = typeof labelled === "string" ? labelled : this.toolSummaries[call.name];
878
+ const card = new ToolCallCard(call.name, call.args, this.toolDisplay, summary);
781
879
  this.#toolCards.set(call.id, card);
782
880
  this.#messages.appendChild(card.element);
783
881
  this.#messages.scrollTop = this.#messages.scrollHeight;
@@ -45,8 +45,21 @@ export interface AgUiClientHandlers {
45
45
  onTextEnd(buffer: string): void;
46
46
  /** Fired when the agent finishes calling a tool (server- or frontend-side). */
47
47
  onToolCall(call: AgUiToolCall): void;
48
+ /**
49
+ * Fired when a server-side tool's result streams back (AG-UI's
50
+ * `TOOL_CALL_RESULT`). Frontend tools don't emit this — the client supplies
51
+ * their result itself — so this is the channel for server-executed output.
52
+ */
53
+ onToolResult(toolCallId: string, content: string): void;
48
54
  onRunEnd(): void;
49
55
  onError(message: string): void;
56
+ /**
57
+ * Fired exactly once when the whole interaction settles — after the run loop
58
+ * 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.
61
+ */
62
+ onSettled(): void;
50
63
  }
51
64
 
52
65
  /**
@@ -140,6 +153,8 @@ export class AgUiClient {
140
153
  await this.#runLoop();
141
154
  } catch (error) {
142
155
  this.#handlers.onError(error instanceof Error ? error.message : String(error));
156
+ } finally {
157
+ this.#handlers.onSettled();
143
158
  }
144
159
  }
145
160
 
@@ -201,6 +216,9 @@ export class AgUiClient {
201
216
  pending.push(call);
202
217
  h.onToolCall(call);
203
218
  },
219
+ onToolCallResultEvent({ event }) {
220
+ h.onToolResult(event.toolCallId, event.content);
221
+ },
204
222
  onRunErrorEvent({ event }) {
205
223
  h.onError(event.message);
206
224
  },
@@ -1,4 +1,4 @@
1
- import { X_NAVIGATES_KEY } from "../constants.js";
1
+ import { X_NAVIGATES_KEY, X_SUMMARY_KEY } from "../constants.js";
2
2
  import type { ClientTool } from "./client_tool_registry.js";
3
3
 
4
4
  /** A single navigable route the host declares for the agent. */
@@ -100,7 +100,7 @@ export function createRouteTools(
100
100
  description:
101
101
  "List the routes the app can navigate to. Each route's `pathParams` " +
102
102
  "names the dynamic segments to pass as `params` to `navigate_to_route`.",
103
- parameters: { type: "object", properties: {}, required: [] },
103
+ parameters: { type: "object", properties: {}, required: [], [X_SUMMARY_KEY]: "List pages" },
104
104
  handler: (): RouteWithParams[] =>
105
105
  getRouteMap().map((route) => ({ ...route, pathParams: pathParamNames(route.path) })),
106
106
  },
@@ -117,6 +117,7 @@ export function createRouteTools(
117
117
  },
118
118
  required: ["route_id"],
119
119
  [X_NAVIGATES_KEY]: true,
120
+ [X_SUMMARY_KEY]: "Navigate",
120
121
  },
121
122
  handler: (args) => {
122
123
  const routeId = args["route_id"];
@@ -1,4 +1,4 @@
1
- import { X_DESTRUCTIVE_KEY } from "../constants.js";
1
+ import { X_DESTRUCTIVE_KEY, X_SUMMARY_KEY } from "../constants.js";
2
2
  import type { ClientTool } from "./client_tool_registry.js";
3
3
 
4
4
  /**
@@ -27,7 +27,12 @@ export function createStateHookTools(hook: StateHook): ClientTool[] {
27
27
  {
28
28
  name: `read_${hook.name}`,
29
29
  description: `Read the "${hook.name}" state.`,
30
- parameters: { type: "object", properties: {}, required: [] },
30
+ parameters: {
31
+ type: "object",
32
+ properties: {},
33
+ required: [],
34
+ [X_SUMMARY_KEY]: `Read ${hook.name}`,
35
+ },
31
36
  handler: () => hook.read(),
32
37
  },
33
38
  ];
@@ -36,7 +41,11 @@ export function createStateHookTools(hook: StateHook): ClientTool[] {
36
41
  tools.push({
37
42
  name: `set_${hook.name}`,
38
43
  description: `Update the "${hook.name}" state.`,
39
- parameters: { ...(hook.schema ?? { type: "object" }), [X_DESTRUCTIVE_KEY]: true },
44
+ parameters: {
45
+ ...(hook.schema ?? { type: "object" }),
46
+ [X_DESTRUCTIVE_KEY]: true,
47
+ [X_SUMMARY_KEY]: `Update ${hook.name}`,
48
+ },
40
49
  handler: (args) => write(args),
41
50
  });
42
51
  }
package/src/ui/styles.ts CHANGED
@@ -241,7 +241,10 @@ export const STYLES = `
241
241
  }
242
242
 
243
243
  /* ── Incoming-text animations (data-text-animation) ─────────────────────── */
244
- :host([data-text-animation="fade"]) .message--assistant {
244
+ /* .message--restored (rehydrated history) is excluded: entrance animations are
245
+ for freshly-arriving messages, not the whole transcript replaying on reload.
246
+ Word mode is excluded implicitly — restored bubbles aren't wrapped. */
247
+ :host([data-text-animation="fade"]) .message--assistant:not(.message--restored) {
245
248
  animation: ag-ui-msg-fade 0.25s ease both;
246
249
  }
247
250
 
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION: string = "0.2.0";
1
+ export const VERSION: string = "0.2.2";