@artooi/ag-ui-web-component 0.3.0 → 0.4.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.
@@ -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/ui/styles.ts CHANGED
@@ -501,6 +501,19 @@ export const STYLES = `
501
501
  cursor: default;
502
502
  }
503
503
 
504
+ /* The composer button doubles as the Stop control while a run is in flight. */
505
+ .send[data-state="running"] {
506
+ background: var(--ag-ui-muted);
507
+ }
508
+
509
+ /* Muted "⏹ Stopped" line after a cancelled run — a note, not an error bubble. */
510
+ .stopped-note {
511
+ align-self: flex-start;
512
+ color: var(--ag-ui-muted);
513
+ font-size: 12px;
514
+ padding: 2px 4px;
515
+ }
516
+
504
517
  /* Inline confirmation card — lives in the transcript, no focus-stealing overlay. */
505
518
  .confirm {
506
519
  align-self: stretch;
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION: string = "0.3.0";
1
+ export const VERSION: string = "0.4.0";