@artooi/ag-ui-web-component 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -1
- package/README.md +69 -2
- package/dist/ag-ui-web-component.bundle.js +155 -43
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +16 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +17 -1
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +7 -1
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/core/attachment.d.ts +35 -0
- package/dist/core/attachment.d.ts.map +1 -0
- package/dist/core/upload_attachment.d.ts +32 -0
- package/dist/core/upload_attachment.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +578 -20
- package/dist/index.js.map +4 -4
- package/dist/ui/attachment_chips.d.ts +13 -0
- package/dist/ui/attachment_chips.d.ts.map +1 -0
- package/dist/ui/attachment_tray.d.ts +42 -0
- package/dist/ui/attachment_tray.d.ts.map +1 -0
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +18 -0
- package/src/core/ag_ui_chat.ts +178 -12
- package/src/core/agui_client.ts +15 -2
- package/src/core/attachment.ts +39 -0
- package/src/core/upload_attachment.ts +113 -0
- package/src/index.ts +6 -0
- package/src/ui/attachment_chips.ts +68 -0
- package/src/ui/attachment_tray.ts +237 -0
- package/src/ui/styles.ts +110 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AttachmentRef } from "../core/attachment.js";
|
|
2
|
+
/**
|
|
3
|
+
* Render the read-only attachment chips shown on a sent user message bubble
|
|
4
|
+
* (and on restored history) — one chip per ref with a type icon, the filename,
|
|
5
|
+
* and a human size. Static by design: no progress, no remove (that lives in the
|
|
6
|
+
* composer tray); a restored bubble re-renders these with no animation.
|
|
7
|
+
*/
|
|
8
|
+
export declare function renderAttachmentChips(refs: readonly AttachmentRef[]): HTMLDivElement;
|
|
9
|
+
/** A coarse type icon for a chip — image, document, or generic file. */
|
|
10
|
+
export declare function iconFor(mime: string): string;
|
|
11
|
+
/** A compact human-readable byte size (e.g. `1.2 MB`). */
|
|
12
|
+
export declare function formatBytes(bytes: number): string;
|
|
13
|
+
//# sourceMappingURL=attachment_chips.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment_chips.d.ts","sourceRoot":"","sources":["../../src/ui/attachment_chips.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,SAAS,aAAa,EAAE,GAAG,cAAc,CAOpF;AAwBD,wEAAwE;AACxE,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAW5C;AAED,0DAA0D;AAC1D,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAajD"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { AttachmentRef } from "../core/attachment.js";
|
|
2
|
+
import type { UploadHandler } from "../core/upload_attachment.js";
|
|
3
|
+
/** Config the host ({@link AgUiChat}) hands the tray. */
|
|
4
|
+
export interface AttachmentTrayConfig {
|
|
5
|
+
/** Upload one file, reporting `0..1` progress; resolves to a durable ref. */
|
|
6
|
+
readonly upload: UploadHandler;
|
|
7
|
+
/** Client-side size cap in bytes (`0` = no cap). The server stays authoritative. */
|
|
8
|
+
readonly maxBytes: number;
|
|
9
|
+
/** Client-side accept list (`<input accept>` syntax; `""` = any). */
|
|
10
|
+
readonly accept: string;
|
|
11
|
+
/** Fired when the set of attachments changes (add / settle / remove). */
|
|
12
|
+
readonly onChange?: () => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The composer's pending-attachments tray: a chip per picked file with a
|
|
16
|
+
* progress bar while it uploads, settling to a ready chip (holding the durable
|
|
17
|
+
* ref) or an error chip (with retry). A *stateful view* in the spirit of
|
|
18
|
+
* {@link ThreadDrawer} — the host appends {@link element}, calls {@link add} on
|
|
19
|
+
* pick/drop, reads {@link readyRefs} when the user sends, and clears it.
|
|
20
|
+
*
|
|
21
|
+
* Client-side size/type guards reject a bad file into an error chip without
|
|
22
|
+
* uploading — instant feedback, but the server is the authority.
|
|
23
|
+
*/
|
|
24
|
+
export declare class AttachmentTray {
|
|
25
|
+
#private;
|
|
26
|
+
/** The tray root; append above the input row. Hidden while empty. */
|
|
27
|
+
readonly element: HTMLDivElement;
|
|
28
|
+
constructor(config: AttachmentTrayConfig);
|
|
29
|
+
/** Queue a file: reject oversize/disallowed into an error chip, else upload. */
|
|
30
|
+
add(file: File): void;
|
|
31
|
+
/** The durable refs of every chip that finished uploading. */
|
|
32
|
+
readyRefs(): readonly AttachmentRef[];
|
|
33
|
+
/** Whether any chip is still uploading (a send would drop nothing if false). */
|
|
34
|
+
hasPending(): boolean;
|
|
35
|
+
/** Whether the tray holds no chips. */
|
|
36
|
+
isEmpty(): boolean;
|
|
37
|
+
/** Drop the settled (ready / error) chips, leaving any still uploading. */
|
|
38
|
+
clearReady(): void;
|
|
39
|
+
/** Drop every chip (a reset / new-chat). */
|
|
40
|
+
clear(): void;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=attachment_tray.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment_tray.d.ts","sourceRoot":"","sources":["../../src/ui/attachment_tray.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAMlE,yDAAyD;AACzD,MAAM,WAAW,oBAAoB;IACnC,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,oFAAoF;IACpF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;CAChC;AAYD;;;;;;;;;GASG;AACH,qBAAa,cAAc;;IACzB,qEAAqE;IACrE,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;gBAKrB,MAAM,EAAE,oBAAoB;IAOxC,gFAAgF;IAChF,GAAG,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI;IAuBrB,8DAA8D;IAC9D,SAAS,IAAI,SAAS,aAAa,EAAE;IAUrC,gFAAgF;IAChF,UAAU,IAAI,OAAO;IAIrB,uCAAuC;IACvC,OAAO,IAAI,OAAO;IAIlB,2EAA2E;IAC3E,UAAU,IAAI,IAAI;IAKlB,4CAA4C;IAC5C,KAAK,IAAI,IAAI;CA2Gd"}
|
package/dist/ui/styles.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const STYLES = "\n:host {\n /* Colors */\n --ag-ui-bg: #ffffff;\n --ag-ui-fg: #1a1a2e;\n --ag-ui-accent: #4f46e5;\n --ag-ui-user-bg: #4f46e5;\n --ag-ui-user-fg: #ffffff;\n --ag-ui-assistant-bg: #f1f1f6;\n --ag-ui-input-bg: var(--ag-ui-bg);\n --ag-ui-tool-bg: var(--ag-ui-assistant-bg);\n --ag-ui-tool-fg: var(--ag-ui-accent);\n --ag-ui-header-bg: var(--ag-ui-accent);\n --ag-ui-header-fg: #ffffff;\n --ag-ui-border: #e2e2ec;\n --ag-ui-radius: 12px;\n\n /* Status accents for tool-call cards. */\n --ag-ui-success: #15803d;\n --ag-ui-danger: #b91c1c;\n --ag-ui-muted: #6b7280;\n\n /* Surface \u2014 set --ag-ui-shadow: none for a flush, embedded panel. */\n --ag-ui-shadow: 0 12px 32px rgba(20, 20, 50, 0.18);\n --ag-ui-font: inherit;\n --ag-ui-font-size: 14px;\n --ag-ui-code-font: ui-monospace, \"SF Mono\", Menlo, monospace;\n\n /* Spacing \u2014 the density preset overrides these. */\n --ag-ui-space: 10px;\n --ag-ui-pad: 16px;\n --ag-ui-msg-pad: 8px 12px;\n --ag-ui-msg-radius: 14px;\n\n /* Layout \u2014 override from outside to dock the widget anywhere.\n Set --ag-ui-position: static (and place this element in your own\n grid/flex layout) to embed it in the page flow instead of floating. */\n --ag-ui-position: fixed;\n --ag-ui-z-index: 2147483000;\n --ag-ui-width: 380px;\n --ag-ui-height: 560px;\n --ag-ui-inset: auto 24px 24px auto;\n --ag-ui-max-width: calc(100vw - 48px);\n --ag-ui-max-height: calc(100vh - 48px);\n\n position: var(--ag-ui-position);\n inset: var(--ag-ui-inset);\n z-index: var(--ag-ui-z-index);\n width: var(--ag-ui-width);\n max-width: var(--ag-ui-max-width);\n height: var(--ag-ui-height);\n max-height: var(--ag-ui-max-height);\n display: flex;\n font-family: var(--ag-ui-font);\n font-size: var(--ag-ui-font-size);\n color: var(--ag-ui-fg);\n}\n\n/* \u2500\u2500 Themes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Themes only re-set the colour variables; layout/spacing are unaffected.\n theme=\"auto\" follows the OS via prefers-color-scheme. */\n:host([theme=\"dark\"]) {\n --ag-ui-bg: #15151f;\n --ag-ui-fg: #e8e8f2;\n --ag-ui-assistant-bg: #25253a;\n --ag-ui-header-bg: #1f1f30;\n --ag-ui-header-fg: #e8e8f2;\n --ag-ui-border: #33334a;\n --ag-ui-muted: #9aa0b4;\n --ag-ui-shadow: 0 12px 32px rgba(0, 0, 0, 0.5);\n}\n\n@media (prefers-color-scheme: dark) {\n :host([theme=\"auto\"]) {\n --ag-ui-bg: #15151f;\n --ag-ui-fg: #e8e8f2;\n --ag-ui-assistant-bg: #25253a;\n --ag-ui-header-bg: #1f1f30;\n --ag-ui-header-fg: #e8e8f2;\n --ag-ui-border: #33334a;\n --ag-ui-muted: #9aa0b4;\n --ag-ui-shadow: 0 12px 32px rgba(0, 0, 0, 0.5);\n }\n}\n\n/* A terminal-flavoured \"code\" theme: dark, monospace, green accent. */\n:host([theme=\"code\"]) {\n --ag-ui-bg: #0d1117;\n --ag-ui-fg: #c9d1d9;\n --ag-ui-accent: #3fb950;\n --ag-ui-user-bg: #238636;\n --ag-ui-assistant-bg: #161b22;\n --ag-ui-header-bg: #010409;\n --ag-ui-header-fg: #c9d1d9;\n --ag-ui-border: #30363d;\n --ag-ui-muted: #8b949e;\n --ag-ui-font: var(--ag-ui-code-font);\n --ag-ui-shadow: 0 12px 32px rgba(0, 0, 0, 0.6);\n}\n\n/* \u2500\u2500 Density \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n:host([density=\"compact\"]) {\n --ag-ui-font-size: 13px;\n --ag-ui-space: 6px;\n --ag-ui-pad: 10px;\n --ag-ui-msg-pad: 5px 9px;\n --ag-ui-msg-radius: 10px;\n}\n\n/* \u2500\u2500 Placement presets \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n:host([placement=\"bottom-left\"]) {\n --ag-ui-inset: auto auto 24px 24px;\n}\n\n:host([placement=\"side\"]) {\n --ag-ui-inset: 0 0 0 auto;\n --ag-ui-width: 420px;\n --ag-ui-height: 100vh;\n --ag-ui-max-height: 100vh;\n --ag-ui-radius: 0;\n}\n\n:host([placement=\"full\"]) {\n --ag-ui-inset: 0;\n --ag-ui-width: 100vw;\n --ag-ui-height: 100vh;\n --ag-ui-max-width: 100vw;\n --ag-ui-max-height: 100vh;\n --ag-ui-radius: 0;\n}\n\n/* Embedded: drop the floating chrome and the high z-index stacking context so\n the widget lives in the host's own layout (fixes overlay/z-index clashes). */\n:host([placement=\"embedded\"]) {\n --ag-ui-position: static;\n --ag-ui-width: 100%;\n --ag-ui-height: 100%;\n --ag-ui-max-width: 100%;\n --ag-ui-max-height: 100%;\n --ag-ui-shadow: none;\n --ag-ui-z-index: auto;\n}\n\n.chat {\n position: relative;\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n background: var(--ag-ui-bg);\n border: 1px solid var(--ag-ui-border);\n border-radius: var(--ag-ui-radius);\n box-shadow: var(--ag-ui-shadow);\n overflow: hidden;\n}\n\n.header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 10px 14px;\n border-bottom: 1px solid var(--ag-ui-border);\n background: var(--ag-ui-header-bg);\n color: var(--ag-ui-header-fg);\n}\n\n.header-title {\n font-weight: 600;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.header-controls {\n display: flex;\n gap: 2px;\n flex: none;\n}\n\n.header-btn {\n border: none;\n background: transparent;\n color: inherit;\n font: inherit;\n line-height: 1;\n padding: 4px 7px;\n border-radius: 6px;\n cursor: pointer;\n opacity: 0.85;\n}\n\n.header-btn:hover {\n opacity: 1;\n background: rgba(255, 255, 255, 0.18);\n}\n\n/* Collapsed: keep just the header bar \u2014 hide the whole body, and let the host\n shrink to the header. (A host can also fully hide the element itself.) */\n:host([collapsed]) {\n height: auto;\n max-height: none;\n}\n\n:host([collapsed]) .messages,\n:host([collapsed]) .input-row,\n:host([collapsed]) .skill-chips,\n:host([collapsed]) .skill-palette,\n:host([collapsed]) .skill-hint {\n display: none;\n}\n\n/* Edge-docked placements pin top *and* bottom, which would otherwise keep the\n panel full-height when collapsed; unpin the bottom so it shrinks to the\n header. (Floating/bottom-left pin only the bottom, so they already shrink.) */\n:host([collapsed][placement=\"side\"]),\n:host([collapsed][placement=\"full\"]) {\n bottom: auto;\n}\n\n.messages {\n flex: 1;\n overflow-y: auto;\n padding: var(--ag-ui-pad);\n display: flex;\n flex-direction: column;\n gap: var(--ag-ui-space);\n}\n\n.message {\n max-width: 80%;\n padding: var(--ag-ui-msg-pad);\n border-radius: var(--ag-ui-msg-radius);\n line-height: 1.4;\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n/* \u2500\u2500 Incoming-text animations (data-text-animation) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n/* .message--restored (rehydrated history) is excluded: entrance animations are\n for freshly-arriving messages, not the whole transcript replaying on reload.\n Word mode is excluded implicitly \u2014 restored bubbles aren't wrapped. */\n:host([data-text-animation=\"fade\"]) .message--assistant:not(.message--restored) {\n animation: ag-ui-msg-fade 0.25s ease both;\n}\n\n@keyframes ag-ui-msg-fade {\n from { opacity: 0; transform: translateY(4px); }\n to { opacity: 1; transform: none; }\n}\n\n.message--assistant .word {\n animation: ag-ui-word-in 0.3s ease both;\n animation-delay: calc(var(--ag-ui-word-index, 0) * 35ms);\n}\n\n@keyframes ag-ui-word-in {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n :host([data-text-animation=\"fade\"]) .message--assistant,\n .message--assistant .word {\n animation: none;\n }\n}\n\n.message--user {\n align-self: flex-end;\n background: var(--ag-ui-user-bg);\n color: var(--ag-ui-user-fg);\n border-bottom-right-radius: 4px;\n}\n\n.message--assistant {\n align-self: flex-start;\n background: var(--ag-ui-assistant-bg);\n border-bottom-left-radius: 4px;\n /* Assistant bubbles hold rendered markdown/HTML, so collapse the source\n whitespace the renderer leaves between block tags. */\n white-space: normal;\n}\n\n/* Rendered-markdown elements inside an assistant bubble. */\n.message--assistant > :first-child {\n margin-top: 0;\n}\n\n.message--assistant > :last-child {\n margin-bottom: 0;\n}\n\n.message--assistant p,\n.message--assistant ul,\n.message--assistant ol,\n.message--assistant blockquote,\n.message--assistant pre {\n margin: 0.5em 0;\n}\n\n.message--assistant ul,\n.message--assistant ol {\n padding-left: 1.3em;\n}\n\n.message--assistant a {\n color: var(--ag-ui-accent);\n text-decoration: underline;\n}\n\n.message--assistant code {\n font-family: ui-monospace, \"SF Mono\", Menlo, monospace;\n font-size: 0.92em;\n background: rgba(127, 127, 127, 0.16);\n padding: 1px 4px;\n border-radius: 4px;\n}\n\n.message--assistant pre {\n padding: 8px 10px;\n overflow: auto;\n background: var(--ag-ui-bg);\n border: 1px solid var(--ag-ui-border);\n border-radius: 6px;\n}\n\n.message--assistant pre code {\n background: none;\n padding: 0;\n}\n\n.message--assistant blockquote {\n padding-left: 10px;\n border-left: 3px solid var(--ag-ui-border);\n color: var(--ag-ui-muted);\n}\n\n.pending {\n align-self: flex-start;\n display: inline-flex;\n gap: 4px;\n align-items: center;\n padding: 12px 14px;\n background: var(--ag-ui-assistant-bg);\n border-radius: 14px;\n border-bottom-left-radius: 4px;\n}\n\n.pending-dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: var(--ag-ui-muted);\n animation: ag-ui-pending 1.2s infinite ease-in-out both;\n}\n\n.pending-dot:nth-child(2) {\n animation-delay: 0.16s;\n}\n\n.pending-dot:nth-child(3) {\n animation-delay: 0.32s;\n}\n\n@keyframes ag-ui-pending {\n 0%, 80%, 100% { opacity: 0.3; transform: translateY(0); }\n 40% { opacity: 1; transform: translateY(-3px); }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .pending-dot {\n animation: none;\n opacity: 0.6;\n }\n}\n\n.tool-call {\n align-self: flex-start;\n max-width: 80%;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n gap: 6px;\n font-size: 12px;\n font-family: ui-monospace, \"SF Mono\", Menlo, monospace;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--ag-ui-tool-bg);\n border: 1px solid var(--ag-ui-border);\n color: var(--ag-ui-tool-fg);\n}\n\n.tool-call-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n\n.tool-call-name {\n font-weight: 600;\n word-break: break-word;\n}\n\n.tool-call-status {\n flex: none;\n padding: 1px 8px;\n border-radius: 999px;\n font-size: 11px;\n font-weight: 600;\n background: rgba(127, 127, 127, 0.16);\n color: var(--ag-ui-muted);\n}\n\n.tool-call[data-status=\"done\"] .tool-call-status {\n color: var(--ag-ui-success);\n}\n\n.tool-call[data-status=\"error\"] .tool-call-status {\n color: var(--ag-ui-danger);\n}\n\n.tool-call[data-status=\"declined\"] .tool-call-status {\n color: var(--ag-ui-muted);\n}\n\n.tool-call-args,\n.tool-call-result {\n margin: 0;\n padding: 6px 8px;\n max-height: 160px;\n overflow: auto;\n background: var(--ag-ui-bg);\n border: 1px solid var(--ag-ui-border);\n border-radius: 6px;\n white-space: pre-wrap;\n word-break: break-word;\n color: var(--ag-ui-fg);\n}\n\n.tool-call-toggle {\n align-self: flex-start;\n border: none;\n padding: 0;\n background: none;\n font: inherit;\n font-weight: 600;\n color: var(--ag-ui-accent);\n cursor: pointer;\n}\n\n.tool-call-toggle::before {\n content: \"\u25B8 \";\n}\n\n.tool-call-toggle[aria-expanded=\"true\"]::before {\n content: \"\u25BE \";\n}\n\n.input-row {\n display: flex;\n gap: 8px;\n padding: 12px;\n border-top: 1px solid var(--ag-ui-border);\n}\n\n.input {\n flex: 1;\n resize: none;\n background: var(--ag-ui-input-bg);\n border: 1px solid var(--ag-ui-border);\n border-radius: 8px;\n padding: 8px 10px;\n font: inherit;\n color: inherit;\n outline: none;\n}\n\n.input:focus {\n border-color: var(--ag-ui-accent);\n}\n\n.send {\n border: none;\n border-radius: 8px;\n padding: 0 16px;\n background: var(--ag-ui-accent);\n color: #ffffff;\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n}\n\n.send:disabled {\n opacity: 0.5;\n cursor: default;\n}\n\n/* 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";
|
|
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/* \u2500\u2500 File attachments \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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/* The \uD83D\uDCCE picker button sits left of the input; hidden until data-attachments-url. */\n.attach-btn {\n border: 1px solid var(--ag-ui-border);\n border-radius: 8px;\n padding: 0 10px;\n background: var(--ag-ui-input-bg);\n color: inherit;\n font: inherit;\n cursor: pointer;\n}\n\n.attach-btn:hover {\n border-color: var(--ag-ui-accent);\n}\n\n.attach-input {\n display: none;\n}\n\n/* Pending-attachments tray, above the input row; collapses (hidden) when empty. */\n.attachment-slot {\n display: contents;\n}\n\n.attachment-tray {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n padding: 8px 12px 0;\n}\n\n.attachment-chips {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n margin-top: 6px;\n}\n\n.attachment-chip {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n max-width: 100%;\n padding: 4px 8px;\n border: 1px solid var(--ag-ui-border);\n border-radius: 999px;\n background: var(--ag-ui-assistant-bg);\n font-size: 0.85em;\n position: relative;\n}\n\n.attachment-chip--error {\n border-color: var(--ag-ui-danger);\n color: var(--ag-ui-danger);\n}\n\n.attachment-chip-name {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n max-width: 14ch;\n}\n\n.attachment-chip-size {\n color: var(--ag-ui-muted);\n white-space: nowrap;\n}\n\n.attachment-chip--error .attachment-chip-size {\n color: var(--ag-ui-danger);\n}\n\n/* The progress bar fills as the file uploads. */\n.attachment-chip-bar {\n flex-basis: 100%;\n height: 3px;\n border-radius: 2px;\n background: var(--ag-ui-border);\n overflow: hidden;\n}\n\n.attachment-chip-bar-fill {\n height: 100%;\n background: var(--ag-ui-accent);\n transition: width 0.15s ease;\n}\n\n.attachment-chip-remove,\n.attachment-chip-retry {\n border: none;\n background: none;\n color: inherit;\n cursor: pointer;\n padding: 0;\n line-height: 1;\n opacity: 0.7;\n}\n\n.attachment-chip-remove:hover,\n.attachment-chip-retry:hover {\n opacity: 1;\n}\n\n/* A subtle outline while a file is dragged over the shell. */\n.chat--dragover {\n outline: 2px dashed var(--ag-ui-accent);\n outline-offset: -4px;\n}\n\n/* Muted \"\u23F9 Stopped\" line after a cancelled run \u2014 a note, not an error bubble. */\n.stopped-note {\n align-self: flex-start;\n color: var(--ag-ui-muted);\n font-size: 12px;\n padding: 2px 4px;\n}\n\n/* Inline confirmation card \u2014 lives in the transcript, no focus-stealing overlay. */\n.confirm {\n align-self: stretch;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 12px;\n background: var(--ag-ui-bg);\n border: 1px solid var(--ag-ui-accent);\n border-radius: 10px;\n}\n\n.confirm[data-resolved] {\n opacity: 0.7;\n border-color: var(--ag-ui-border);\n}\n\n.confirm-body {\n font-weight: 600;\n}\n\n.confirm-args {\n margin: 0;\n padding: 8px 10px;\n max-height: 140px;\n overflow: auto;\n font-size: 12px;\n font-family: ui-monospace, \"SF Mono\", Menlo, monospace;\n background: var(--ag-ui-assistant-bg);\n border-radius: 8px;\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n.confirm-actions {\n display: flex;\n gap: 8px;\n justify-content: flex-end;\n}\n\n.confirm-btn {\n border: 1px solid var(--ag-ui-border);\n border-radius: 8px;\n padding: 8px 14px;\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n background: var(--ag-ui-bg);\n color: var(--ag-ui-fg);\n}\n\n.confirm-btn:disabled {\n cursor: default;\n opacity: 0.6;\n}\n\n.confirm-btn--confirm {\n border-color: var(--ag-ui-accent);\n background: var(--ag-ui-accent);\n color: #ffffff;\n}\n\n/* Skills \u2014 chips row + the /-command palette, above the input. */\n.skill-chips {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n padding: 10px 12px;\n}\n\n.skill-chip {\n border: 1px solid var(--ag-ui-border);\n border-radius: 999px;\n padding: 4px 12px;\n font: inherit;\n font-size: 0.9em;\n cursor: pointer;\n background: var(--ag-ui-assistant-bg);\n color: var(--ag-ui-fg);\n}\n\n.skill-chip:hover {\n border-color: var(--ag-ui-accent);\n}\n\n.skill-palette {\n margin: 8px 12px 0;\n display: flex;\n flex-direction: column;\n max-height: 220px;\n overflow: auto;\n background: var(--ag-ui-bg);\n border: 1px solid var(--ag-ui-border);\n border-radius: 8px;\n box-shadow: 0 8px 24px rgba(20, 20, 50, 0.16);\n}\n\n.skill-item {\n display: flex;\n flex-direction: column;\n gap: 2px;\n align-items: flex-start;\n padding: 8px 12px;\n border: none;\n background: none;\n font: inherit;\n text-align: left;\n cursor: pointer;\n color: var(--ag-ui-fg);\n}\n\n.skill-item[aria-selected=\"true\"] {\n background: var(--ag-ui-assistant-bg);\n}\n\n.skill-item-title {\n font-weight: 600;\n}\n\n.skill-item-desc {\n font-size: 0.85em;\n color: var(--ag-ui-muted);\n}\n\n.skill-hint {\n margin: 8px 12px 0;\n font-size: 0.85em;\n color: var(--ag-ui-danger);\n}\n\n/* Chat-history drawer \u2014 a slide-over within the chat panel. */\n.drawer {\n position: absolute;\n inset: 0;\n z-index: 5;\n display: flex;\n}\n\n.drawer[hidden] {\n display: none;\n}\n\n.drawer-backdrop {\n position: absolute;\n inset: 0;\n background: rgba(20, 20, 50, 0.32);\n}\n\n.drawer-panel {\n position: relative;\n display: flex;\n flex-direction: column;\n width: min(300px, 85%);\n height: 100%;\n background: var(--ag-ui-bg);\n border-right: 1px solid var(--ag-ui-border);\n box-shadow: var(--ag-ui-shadow);\n overflow: hidden;\n}\n\n.drawer-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--ag-ui-space);\n padding: var(--ag-ui-pad);\n border-bottom: 1px solid var(--ag-ui-border);\n}\n\n.drawer-title {\n font-weight: 600;\n}\n\n.drawer-new {\n border: 1px solid var(--ag-ui-border);\n border-radius: var(--ag-ui-radius);\n background: var(--ag-ui-bg);\n color: var(--ag-ui-accent);\n padding: 4px 10px;\n font: inherit;\n font-size: 0.85em;\n cursor: pointer;\n}\n\n.drawer-list {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n}\n\n.drawer-empty {\n padding: var(--ag-ui-pad);\n font-size: 0.9em;\n color: var(--ag-ui-muted);\n}\n\n.drawer-row {\n display: flex;\n align-items: stretch;\n border-bottom: 1px solid var(--ag-ui-border);\n}\n\n.drawer-row--active {\n background: var(--ag-ui-assistant-bg);\n}\n\n.drawer-row-select {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 8px 12px;\n border: none;\n background: none;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.drawer-row-title {\n font-weight: 600;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n.drawer-row-time {\n font-size: 0.72em;\n color: var(--ag-ui-muted);\n}\n\n.drawer-row-preview {\n font-size: 0.8em;\n color: var(--ag-ui-muted);\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n.drawer-row-actions {\n display: flex;\n align-items: center;\n gap: 2px;\n padding: 0 6px;\n}\n\n.drawer-row-rename,\n.drawer-row-delete {\n border: none;\n background: none;\n color: var(--ag-ui-muted);\n font-size: 0.9em;\n padding: 4px;\n cursor: pointer;\n}\n\n.drawer-rename-input {\n flex: 1;\n min-width: 0;\n margin: 6px 10px;\n padding: 4px 8px;\n border: 1px solid var(--ag-ui-accent);\n border-radius: 6px;\n background: var(--ag-ui-input-bg);\n color: var(--ag-ui-fg);\n font: inherit;\n}\n\n.drawer-confirm {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n font-size: 0.85em;\n}\n\n.drawer-confirm-label {\n color: var(--ag-ui-danger);\n}\n\n.drawer-confirm-yes {\n border: none;\n border-radius: 6px;\n background: var(--ag-ui-danger);\n color: #ffffff;\n padding: 3px 10px;\n font: inherit;\n cursor: pointer;\n}\n\n.drawer-confirm-no {\n border: 1px solid var(--ag-ui-border);\n border-radius: 6px;\n background: none;\n color: inherit;\n padding: 3px 10px;\n font: inherit;\n cursor: pointer;\n}\n\n/* Embedded placement: an inline, flush side panel rather than a dimmed,\n floating slide-over. */\n:host([placement=\"embedded\"]) .drawer-backdrop {\n background: none;\n}\n\n:host([placement=\"embedded\"]) .drawer-panel {\n width: 100%;\n border-right: none;\n box-shadow: none;\n}\n";
|
|
2
2
|
//# sourceMappingURL=styles.d.ts.map
|
package/dist/ui/styles.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/ui/styles.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,MAAM,
|
|
1
|
+
{"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/ui/styles.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,MAAM,43qBAw6BlB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@artooi/ag-ui-web-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Framework-free <ag-ui-chat> Web Component over the AG-UI protocol. Drop-in chat sidebar with a pluggable client-side tool registry, DOM driver primitives, animations, and destructive-action confirmation modal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
package/src/constants.ts
CHANGED
|
@@ -70,6 +70,24 @@ export const TOOL_CALL_STATUS = {
|
|
|
70
70
|
DECLINED: "declined",
|
|
71
71
|
} as const;
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Lifecycle status of a pending-attachment chip in the composer tray. A chip
|
|
75
|
+
* opens as `UPLOADING` (with a progress bar), then settles to `READY` (a durable
|
|
76
|
+
* ref) or `ERROR` (with a retry control).
|
|
77
|
+
*/
|
|
78
|
+
export const ATTACHMENT_STATUS = {
|
|
79
|
+
UPLOADING: "uploading",
|
|
80
|
+
READY: "ready",
|
|
81
|
+
ERROR: "error",
|
|
82
|
+
} as const;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Default client-side upload size cap (10 MiB), matching django-ag-ui's
|
|
86
|
+
* `ATTACHMENT_MAX_BYTES` default. Overridable per element via
|
|
87
|
+
* `data-attachment-max-bytes`; the server stays authoritative.
|
|
88
|
+
*/
|
|
89
|
+
export const DEFAULT_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
|
|
90
|
+
|
|
73
91
|
/**
|
|
74
92
|
* How much detail a tool-call card shows. Set via the `data-tool-display`
|
|
75
93
|
* attribute on `<ag-ui-chat>`; defaults to `full` (back-compatible).
|
package/src/core/ag_ui_chat.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Context, Message, Tool } from "@ag-ui/core";
|
|
2
2
|
import {
|
|
3
|
+
DEFAULT_ATTACHMENT_MAX_BYTES,
|
|
3
4
|
MESSAGE_ROLE,
|
|
4
5
|
SUBMIT_EVENT,
|
|
5
6
|
TOGGLE_EVENT,
|
|
@@ -18,6 +19,8 @@ import { createPageMapContext, type PageMap } from "../tools/page_map.js";
|
|
|
18
19
|
import { parseToolCatalog } from "../tools/parse_tool_catalog.js";
|
|
19
20
|
import { createRouteTools, type RouteMap } from "../tools/route_map.js";
|
|
20
21
|
import { createStateHookTools, type StateHook } from "../tools/state_hook.js";
|
|
22
|
+
import { renderAttachmentChips } from "../ui/attachment_chips.js";
|
|
23
|
+
import { AttachmentTray } from "../ui/attachment_tray.js";
|
|
21
24
|
import { type ConfirmationRequest, requestConfirmation } from "../ui/confirmation_card.js";
|
|
22
25
|
import { prettifyToolName } from "../ui/prettify_tool_name.js";
|
|
23
26
|
import { renderMarkdown } from "../ui/render_markdown.js";
|
|
@@ -32,6 +35,7 @@ import {
|
|
|
32
35
|
type AgUiToolCall,
|
|
33
36
|
type ToolExecution,
|
|
34
37
|
} from "./agui_client.js";
|
|
38
|
+
import { type AttachmentRef, messageAttachments } from "./attachment.js";
|
|
35
39
|
import {
|
|
36
40
|
type ClientConversationStore,
|
|
37
41
|
type NavigationCheckpoint,
|
|
@@ -39,6 +43,7 @@ import {
|
|
|
39
43
|
} from "./conversation_store.js";
|
|
40
44
|
import { type AgentFactory, createHttpAgent } from "./create_http_agent.js";
|
|
41
45
|
import { RemoteConversationStore } from "./remote_conversation_store.js";
|
|
46
|
+
import { type UploadHandler, uploadAttachment } from "./upload_attachment.js";
|
|
42
47
|
|
|
43
48
|
/** The role a rendered chat message takes. */
|
|
44
49
|
export type MessageRole = (typeof MESSAGE_ROLE)[keyof typeof MESSAGE_ROLE];
|
|
@@ -46,6 +51,8 @@ export type MessageRole = (typeof MESSAGE_ROLE)[keyof typeof MESSAGE_ROLE];
|
|
|
46
51
|
/** `detail` shape of the {@link SUBMIT_EVENT} CustomEvent. */
|
|
47
52
|
export interface SubmitDetail {
|
|
48
53
|
readonly content: string;
|
|
54
|
+
/** Durable refs for the files attached to this message (empty when none). */
|
|
55
|
+
readonly attachments: readonly AttachmentRef[];
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
/** `detail` shape of the {@link TOGGLE_EVENT} CustomEvent. */
|
|
@@ -114,9 +121,14 @@ export class AgUiChat extends HTMLElement {
|
|
|
114
121
|
|
|
115
122
|
/**
|
|
116
123
|
* Per-run context provider. Defaults to the compact page map (when a
|
|
117
|
-
* {@link getPageMap} provider is set and {@link autoInjectPageMap} is on)
|
|
124
|
+
* {@link getPageMap} provider is set and {@link autoInjectPageMap} is on)
|
|
125
|
+
* plus a one-line manifest of the files attached to the message being sent,
|
|
126
|
+
* so the agent knows which `read_attachment` ids are available.
|
|
118
127
|
*/
|
|
119
|
-
getContext: () => Context[] = () =>
|
|
128
|
+
getContext: () => Context[] = () => [
|
|
129
|
+
...createPageMapContext(this.getPageMap, this.autoInjectPageMap),
|
|
130
|
+
...this.#attachmentContext(),
|
|
131
|
+
];
|
|
120
132
|
|
|
121
133
|
/**
|
|
122
134
|
* Navigable routes the agent can jump to via the built-in `route.*` tools.
|
|
@@ -144,6 +156,17 @@ export class AgUiChat extends HTMLElement {
|
|
|
144
156
|
*/
|
|
145
157
|
conversationStore: ClientConversationStore = new SessionStorageStore();
|
|
146
158
|
|
|
159
|
+
/**
|
|
160
|
+
* How attached files are uploaded. `null` (default) uses the built-in
|
|
161
|
+
* multipart `POST` to `data-attachments-url`. Set a custom
|
|
162
|
+
* {@link UploadHandler} — `(file, onProgress) => Promise<AttachmentRef>` — to
|
|
163
|
+
* swap the transport (e.g. a `tus-js-client` resumable adapter or
|
|
164
|
+
* direct-to-S3 multipart) without changing the tray, the chips, or the AG-UI
|
|
165
|
+
* wire (refs are transport-agnostic). When set, the 📎 affordance appears even
|
|
166
|
+
* with no `data-attachments-url`; the handler owns its own endpoint + headers.
|
|
167
|
+
*/
|
|
168
|
+
uploadHandler: UploadHandler | null = null;
|
|
169
|
+
|
|
147
170
|
/**
|
|
148
171
|
* Builds the tool result a navigating tool resumes with after the page
|
|
149
172
|
* reloads. Defaults to the landed URL; a host (e.g. the admin package) can
|
|
@@ -196,6 +219,14 @@ export class AgUiChat extends HTMLElement {
|
|
|
196
219
|
readonly #skillsMenu: SkillsMenu;
|
|
197
220
|
readonly #drawer: ThreadDrawer;
|
|
198
221
|
readonly #skillHint: HTMLDivElement;
|
|
222
|
+
/** File-picker button + hidden input + tray slot; the tray mounts on connect. */
|
|
223
|
+
readonly #attachButton: HTMLButtonElement;
|
|
224
|
+
readonly #fileInput: HTMLInputElement;
|
|
225
|
+
readonly #attachSlot: HTMLDivElement;
|
|
226
|
+
/** Upload tray; created on connect only when `data-attachments-url` is set. */
|
|
227
|
+
#attachTray: AttachmentTray | null = null;
|
|
228
|
+
/** Refs attached to the message currently being sent (the context manifest). */
|
|
229
|
+
#runAttachments: readonly AttachmentRef[] = [];
|
|
199
230
|
|
|
200
231
|
#client: AgUiClient | null = null;
|
|
201
232
|
// Whether an interaction is in flight (first onRunStart → onSettled). Drives
|
|
@@ -227,6 +258,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
227
258
|
this.#send = document.createElement("button");
|
|
228
259
|
this.#title = document.createElement("span");
|
|
229
260
|
this.#skillHint = document.createElement("div");
|
|
261
|
+
this.#attachButton = document.createElement("button");
|
|
262
|
+
this.#fileInput = document.createElement("input");
|
|
263
|
+
this.#attachSlot = document.createElement("div");
|
|
230
264
|
this.#skillsMenu = new SkillsMenu((skill) => this.#applySkill(skill));
|
|
231
265
|
this.#drawer = new ThreadDrawer({
|
|
232
266
|
onSelect: (threadId) => {
|
|
@@ -357,10 +391,103 @@ export class AgUiChat extends HTMLElement {
|
|
|
357
391
|
this.#initSkills();
|
|
358
392
|
void this.#fetchToolCatalog();
|
|
359
393
|
this.#wireThreadStore();
|
|
394
|
+
this.#wireAttachments();
|
|
360
395
|
this.#threadId = this.conversationStore.threadId();
|
|
361
396
|
void this.#rehydrate();
|
|
362
397
|
}
|
|
363
398
|
|
|
399
|
+
/**
|
|
400
|
+
* Enable the composer's file-upload tray when uploads are possible — either a
|
|
401
|
+
* custom {@link uploadHandler} is set or `data-attachments-url` provides the
|
|
402
|
+
* built-in multipart endpoint: reveal the 📎 button, wire the hidden file
|
|
403
|
+
* input + drag-and-drop, and mount the tray. With neither, the affordance
|
|
404
|
+
* stays hidden and the chat degrades to text-only.
|
|
405
|
+
*/
|
|
406
|
+
#wireAttachments(): void {
|
|
407
|
+
const url = this.getAttribute("data-attachments-url");
|
|
408
|
+
const upload = this.uploadHandler ?? this.#defaultUploadHandler(url);
|
|
409
|
+
if (upload === null) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const accept = this.getAttribute("data-attachment-accept") ?? "";
|
|
413
|
+
this.#attachTray = new AttachmentTray({
|
|
414
|
+
upload,
|
|
415
|
+
maxBytes: this.#attachmentMaxBytes(),
|
|
416
|
+
accept,
|
|
417
|
+
});
|
|
418
|
+
this.#attachSlot.appendChild(this.#attachTray.element);
|
|
419
|
+
this.#fileInput.accept = accept;
|
|
420
|
+
this.#attachButton.hidden = false;
|
|
421
|
+
this.#enableDragAndDrop();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** The built-in multipart upload handler for `data-attachments-url`, or `null`. */
|
|
425
|
+
#defaultUploadHandler(url: string | null): UploadHandler | null {
|
|
426
|
+
if (url === null) {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
return (file, onProgress) => uploadAttachment(file, { url, headers: this.headers, onProgress });
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** The client-side upload size cap from `data-attachment-max-bytes`. */
|
|
433
|
+
#attachmentMaxBytes(): number {
|
|
434
|
+
const attr = this.getAttribute("data-attachment-max-bytes");
|
|
435
|
+
if (attr === null) {
|
|
436
|
+
return DEFAULT_ATTACHMENT_MAX_BYTES;
|
|
437
|
+
}
|
|
438
|
+
const parsed = Number.parseInt(attr, 10);
|
|
439
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_ATTACHMENT_MAX_BYTES;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Queue every file from the picker into the tray, then reset the input. */
|
|
443
|
+
#onFilesPicked(): void {
|
|
444
|
+
const files = this.#fileInput.files;
|
|
445
|
+
if (files !== null) {
|
|
446
|
+
for (const file of Array.from(files)) {
|
|
447
|
+
this.#attachTray?.add(file);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// Reset so re-picking the same file fires `change` again.
|
|
451
|
+
this.#fileInput.value = "";
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** Accept files dropped anywhere on the chat shell into the tray. */
|
|
455
|
+
#enableDragAndDrop(): void {
|
|
456
|
+
this.#chat.addEventListener("dragover", (event) => {
|
|
457
|
+
event.preventDefault();
|
|
458
|
+
this.#chat.classList.add("chat--dragover");
|
|
459
|
+
});
|
|
460
|
+
this.#chat.addEventListener("dragleave", () => {
|
|
461
|
+
this.#chat.classList.remove("chat--dragover");
|
|
462
|
+
});
|
|
463
|
+
this.#chat.addEventListener("drop", (event) => {
|
|
464
|
+
event.preventDefault();
|
|
465
|
+
this.#chat.classList.remove("chat--dragover");
|
|
466
|
+
const files = event.dataTransfer?.files;
|
|
467
|
+
if (files !== undefined) {
|
|
468
|
+
for (const file of Array.from(files)) {
|
|
469
|
+
this.#attachTray?.add(file);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** The one-line manifest of the message's attachments, for the run context. */
|
|
476
|
+
#attachmentContext(): Context[] {
|
|
477
|
+
if (this.#runAttachments.length === 0) {
|
|
478
|
+
return [];
|
|
479
|
+
}
|
|
480
|
+
const lines = this.#runAttachments.map(
|
|
481
|
+
(ref) => `- ${ref.name} (id: ${ref.id}, ${ref.mime || "unknown type"}, ${ref.size} bytes)`,
|
|
482
|
+
);
|
|
483
|
+
return [
|
|
484
|
+
{
|
|
485
|
+
description: "Files the user attached to this message",
|
|
486
|
+
value: `${lines.join("\n")}\nUse the read_attachment tool with an id to read a file's contents.`,
|
|
487
|
+
},
|
|
488
|
+
];
|
|
489
|
+
}
|
|
490
|
+
|
|
364
491
|
/**
|
|
365
492
|
* When `data-threads-url` is set, route thread enumeration / load / rename /
|
|
366
493
|
* delete through that server endpoint (wrapping the current store as the
|
|
@@ -523,6 +650,8 @@ export class AgUiChat extends HTMLElement {
|
|
|
523
650
|
this.#toolCards.clear();
|
|
524
651
|
this.#serverSettled.clear();
|
|
525
652
|
this.#initialMessages = [];
|
|
653
|
+
this.#runAttachments = [];
|
|
654
|
+
this.#attachTray?.clear();
|
|
526
655
|
this.#messages.replaceChildren();
|
|
527
656
|
}
|
|
528
657
|
|
|
@@ -587,8 +716,12 @@ export class AgUiChat extends HTMLElement {
|
|
|
587
716
|
#renderHistoricMessage(message: Message): void {
|
|
588
717
|
const text = typeof message.content === "string" ? message.content : "";
|
|
589
718
|
if (message.role === MESSAGE_ROLE.USER) {
|
|
590
|
-
|
|
591
|
-
|
|
719
|
+
const attachments = messageAttachments(message);
|
|
720
|
+
if (text !== "" || attachments.length > 0) {
|
|
721
|
+
const bubble = this.appendMessage(MESSAGE_ROLE.USER, text);
|
|
722
|
+
if (attachments.length > 0) {
|
|
723
|
+
bubble.appendChild(renderAttachmentChips(attachments));
|
|
724
|
+
}
|
|
592
725
|
}
|
|
593
726
|
return;
|
|
594
727
|
}
|
|
@@ -752,15 +885,35 @@ export class AgUiChat extends HTMLElement {
|
|
|
752
885
|
this.#skillHint.className = "skill-hint";
|
|
753
886
|
this.#skillHint.hidden = true;
|
|
754
887
|
|
|
755
|
-
|
|
888
|
+
// File-upload affordance: a 📎 button (hidden until `data-attachments-url`
|
|
889
|
+
// is wired) opening a hidden multi-file input. Drag-and-drop covers the
|
|
890
|
+
// whole shell (wired in #enableDragAndDrop).
|
|
891
|
+
this.#attachButton.className = "attach-btn";
|
|
892
|
+
this.#attachButton.type = "button";
|
|
893
|
+
this.#attachButton.textContent = "📎";
|
|
894
|
+
this.#attachButton.title = "Attach files";
|
|
895
|
+
this.#attachButton.setAttribute("aria-label", "Attach files");
|
|
896
|
+
this.#attachButton.hidden = true;
|
|
897
|
+
this.#attachButton.addEventListener("click", () => this.#fileInput.click());
|
|
898
|
+
|
|
899
|
+
this.#fileInput.className = "attach-input";
|
|
900
|
+
this.#fileInput.type = "file";
|
|
901
|
+
this.#fileInput.multiple = true;
|
|
902
|
+
this.#fileInput.hidden = true;
|
|
903
|
+
this.#fileInput.addEventListener("change", () => this.#onFilesPicked());
|
|
904
|
+
|
|
905
|
+
this.#attachSlot.className = "attachment-slot";
|
|
906
|
+
|
|
907
|
+
inputRow.append(this.#attachButton, this.#input, this.#send, this.#fileInput);
|
|
756
908
|
// Skill surfaces sit just above the input: palette (opens on `/`), chips,
|
|
757
|
-
//
|
|
909
|
+
// the missing-placeholder hint, and the pending-attachments tray.
|
|
758
910
|
this.#chat.append(
|
|
759
911
|
header,
|
|
760
912
|
this.#messages,
|
|
761
913
|
this.#skillsMenu.palette,
|
|
762
914
|
this.#skillsMenu.chips,
|
|
763
915
|
this.#skillHint,
|
|
916
|
+
this.#attachSlot,
|
|
764
917
|
inputRow,
|
|
765
918
|
this.#drawer.element,
|
|
766
919
|
);
|
|
@@ -814,26 +967,36 @@ export class AgUiChat extends HTMLElement {
|
|
|
814
967
|
|
|
815
968
|
async #submit(): Promise<void> {
|
|
816
969
|
const content = this.#input.value.trim();
|
|
817
|
-
|
|
970
|
+
const attachments = this.#attachTray?.readyRefs() ?? [];
|
|
971
|
+
// Allow an attachments-only message (no typed text), but nothing empty.
|
|
972
|
+
if (content === "" && attachments.length === 0) {
|
|
818
973
|
return;
|
|
819
974
|
}
|
|
820
|
-
this.appendMessage(MESSAGE_ROLE.USER, content);
|
|
975
|
+
const bubble = this.appendMessage(MESSAGE_ROLE.USER, content);
|
|
976
|
+
if (attachments.length > 0) {
|
|
977
|
+
bubble.appendChild(renderAttachmentChips(attachments));
|
|
978
|
+
}
|
|
821
979
|
this.#input.value = "";
|
|
980
|
+
// The refs are now on the bubble; drop the settled chips, keep any still
|
|
981
|
+
// uploading for a follow-up message.
|
|
982
|
+
this.#attachTray?.clearReady();
|
|
983
|
+
// Surfaced to the run via the context manifest until the run settles.
|
|
984
|
+
this.#runAttachments = attachments;
|
|
822
985
|
this.dispatchEvent(
|
|
823
986
|
new CustomEvent<SubmitDetail>(SUBMIT_EVENT, {
|
|
824
|
-
detail: { content },
|
|
987
|
+
detail: { content, attachments },
|
|
825
988
|
bubbles: true,
|
|
826
989
|
composed: true,
|
|
827
990
|
}),
|
|
828
991
|
);
|
|
829
|
-
await this.#client_send(content);
|
|
992
|
+
await this.#client_send(content, attachments);
|
|
830
993
|
}
|
|
831
994
|
|
|
832
|
-
async #client_send(content: string): Promise<void> {
|
|
995
|
+
async #client_send(content: string, attachments: readonly AttachmentRef[]): Promise<void> {
|
|
833
996
|
if (this.endpoint === "") {
|
|
834
997
|
return;
|
|
835
998
|
}
|
|
836
|
-
await this.#ensureClient().send(content);
|
|
999
|
+
await this.#ensureClient().send(content, attachments);
|
|
837
1000
|
}
|
|
838
1001
|
|
|
839
1002
|
#ensureClient(): AgUiClient {
|
|
@@ -998,6 +1161,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
998
1161
|
this.#hidePending();
|
|
999
1162
|
this.#setRunning(false);
|
|
1000
1163
|
this.#streamingBubble = null;
|
|
1164
|
+
// The attachment manifest was for this run only; the model has read what
|
|
1165
|
+
// it needed (results now live in history).
|
|
1166
|
+
this.#runAttachments = [];
|
|
1001
1167
|
},
|
|
1002
1168
|
};
|
|
1003
1169
|
}
|
package/src/core/agui_client.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type AbstractAgent, type AgentSubscriber, randomUUID } from "@ag-ui/client";
|
|
2
2
|
import type { Context, Message, Tool } from "@ag-ui/core";
|
|
3
3
|
import { MAX_TOOL_ROUNDS } from "../constants.js";
|
|
4
|
+
import type { AttachmentRef } from "./attachment.js";
|
|
4
5
|
|
|
5
6
|
/** A tool call surfaced to the host by {@link AgUiClient}. */
|
|
6
7
|
export interface AgUiToolCall {
|
|
@@ -137,9 +138,21 @@ export class AgUiClient {
|
|
|
137
138
|
* When the agent calls frontend tools, this executes them and re-runs the
|
|
138
139
|
* agent with the results, looping until the agent stops calling frontend
|
|
139
140
|
* tools (bounded by {@link MAX_TOOL_ROUNDS}).
|
|
141
|
+
*
|
|
142
|
+
* `attachments` ride on the user message as a non-standard field so the
|
|
143
|
+
* default client store round-trips them for history replay; the agent learns
|
|
144
|
+
* the ids from the run context (the server's strict validation ignores the
|
|
145
|
+
* unknown message field), then reads bytes via the `read_attachment` tool.
|
|
140
146
|
*/
|
|
141
|
-
async send(content: string): Promise<void> {
|
|
142
|
-
|
|
147
|
+
async send(content: string, attachments: readonly AttachmentRef[] = []): Promise<void> {
|
|
148
|
+
// Cast at the AG-UI boundary: `attachments` is a web-component augmentation
|
|
149
|
+
// the strict `Message` union doesn't declare, but `addMessage` /
|
|
150
|
+
// `structuredClone` preserve it verbatim.
|
|
151
|
+
const message = { id: randomUUID(), role: "user", content } as Message;
|
|
152
|
+
if (attachments.length > 0) {
|
|
153
|
+
(message as { attachments?: readonly AttachmentRef[] }).attachments = attachments;
|
|
154
|
+
}
|
|
155
|
+
this.#agent.addMessage(message);
|
|
143
156
|
this.#onPersist(this.#agent.messages);
|
|
144
157
|
await this.#run();
|
|
145
158
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Message } from "@ag-ui/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A durable, lightweight reference to one uploaded file — what an upload
|
|
5
|
+
* returns and what rides on a sent message, never the bytes.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors django-ag-ui's `AttachmentRef`: a file uploads out-of-band to the
|
|
8
|
+
* attachments endpoint, the server hands back this ref, and the agent reads the
|
|
9
|
+
* actual content server-side via the `read_attachment` tool. Keeping the AG-UI
|
|
10
|
+
* message stream free of base64 mirrors how the tool catalog keeps schemas off
|
|
11
|
+
* the wire.
|
|
12
|
+
*/
|
|
13
|
+
export interface AttachmentRef {
|
|
14
|
+
/** Opaque, owner-scoped handle the server resolves back to bytes. */
|
|
15
|
+
readonly id: string;
|
|
16
|
+
/** Original filename, for display on the chip. */
|
|
17
|
+
readonly name: string;
|
|
18
|
+
/** Declared content type (a hint — the server is authoritative). */
|
|
19
|
+
readonly mime: string;
|
|
20
|
+
/** Size in bytes. */
|
|
21
|
+
readonly size: number;
|
|
22
|
+
/** Optional direct fetch URL (the owner-checked download endpoint). */
|
|
23
|
+
readonly url?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The attachment refs a user message carries.
|
|
28
|
+
*
|
|
29
|
+
* Refs are stored on the user message as a non-standard `attachments` field: a
|
|
30
|
+
* web-component augmentation that the default client store round-trips and
|
|
31
|
+
* `@ag-ui/client` preserves through `addMessage` / `structuredClone`, so a
|
|
32
|
+
* restored conversation re-renders its attachment chips. The server's strict
|
|
33
|
+
* `RunAgentInput` validation ignores the unknown field — the model learns the
|
|
34
|
+
* ids from the run context manifest instead.
|
|
35
|
+
*/
|
|
36
|
+
export function messageAttachments(message: Message): readonly AttachmentRef[] {
|
|
37
|
+
const refs = (message as { attachments?: unknown }).attachments;
|
|
38
|
+
return Array.isArray(refs) ? (refs as readonly AttachmentRef[]) : [];
|
|
39
|
+
}
|