@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.
@@ -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"}
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/ui/styles.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,MAAM,o7lBA0zBlB,CAAC"}
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.5.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).
@@ -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[] = () => createPageMapContext(this.getPageMap, this.autoInjectPageMap);
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
- if (text !== "") {
591
- this.appendMessage(MESSAGE_ROLE.USER, text);
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
- inputRow.append(this.#input, this.#send);
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
- // and the missing-placeholder hint.
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
- if (content === "") {
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
  }
@@ -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
- this.#agent.addMessage({ id: randomUUID(), role: "user", content });
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
+ }