@artooi/ag-ui-web-component 0.3.0 → 0.3.1
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 +43 -1
- package/README.md +9 -5
- package/dist/ag-ui-web-component.bundle.js +23 -23
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/core/ag_ui_chat.d.ts +7 -0
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/create_http_agent.d.ts +9 -0
- package/dist/core/create_http_agent.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +51 -13
- package/dist/index.js.map +3 -3
- package/dist/ui/prettify_tool_name.d.ts +11 -0
- package/dist/ui/prettify_tool_name.d.ts.map +1 -0
- package/dist/ui/render_markdown.d.ts +10 -1
- package/dist/ui/render_markdown.d.ts.map +1 -1
- package/package.json +2 -3
- package/src/core/ag_ui_chat.ts +18 -3
- package/src/core/create_http_agent.ts +23 -2
- package/src/index.ts +2 -1
- package/src/ui/prettify_tool_name.ts +16 -0
- package/src/ui/render_markdown.ts +37 -11
- package/src/version.ts +1 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prettify a raw tool name for display: separators become spaces and the
|
|
3
|
+
* first letter is capitalised — `list_projects` → "List projects",
|
|
4
|
+
* `invoices.retrieve` → "Invoices retrieve".
|
|
5
|
+
*
|
|
6
|
+
* Final fallback of the tool-card label chain (`x-summary` →
|
|
7
|
+
* `toolSummaries` → fetched catalog → this). Purely cosmetic: the original
|
|
8
|
+
* name still rides on the card's dataset for debugging.
|
|
9
|
+
*/
|
|
10
|
+
export declare function prettifyToolName(name: string): string;
|
|
11
|
+
//# sourceMappingURL=prettify_tool_name.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prettify_tool_name.d.ts","sourceRoot":"","sources":["../../src/ui/prettify_tool_name.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMrD"}
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/** Options for {@link renderMarkdown}. */
|
|
2
|
+
export interface RenderMarkdownOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Permit `<img>` tags (and their `src`/`alt`/`width`/`height` attributes)
|
|
5
|
+
* in the sanitised output. **Off by default** — see the allowlist note on
|
|
6
|
+
* the exfiltration risk. Only enable for trusted content sources.
|
|
7
|
+
*/
|
|
8
|
+
readonly allowImages?: boolean;
|
|
9
|
+
}
|
|
1
10
|
/**
|
|
2
11
|
* Render markdown (and any embedded raw HTML) to a sanitised HTML string.
|
|
3
12
|
*
|
|
@@ -9,5 +18,5 @@
|
|
|
9
18
|
* The result is trimmed so a single-paragraph message round-trips to clean
|
|
10
19
|
* `textContent` (no trailing newline from the wrapping `<p>`).
|
|
11
20
|
*/
|
|
12
|
-
export declare function renderMarkdown(text: string): string;
|
|
21
|
+
export declare function renderMarkdown(text: string, options?: RenderMarkdownOptions): string;
|
|
13
22
|
//# sourceMappingURL=render_markdown.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render_markdown.d.ts","sourceRoot":"","sources":["../../src/ui/render_markdown.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"render_markdown.d.ts","sourceRoot":"","sources":["../../src/ui/render_markdown.ts"],"names":[],"mappings":"AA0DA,0CAA0C;AAC1C,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;CAChC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,MAAM,CAcpF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@artooi/ag-ui-web-component",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
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",
|
|
@@ -13,8 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"./bundle": {
|
|
15
15
|
"import": "./dist/ag-ui-web-component.bundle.js"
|
|
16
|
-
}
|
|
17
|
-
"./style.css": "./dist/ag-ui-web-component.bundle.css"
|
|
16
|
+
}
|
|
18
17
|
},
|
|
19
18
|
"files": [
|
|
20
19
|
"dist/",
|
package/src/core/ag_ui_chat.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { parseToolCatalog } from "../tools/parse_tool_catalog.js";
|
|
|
19
19
|
import { createRouteTools, type RouteMap } from "../tools/route_map.js";
|
|
20
20
|
import { createStateHookTools, type StateHook } from "../tools/state_hook.js";
|
|
21
21
|
import { type ConfirmationRequest, requestConfirmation } from "../ui/confirmation_card.js";
|
|
22
|
+
import { prettifyToolName } from "../ui/prettify_tool_name.js";
|
|
22
23
|
import { renderMarkdown } from "../ui/render_markdown.js";
|
|
23
24
|
import { wrapWords } from "../ui/reveal_words.js";
|
|
24
25
|
import { SkillsMenu } from "../ui/skills_menu.js";
|
|
@@ -72,6 +73,14 @@ export class AgUiChat extends HTMLElement {
|
|
|
72
73
|
/** Extra HTTP headers for the AG-UI endpoint (e.g. CSRF). */
|
|
73
74
|
headers: Record<string, string> = {};
|
|
74
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Permit `<img>` in rendered assistant markdown. **Off by default**: a
|
|
78
|
+
* model-controlled image URL is fetched with no user interaction, which
|
|
79
|
+
* makes it a zero-click exfiltration channel for prompt-injected page
|
|
80
|
+
* data. Enable only when the content source is trusted.
|
|
81
|
+
*/
|
|
82
|
+
allowImages = false;
|
|
83
|
+
|
|
75
84
|
/** When true, destructive tools execute without a confirmation modal. */
|
|
76
85
|
autoConfirm = false;
|
|
77
86
|
|
|
@@ -569,7 +578,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
569
578
|
const bubble = document.createElement("div");
|
|
570
579
|
bubble.className = `message message--${role}`;
|
|
571
580
|
if (role === MESSAGE_ROLE.ASSISTANT) {
|
|
572
|
-
bubble.innerHTML = renderMarkdown(content);
|
|
581
|
+
bubble.innerHTML = renderMarkdown(content, { allowImages: this.allowImages });
|
|
573
582
|
} else {
|
|
574
583
|
bubble.textContent = content;
|
|
575
584
|
}
|
|
@@ -700,6 +709,10 @@ export class AgUiChat extends HTMLElement {
|
|
|
700
709
|
const agent = this.agentFactory({
|
|
701
710
|
endpoint: this.endpoint,
|
|
702
711
|
headers: this.headers,
|
|
712
|
+
// Live getter: the client is built once and cached, but a rotated
|
|
713
|
+
// token must still reach every request — the factory's fetch wrapper
|
|
714
|
+
// re-reads this on each call.
|
|
715
|
+
getHeaders: () => this.headers,
|
|
703
716
|
threadId: this.#threadId,
|
|
704
717
|
initialMessages: this.#initialMessages,
|
|
705
718
|
});
|
|
@@ -878,7 +891,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
878
891
|
this.#streamingBubble = this.appendMessage(MESSAGE_ROLE.ASSISTANT, "");
|
|
879
892
|
this.#streamDeltas = 0;
|
|
880
893
|
}
|
|
881
|
-
this.#streamingBubble.innerHTML = renderMarkdown(buffer);
|
|
894
|
+
this.#streamingBubble.innerHTML = renderMarkdown(buffer, { allowImages: this.allowImages });
|
|
882
895
|
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
883
896
|
return this.#streamingBubble;
|
|
884
897
|
}
|
|
@@ -901,7 +914,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
901
914
|
const summary =
|
|
902
915
|
typeof labelled === "string"
|
|
903
916
|
? labelled
|
|
904
|
-
: (this.toolSummaries[call.name] ??
|
|
917
|
+
: (this.toolSummaries[call.name] ??
|
|
918
|
+
this.#toolCatalog[call.name] ??
|
|
919
|
+
prettifyToolName(call.name));
|
|
905
920
|
const card = new ToolCallCard(call.name, call.args, this.toolDisplay, summary);
|
|
906
921
|
this.#toolCards.set(call.id, card);
|
|
907
922
|
this.#messages.appendChild(card.element);
|
|
@@ -5,6 +5,15 @@ import type { Message } from "@ag-ui/core";
|
|
|
5
5
|
export interface HttpAgentOptions {
|
|
6
6
|
endpoint: string;
|
|
7
7
|
headers?: Record<string, string>;
|
|
8
|
+
/**
|
|
9
|
+
* Live header source, re-read on **every** request. `HttpAgent` bakes the
|
|
10
|
+
* static `headers` into its constructor and the element caches the agent
|
|
11
|
+
* for the whole conversation — so a rotated token (CSRF, short-lived JWT)
|
|
12
|
+
* would otherwise never reach the agent endpoint and a long session 401s
|
|
13
|
+
* mid-conversation. When set, the fetch wrapper overlays these values on
|
|
14
|
+
* each call; `headers` still seeds the initial/static configuration.
|
|
15
|
+
*/
|
|
16
|
+
getHeaders?: () => Record<string, string>;
|
|
8
17
|
/** Stable conversation id, so the agent's runs share a thread. */
|
|
9
18
|
threadId?: string;
|
|
10
19
|
/** Rehydrated history to seed the agent with (durable conversation). */
|
|
@@ -25,8 +34,20 @@ export function createHttpAgent(options: HttpAgentOptions): AbstractAgent {
|
|
|
25
34
|
// HttpAgent invokes its configured fetch as a method (`this.fetch(...)`),
|
|
26
35
|
// which would rebind the global `fetch` to the agent instance and trigger
|
|
27
36
|
// "Illegal invocation" in browsers. Wrap it so `fetch` is always called as
|
|
28
|
-
// a free function with the correct receiver.
|
|
29
|
-
|
|
37
|
+
// a free function with the correct receiver. The wrapper also overlays
|
|
38
|
+
// `getHeaders()` per request, so header rotation (CSRF, short-lived JWT)
|
|
39
|
+
// reaches the stream even though the agent instance is cached.
|
|
40
|
+
fetch: (url, init) => {
|
|
41
|
+
const fresh = options.getHeaders?.();
|
|
42
|
+
if (fresh === undefined) {
|
|
43
|
+
return fetch(url, init);
|
|
44
|
+
}
|
|
45
|
+
const headers = new Headers(init?.headers);
|
|
46
|
+
for (const [name, value] of Object.entries(fresh)) {
|
|
47
|
+
headers.set(name, value);
|
|
48
|
+
}
|
|
49
|
+
return fetch(url, { ...init, headers });
|
|
50
|
+
},
|
|
30
51
|
// Spread conditionally: under `exactOptionalPropertyTypes` an explicit
|
|
31
52
|
// `undefined` is not assignable to these optional config fields.
|
|
32
53
|
...(options.threadId !== undefined ? { threadId: options.threadId } : {}),
|
package/src/index.ts
CHANGED
|
@@ -80,7 +80,8 @@ export {
|
|
|
80
80
|
} from "./tools/route_map.js";
|
|
81
81
|
export { createStateHookTools, type StateHook } from "./tools/state_hook.js";
|
|
82
82
|
export { type ConfirmationRequest, requestConfirmation } from "./ui/confirmation_card.js";
|
|
83
|
-
export {
|
|
83
|
+
export { prettifyToolName } from "./ui/prettify_tool_name.js";
|
|
84
|
+
export { type RenderMarkdownOptions, renderMarkdown } from "./ui/render_markdown.js";
|
|
84
85
|
export {
|
|
85
86
|
type SettledStatus,
|
|
86
87
|
ToolCallCard,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prettify a raw tool name for display: separators become spaces and the
|
|
3
|
+
* first letter is capitalised — `list_projects` → "List projects",
|
|
4
|
+
* `invoices.retrieve` → "Invoices retrieve".
|
|
5
|
+
*
|
|
6
|
+
* Final fallback of the tool-card label chain (`x-summary` →
|
|
7
|
+
* `toolSummaries` → fetched catalog → this). Purely cosmetic: the original
|
|
8
|
+
* name still rides on the card's dataset for debugging.
|
|
9
|
+
*/
|
|
10
|
+
export function prettifyToolName(name: string): string {
|
|
11
|
+
const spaced = name.replace(/[._-]+/g, " ").trim();
|
|
12
|
+
if (spaced === "") {
|
|
13
|
+
return name;
|
|
14
|
+
}
|
|
15
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
16
|
+
}
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import DOMPurify from "dompurify";
|
|
2
|
-
import {
|
|
2
|
+
import { Marked } from "marked";
|
|
3
3
|
|
|
4
|
-
//
|
|
5
|
-
|
|
4
|
+
// Local parser instance so configuration never leaks into the shared `marked`
|
|
5
|
+
// singleton (a host app's deduped copy keeps its own options). GitHub-flavoured
|
|
6
|
+
// markdown with single-newline line breaks (chat-like). Constructed once at
|
|
7
|
+
// module scope — configured here and never mutated afterwards; per-call
|
|
8
|
+
// construction would re-pay setup on every streaming re-render.
|
|
9
|
+
const parser = new Marked({ gfm: true, breaks: true });
|
|
6
10
|
|
|
7
11
|
// Conservative allowlist for assistant chat content: inline emphasis, code,
|
|
8
|
-
// lists, quotes, headings, links,
|
|
9
|
-
// `
|
|
10
|
-
//
|
|
12
|
+
// lists, quotes, headings, links, and tables. Deliberately excludes `iframe`,
|
|
13
|
+
// `style`, and any scripting — rendering untrusted model/tool output as HTML
|
|
14
|
+
// is an XSS surface, so the sanitiser is the load-bearing safety net.
|
|
15
|
+
//
|
|
16
|
+
// `img` is excluded by default: a model-controlled `<img src="https://...">`
|
|
17
|
+
// is fetched by the browser with **no user interaction**, which turns any
|
|
18
|
+
// prompt-injected page data into a zero-click exfiltration channel. Hosts
|
|
19
|
+
// that trust their content can opt back in via `allowImages`.
|
|
11
20
|
const ALLOWED_TAGS = [
|
|
12
21
|
"a",
|
|
13
22
|
"p",
|
|
@@ -39,10 +48,23 @@ const ALLOWED_TAGS = [
|
|
|
39
48
|
"tr",
|
|
40
49
|
"th",
|
|
41
50
|
"td",
|
|
42
|
-
"img",
|
|
43
51
|
];
|
|
44
52
|
|
|
45
|
-
const ALLOWED_ATTR = ["href", "title", "class"
|
|
53
|
+
const ALLOWED_ATTR = ["href", "title", "class"];
|
|
54
|
+
|
|
55
|
+
// The image-permitting variants used when the host opts in.
|
|
56
|
+
const ALLOWED_TAGS_WITH_IMAGES = [...ALLOWED_TAGS, "img"];
|
|
57
|
+
const ALLOWED_ATTR_WITH_IMAGES = [...ALLOWED_ATTR, "src", "alt", "width", "height"];
|
|
58
|
+
|
|
59
|
+
/** Options for {@link renderMarkdown}. */
|
|
60
|
+
export interface RenderMarkdownOptions {
|
|
61
|
+
/**
|
|
62
|
+
* Permit `<img>` tags (and their `src`/`alt`/`width`/`height` attributes)
|
|
63
|
+
* in the sanitised output. **Off by default** — see the allowlist note on
|
|
64
|
+
* the exfiltration risk. Only enable for trusted content sources.
|
|
65
|
+
*/
|
|
66
|
+
readonly allowImages?: boolean;
|
|
67
|
+
}
|
|
46
68
|
|
|
47
69
|
/**
|
|
48
70
|
* Render markdown (and any embedded raw HTML) to a sanitised HTML string.
|
|
@@ -55,9 +77,13 @@ const ALLOWED_ATTR = ["href", "title", "class", "src", "alt", "width", "height"]
|
|
|
55
77
|
* The result is trimmed so a single-paragraph message round-trips to clean
|
|
56
78
|
* `textContent` (no trailing newline from the wrapping `<p>`).
|
|
57
79
|
*/
|
|
58
|
-
export function renderMarkdown(text: string): string {
|
|
59
|
-
const
|
|
60
|
-
const
|
|
80
|
+
export function renderMarkdown(text: string, options?: RenderMarkdownOptions): string {
|
|
81
|
+
const allowImages = options?.allowImages === true;
|
|
82
|
+
const rendered = parser.parse(text, { async: false });
|
|
83
|
+
const clean = DOMPurify.sanitize(rendered, {
|
|
84
|
+
ALLOWED_TAGS: allowImages ? ALLOWED_TAGS_WITH_IMAGES : ALLOWED_TAGS,
|
|
85
|
+
ALLOWED_ATTR: allowImages ? ALLOWED_ATTR_WITH_IMAGES : ALLOWED_ATTR,
|
|
86
|
+
});
|
|
61
87
|
const template = document.createElement("template");
|
|
62
88
|
template.innerHTML = clean;
|
|
63
89
|
for (const anchor of template.content.querySelectorAll("a[href]")) {
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION: string = "0.3.
|
|
1
|
+
export const VERSION: string = "0.3.1";
|