@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.
@@ -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":"AA8CA;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAUnD"}
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.0",
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/",
@@ -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] ?? this.#toolCatalog[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
- fetch: (url, init) => fetch(url, init),
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 { renderMarkdown } from "./ui/render_markdown.js";
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 { marked } from "marked";
2
+ import { Marked } from "marked";
3
3
 
4
- // GitHub-flavoured markdown with single-newline line breaks (chat-like).
5
- marked.setOptions({ gfm: true, breaks: true });
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, tables, and images. Deliberately excludes
9
- // `iframe`, `style`, and any scripting — rendering untrusted model/tool output
10
- // as HTML is an XSS surface, so the sanitiser is the load-bearing safety net.
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", "src", "alt", "width", "height"];
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 rendered = marked.parse(text, { async: false });
60
- const clean = DOMPurify.sanitize(rendered, { ALLOWED_TAGS, ALLOWED_ATTR });
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.0";
1
+ export const VERSION: string = "0.3.1";