@kitn.ai/ui 0.24.0 → 0.25.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.
Files changed (64) hide show
  1. package/dist/{create-tween-vA7upv0h.js → create-tween-4z1XYLkR.js} +1 -1
  2. package/dist/{create-tween-CTT5l1bo.js → create-tween-B5Y4Im8G.js} +1 -1
  3. package/dist/{create-tween-BmBnlg25.js → create-tween-CUzOlaMS.js} +1 -1
  4. package/dist/elements/artifact.js +1 -1
  5. package/dist/elements/cards.js +1 -1
  6. package/dist/elements/chat-workspace.js +1 -1
  7. package/dist/elements/chat.js +1 -1
  8. package/dist/elements/choice.js +1 -1
  9. package/dist/elements/chunks/artifact-BSuPYFHY.js +1 -0
  10. package/dist/elements/chunks/{card-renderer-aNA0WRFH.js → card-renderer-CSoTL_e1.js} +1 -1
  11. package/dist/elements/chunks/card-routing-BK09BdmL.js +1 -0
  12. package/dist/elements/chunks/{chat-thread-DN9evldr.js → chat-thread-DxoKaStk.js} +1 -1
  13. package/dist/elements/chunks/{choice-card-kNsjU2m8.js → choice-card-CGMc3kek.js} +1 -1
  14. package/dist/elements/chunks/{confirm-card-CEvFmVhz.js → confirm-card-BWjf2-xo.js} +1 -1
  15. package/dist/elements/chunks/{form-lUuJjz6s.js → form-Cp8Y00Wn.js} +1 -1
  16. package/dist/elements/chunks/link-preview-Cf5KOP-i.js +1 -0
  17. package/dist/elements/chunks/link-preview-DNILK391.js +1 -0
  18. package/dist/elements/chunks/{markdown-BpAHnBJz.js → markdown-DbozyYqa.js} +16 -16
  19. package/dist/elements/chunks/{message-CoqLuLkd.js → message-CE4k-qaj.js} +1 -1
  20. package/dist/elements/chunks/{message-_oYx5mk1.js → message-DtXKPqiR.js} +1 -1
  21. package/dist/elements/chunks/{reasoning-D26Bb4p6.js → reasoning-BrxQfpfI.js} +1 -1
  22. package/dist/elements/chunks/source-w9V0NnBn.js +1 -0
  23. package/dist/elements/chunks/{tasks-card-CfqrkoVO.js → tasks-card-lPxoodlA.js} +1 -1
  24. package/dist/elements/compare.js +1 -1
  25. package/dist/elements/confirm-card.js +1 -1
  26. package/dist/elements/embed.js +1 -1
  27. package/dist/elements/form.js +1 -1
  28. package/dist/elements/link-preview.js +1 -1
  29. package/dist/elements/markdown.js +1 -1
  30. package/dist/elements/message.js +1 -1
  31. package/dist/elements/reasoning.js +1 -1
  32. package/dist/elements/remote.js +1 -1
  33. package/dist/elements/source.js +1 -1
  34. package/dist/elements/tasks.js +1 -1
  35. package/dist/elements/thread.js +1 -1
  36. package/dist/index.js +4011 -3936
  37. package/dist/index.server.js +3302 -3228
  38. package/dist/kai.es.js +1 -1
  39. package/dist/primitives/card-routing.d.ts +33 -0
  40. package/dist/{register-impl-C96AnOJn.js → register-impl-DGmGzrEM.js} +15 -15
  41. package/dist/{solid-C76UXJuy.js → solid-BMuDmFo_.js} +4427 -4352
  42. package/dist/{solid-BuqWLn6x.js → solid-CGYNdLRR.js} +3738 -3664
  43. package/dist/solid.js +1 -1
  44. package/dist/solid.server.js +1 -1
  45. package/dist/{variant-aurora-CD35fdJF.js → variant-aurora-DwSOfraQ.js} +2 -2
  46. package/dist/{variant-aurora-Dau5Z4yR.js → variant-aurora-XEkPT0_z.js} +1 -1
  47. package/dist/{variant-aurora-Dc-2RVH9.js → variant-aurora-wKvAQ5wp.js} +2 -2
  48. package/dist/{variant-custom-CHr_jbMU.js → variant-custom-BhS9wIpU.js} +2 -2
  49. package/dist/{variant-custom-Df7QvXyZ.js → variant-custom-MkTNTycd.js} +1 -1
  50. package/dist/{variant-custom-Cyu8zx4A.js → variant-custom-eFIg2XWQ.js} +2 -2
  51. package/dist/{variant-wave-CXcaP_9W.js → variant-wave-BwzrHD1s.js} +1 -1
  52. package/dist/{variant-wave-BkVpxkVF.js → variant-wave-CUWGx9Bn.js} +2 -2
  53. package/dist/{variant-wave-DcGqjVnK.js → variant-wave-CbVsmgwT.js} +2 -2
  54. package/dist/wire.js +112 -105
  55. package/package.json +1 -1
  56. package/src/components/artifact.tsx +110 -24
  57. package/src/components/markdown.tsx +83 -5
  58. package/src/components/source.tsx +35 -3
  59. package/src/primitives/card-routing.ts +54 -2
  60. package/src/wire/formats/openai.ts +38 -2
  61. package/dist/elements/chunks/artifact-BhJIuh3D.js +0 -1
  62. package/dist/elements/chunks/card-routing-C986KuUA.js +0 -1
  63. package/dist/elements/chunks/link-preview-C9-jNJBf.js +0 -1
  64. package/dist/elements/chunks/source-eJurW_-L.js +0 -1
@@ -16,6 +16,7 @@ import { CodeBlock, CodeBlockCode } from './code-block';
16
16
  import { FileTree, type FileTreeFile } from './file-tree';
17
17
  import { Loader } from './loader';
18
18
  import { isPdfPreviewEnabled, renderPdfInto } from '../primitives/pdf-preview';
19
+ import { isSafeUrl, isScriptUrl } from '../primitives/card-routing';
19
20
  import {
20
21
  ArrowLeft,
21
22
  ArrowRight,
@@ -123,6 +124,18 @@ export interface ArtifactProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>,
123
124
 
124
125
  const DEFAULT_SANDBOX = 'allow-scripts allow-forms';
125
126
 
127
+ /** Refuse loudly. `src` and `files[].url` are MODEL-REACHABLE -- they arrive on
128
+ * an `artifact` card envelope produced by a tool call, and the card schema
129
+ * validates them only as `format: "uri"`, which constrains no scheme -- so
130
+ * every url this component puts in an href, hands to `window.open`, or frames
131
+ * is filtered. A refusal without a trace would be a decision made while hiding
132
+ * that it happened; the component has no error channel of its own, so it warns
133
+ * the way `card-routing` does. */
134
+ function warnBlockedUrl(sink: string, url: string): void {
135
+ // eslint-disable-next-line no-console
136
+ console.warn(`[kai-artifact] blocked unsafe url in ${sink}: ${url}`);
137
+ }
138
+
126
139
  /** Resolve a file's preview URL: explicit `url`, else `<src-origin> + /path`. */
127
140
  function resolveFileUrl(file: ArtifactFile, src: string | undefined): string {
128
141
  if (file.url) return file.url;
@@ -205,14 +218,47 @@ export function Artifact(props: ArtifactProps): JSX.Element {
205
218
  // navigations it initiates (path-edit, file-click, home, src changes) and
206
219
  // back/forward/reload operate on that. (In-frame relative-link clicks navigate
207
220
  // the iframe itself but can't be observed cross-origin — a known, accepted
208
- // sandbox limitation; opt into `allow-same-origin` if the consumer trusts the
209
- // artifact and wants those tracked.)
221
+ // sandbox limitation.)
222
+ //
223
+ // DO NOT add `allow-same-origin` to get those clicks tracked. This comment
224
+ // used to suggest exactly that "if the consumer trusts the artifact", and it
225
+ // was the worst advice in the file: `allow-same-origin` gives the framed
226
+ // document the HOST's origin, so a hostile `src` runs with full access to the
227
+ // embedding page, zero clicks -- confirmed executing in Chromium. And "the
228
+ // artifact is trusted" is not a premise you get to hold when the url can come
229
+ // from tool output: on the card path `src` and `files[].url` are
230
+ // model-supplied. The scheme filter below is a backstop, not a licence --
231
+ // `allow-same-origin` still hands the framed document the host's cookies,
232
+ // storage and DOM for everything else it does. Losing in-frame click tracking
233
+ // is the correct trade.
210
234
  const [history, setHistory] = createSignal<string[]>(local.src ? [local.src] : []);
211
235
  const [cursor, setCursor] = createSignal(local.src ? 0 : -1);
212
236
  const currentUrl = () => history()[cursor()] ?? '';
213
237
  const canBack = () => cursor() > 0;
214
238
  const canForward = () => cursor() < history().length - 1;
215
239
 
240
+ /** What the iframe is actually pointed at.
241
+ *
242
+ * Separate from `currentUrl()` on purpose: the history stack and the address
243
+ * field keep reporting what ARRIVED (a reader looking at a refused artifact
244
+ * should see the address that was refused, not a blank), while this is the
245
+ * only value that reaches the `src` sink.
246
+ *
247
+ * `isScriptUrl` and not `isSafeUrl`: a `data:`/`blob:` src is a legitimate
248
+ * artifact -- see the note on `displayUrl` -- and gets an opaque origin, so
249
+ * it cannot touch the host page. `javascript:`/`vbscript:` run in the
250
+ * EMBEDDER's origin, which the default sandbox already refuses but a
251
+ * consumer-set `allow-same-origin` re-enables (see the note on the history
252
+ * stack above for why nothing here recommends that setting). */
253
+ const framedUrl = createMemo(() => {
254
+ const u = currentUrl();
255
+ if (u && isScriptUrl(u)) {
256
+ warnBlockedUrl('preview', u);
257
+ return '';
258
+ }
259
+ return u;
260
+ });
261
+
216
262
  // Seed the internal tab: a present `tab` (controlled) wins; otherwise the
217
263
  // uncontrolled `defaultTab` (which merges to 'preview'). The user can then
218
264
  // switch freely unless `tab` is set (the effect below re-asserts the controlled
@@ -230,14 +276,23 @@ export function Artifact(props: ArtifactProps): JSX.Element {
230
276
  local.onMaximizeChange?.(next);
231
277
  };
232
278
 
233
- // Open-in-tab — only enabled when there's a concrete url to open.
279
+ // Open-in-tab — only enabled when there's a concrete SAFE url to open. The
280
+ // scheme filter is part of `canOpenInTab` rather than only of the handler so
281
+ // the button renders DISABLED for a url that would be refused: a control that
282
+ // looks live and does nothing is the worse failure.
234
283
  const canOpenInTab = createMemo(() => {
235
284
  const u = currentUrl();
236
- return !!u && u !== 'about:blank';
285
+ return !!u && u !== 'about:blank' && isSafeUrl(u);
237
286
  });
238
287
  const openInNewTab = () => {
239
- if (!canOpenInTab()) return;
240
- window.open(currentUrl(), '_blank', 'noopener,noreferrer');
288
+ const u = currentUrl();
289
+ if (!canOpenInTab()) {
290
+ // `openExternal()` on the controller reaches this without any button, so
291
+ // the refusal has to be stated here and not only in the disabled state.
292
+ if (u && u !== 'about:blank') warnBlockedUrl('openExternal', u);
293
+ return;
294
+ }
295
+ window.open(u, '_blank', 'noopener,noreferrer');
241
296
  };
242
297
 
243
298
  // The expand button is suppressed in standalone (no enclosing resizable).
@@ -300,9 +355,11 @@ export function Artifact(props: ArtifactProps): JSX.Element {
300
355
 
301
356
  /** Point the iframe at the current cursor entry + emit `navigate`. */
302
357
  function loadCurrent() {
303
- const url = currentUrl();
304
- if (iframeEl) iframeEl.src = url || 'about:blank';
305
- local.onNavigate?.(url);
358
+ if (iframeEl) iframeEl.src = framedUrl() || 'about:blank';
359
+ // Reports the REAL url, not the framed one: a consumer observing navigation
360
+ // (or a card patching `src` back into its envelope) must not be told the
361
+ // model sent something it did not.
362
+ local.onNavigate?.(currentUrl());
306
363
  }
307
364
 
308
365
  function selectTab(next: ArtifactTab) {
@@ -329,14 +386,13 @@ export function Artifact(props: ArtifactProps): JSX.Element {
329
386
  loadCurrent();
330
387
  };
331
388
  function reload() {
332
- const url = currentUrl();
333
389
  if (iframeEl) {
334
390
  // Force a real reload even when the src is unchanged.
335
391
  iframeEl.src = 'about:blank';
336
- iframeEl.src = url || 'about:blank';
392
+ iframeEl.src = framedUrl() || 'about:blank';
337
393
  }
338
394
  setReloadKey((k) => k + 1); // re-render the inline PDF viewer too
339
- local.onNavigate?.(url);
395
+ local.onNavigate?.(currentUrl());
340
396
  }
341
397
  const goHome = () => {
342
398
  if (local.src) navigate(local.src);
@@ -458,7 +514,7 @@ export function Artifact(props: ArtifactProps): JSX.Element {
458
514
  fallback={
459
515
  <ArtifactPreview
460
516
  ref={(el) => (iframeEl = el)}
461
- src={currentUrl}
517
+ src={framedUrl}
462
518
  sandbox={local.sandbox}
463
519
  title={local.iframeTitle ?? 'Artifact preview'}
464
520
  onLoad={onIframeLoad}
@@ -719,12 +775,30 @@ function ArtifactCode(props: CodeProps): JSX.Element {
719
775
 
720
776
  // --- ArtifactPdfFallback (internal) ---------------------------------------
721
777
 
722
- /** Shown when inline PDF rendering is disabled or fails (CORS / load / parse). */
778
+ /** Shown when inline PDF rendering is disabled or fails (CORS / load / parse).
779
+ *
780
+ * THIS IS THE DEFAULT VIEW FOR A HOSTILE URL, not an edge case: pdf.js has to
781
+ * FETCH the document to render it inline, and it cannot fetch a `javascript:`
782
+ * or `data:` one, so every such url fails straight through to here -- where two
783
+ * anchors used to put it in an `href`, one click from executing in the host
784
+ * page's origin. The url is model-reachable (`artifact` card `src` /
785
+ * `files[].url`, schema-validated only as `format: "uri"`), so it is filtered
786
+ * with the same `isSafeUrl` the markdown renderer and the card `open` verb use.
787
+ * Relative paths pass, which matters here: `resolveFileUrl` returns a bare
788
+ * `path` when the artifact has no `src`. */
723
789
  function ArtifactPdfFallback(props: { url: string }): JSX.Element {
724
790
  const name = () => {
725
791
  const path = props.url.split(/[?#]/)[0];
726
792
  return path.slice(path.lastIndexOf('/') + 1) || 'document.pdf';
727
793
  };
794
+ /** The url actually put in `href`, or `undefined` -- which OMITS the attribute.
795
+ * Never `''`: an empty href navigates to the current page. */
796
+ const href = createMemo(() => {
797
+ if (!props.url) return undefined;
798
+ if (isSafeUrl(props.url)) return props.url;
799
+ warnBlockedUrl('pdf fallback', props.url);
800
+ return undefined;
801
+ });
728
802
  const linkClass =
729
803
  'inline-flex h-8 items-center gap-1.5 rounded-md border border-border bg-background px-3 text-xs font-medium text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring';
730
804
  return (
@@ -736,16 +810,28 @@ function ArtifactPdfFallback(props: { url: string }): JSX.Element {
736
810
  <FileText size={40} class="text-muted-foreground" aria-hidden="true" />
737
811
  <div class="text-sm font-medium text-foreground">{name()}</div>
738
812
  <div class="text-xs text-muted-foreground">Can't preview this PDF inline.</div>
739
- <div class="flex flex-wrap items-center justify-center gap-2">
740
- <a class={linkClass} href={props.url} target="_blank" rel="noopener noreferrer">
741
- Open in new tab
742
- <ExternalLink size={13} aria-hidden="true" />
743
- </a>
744
- <a class={linkClass} href={props.url} download="">
745
- <Download size={13} aria-hidden="true" />
746
- Download
747
- </a>
748
- </div>
813
+ {/* No safe address, no controls. Two buttons that silently do nothing
814
+ would be worse than saying what happened -- and a bare `<a>` with the
815
+ href dropped is exactly that. */}
816
+ <Show
817
+ when={href()}
818
+ fallback={
819
+ <div class="text-xs text-muted-foreground">
820
+ Blocked: that address uses a scheme this viewer won't open.
821
+ </div>
822
+ }
823
+ >
824
+ <div class="flex flex-wrap items-center justify-center gap-2">
825
+ <a class={linkClass} href={href()} target="_blank" rel="noopener noreferrer">
826
+ Open in new tab
827
+ <ExternalLink size={13} aria-hidden="true" />
828
+ </a>
829
+ <a class={linkClass} href={href()} download="">
830
+ <Download size={13} aria-hidden="true" />
831
+ Download
832
+ </a>
833
+ </div>
834
+ </Show>
749
835
  </div>
750
836
  );
751
837
  }
@@ -1,10 +1,86 @@
1
1
  import { splitProps, createMemo, createUniqueId, For, Show, Switch, Match } from 'solid-js';
2
2
  import { cn } from '../utils/cn';
3
- import { marked } from 'marked';
3
+ import { Marked } from 'marked';
4
4
  import { CodeBlock, CodeBlockCode } from './code-block';
5
5
  import { useChatConfig, textClass } from '../primitives/chat-config';
6
+ import { isSafeUrl } from '../primitives/card-routing';
6
7
 
7
- marked.setOptions({ gfm: true, breaks: true });
8
+ // --- The markdown sink -------------------------------------------------------
9
+ //
10
+ // `MarkdownBlock` writes this parser's output to `innerHTML`, so THIS is the one
11
+ // place in the kit where a string the model produced becomes live DOM in the
12
+ // host page's origin. `marked` passes raw HTML through verbatim by default and
13
+ // dropped its own URL sanitizing in v5, so both have to be handled here.
14
+ //
15
+ // The threat model is NOT a hostile provider. An attacker only needs to
16
+ // influence the model's OUTPUT -- a user asking to be shown an `<img onerror>`
17
+ // example, a prompt-injected model, or RAG over an untrusted document all reach
18
+ // this sink against a perfectly trusted provider.
19
+ //
20
+ // WHY ESCAPE RATHER THAN SANITIZE (no DOMPurify):
21
+ // - A sanitizer needs a DOM. In Node this package's ESM build has no
22
+ // `sanitize` at all (`isSupported === false`), so the SSR path either throws
23
+ // or gets "guarded" into returning the payload UNCHANGED -- a silent
24
+ // passthrough of exactly the string we were trying to neutralise. Escaping
25
+ // is pure string work and behaves identically on server and client.
26
+ // - It costs no dependency. Escaping is the ~20 lines below; DOMPurify is
27
+ // ~35 KB gzipped of ESM in a package that gates its own footprint.
28
+ // - For a CHAT UI it is also the better RENDERING, not just the safer one.
29
+ // Raw HTML in model output is either the model quoting markup -- which the
30
+ // reader should SEE as text -- or an injection. Escaping serves the first
31
+ // case correctly and defuses the second. Nothing in this repo emits markdown
32
+ // that relies on inline HTML.
33
+ // There is deliberately NO opt-in to restore raw HTML: nothing needs it today,
34
+ // and an unsafe switch is the hole again for whoever flips it.
35
+ //
36
+ // This is a PRIVATE `Marked` instance, and that is load-bearing rather than
37
+ // tidiness. Configuring the shared `marked` singleton would mean a consumer's
38
+ // own `marked.use({ renderer })` elsewhere in the same app silently replaces
39
+ // this renderer and reopens the vulnerability. It also stops the kit mutating
40
+ // global `marked` options out from under a consumer, which it used to do.
41
+
42
+ /** Escape for HTML TEXT content. */
43
+ function escapeText(s: string): string {
44
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
45
+ }
46
+
47
+ /** Escape for a double-quoted ATTRIBUTE value. */
48
+ function escapeAttr(s: string): string {
49
+ return escapeText(s).replace(/"/g, '&quot;');
50
+ }
51
+
52
+ const md = new Marked({
53
+ gfm: true,
54
+ breaks: true,
55
+ renderer: {
56
+ /** Raw HTML -- block (`Tokens.HTML`) and inline (`Tokens.Tag`) both land
57
+ * here. Rendered as visible text instead of markup. */
58
+ html({ text }) {
59
+ return escapeText(text);
60
+ },
61
+
62
+ /** Markdown link syntax. marked BUILDS this anchor itself, so escaping raw
63
+ * HTML does not cover it and it needs the scheme allowlist of its own.
64
+ * A blocked link keeps its LABEL -- the reader still sees what the model
65
+ * wrote, we simply refuse to make it clickable. */
66
+ link({ href, title, tokens }) {
67
+ const text = this.parser.parseInline(tokens);
68
+ if (!isSafeUrl(href)) return text;
69
+ const t = title ? ` title="${escapeAttr(title)}"` : '';
70
+ return `<a href="${escapeAttr(href)}"${t}>${text}</a>`;
71
+ },
72
+
73
+ /** Markdown image syntax. Same reasoning as `link`; a blocked image falls
74
+ * back to its alt text. NOTE this also blocks `data:` image URIs, which
75
+ * are inert inside `<img>` -- one allowlist for both is worth more than
76
+ * the rare inline-data-image case it costs. */
77
+ image({ href, title, text }) {
78
+ if (!isSafeUrl(href)) return escapeText(text);
79
+ const t = title ? ` title="${escapeAttr(title)}"` : '';
80
+ return `<img src="${escapeAttr(href)}" alt="${escapeAttr(text)}"${t}>`;
81
+ },
82
+ },
83
+ });
8
84
 
9
85
  export interface MarkdownProps {
10
86
  content: string;
@@ -23,7 +99,7 @@ interface ParsedBlock {
23
99
  }
24
100
 
25
101
  function parseMarkdownIntoBlocks(markdown: string): ParsedBlock[] {
26
- const tokens = marked.lexer(markdown);
102
+ const tokens = md.lexer(markdown);
27
103
  return tokens.map((token) => {
28
104
  if (token.type === 'code') {
29
105
  return {
@@ -42,9 +118,11 @@ function parseMarkdownIntoBlocks(markdown: string): ParsedBlock[] {
42
118
  function MarkdownBlock(props: { content: string }) {
43
119
  const html = createMemo(() => {
44
120
  try {
45
- return marked.parse(props.content, { async: false }) as string;
121
+ return md.parse(props.content, { async: false }) as string;
46
122
  } catch {
47
- return props.content;
123
+ // The parse failed, so nothing has been escaped -- the raw source must
124
+ // NOT go to `innerHTML`. Escape it and show it as text.
125
+ return escapeText(props.content);
48
126
  }
49
127
  });
50
128
 
@@ -1,6 +1,7 @@
1
1
  import { type JSX, createContext, useContext, Show, splitProps } from 'solid-js';
2
2
  import { cn } from '../utils/cn';
3
3
  import { HoverCardRoot, HoverCardTrigger, HoverCardContent } from '../ui/hover-card';
4
+ import { isRenderableLink } from '../primitives/link-preview';
4
5
 
5
6
  // --- Context ---
6
7
 
@@ -30,12 +31,43 @@ export interface SourceProps {
30
31
  }
31
32
 
32
33
  function Source(props: SourceProps) {
33
- const href = () => props.href ?? '';
34
+ const raw = () => props.href ?? '';
35
+
36
+ /** The url actually put in `href`.
37
+ *
38
+ * A citation url is MODEL-SUPPLIED: `wire/formats/openai.ts` (`sourcesOf`)
39
+ * and `wire/formats/anthropic.ts` (`citations_delta`) both take it as an
40
+ * arbitrary string. `javascript:`/`data:` in an href is script execution one
41
+ * click away, and today the only thing stopping it is `target="_blank"` plus
42
+ * current Chrome behaviour -- an accident of the browser, not a guard. Anyone
43
+ * dropping `target="_blank"` for styling would silently make it live.
44
+ *
45
+ * Filtered HERE, at render, rather than in `wire/`, for two reasons.
46
+ * (1) The wire layer is OPTIONAL: a consumer with their own backend adapter
47
+ * can set `messages` containing `source` parts directly and never touch
48
+ * `wire/`, so a wire-side filter would leave that path exploitable while
49
+ * looking fixed. Every path renders through here.
50
+ * (2) `parts` should keep reporting what the model actually said, so a
51
+ * consumer logging or auditing citations is not lied to. The kit refuses to
52
+ * LINK the url; it does not pretend the url was never there.
53
+ *
54
+ * `isRenderableLink` (absolute http(s) only) is the right guard rather than
55
+ * `isSafeUrl` from card-routing: a citation is a reference to a page on the
56
+ * web, so unlike a markdown body link it has no business being relative. */
57
+ const href = () => (isRenderableLink(raw()) ? raw() : '');
58
+
59
+ /** Display only, and deliberately derived from the RAW value: a blocked url
60
+ * still has to label its chip, and dropping to an empty string here would
61
+ * render a blank chip instead of showing the reader what arrived. The value
62
+ * lands in a text node, which Solid escapes. */
34
63
  const domain = () => {
35
- const h = href();
64
+ const h = raw();
36
65
  if (!h) return '';
37
66
  try {
38
- return new URL(h).hostname;
67
+ // `new URL('javascript:x')` parses fine and yields an EMPTY hostname, so
68
+ // a blocked citation would otherwise label an empty chip. Fall back to the
69
+ // raw string: the reader sees what arrived, just not as a link.
70
+ return new URL(h).hostname || h;
39
71
  } catch {
40
72
  return h.split('/').pop() || h;
41
73
  }
@@ -17,8 +17,60 @@ export function emitCardEvent(element: HTMLElement, event: CardEvent): void {
17
17
  }
18
18
 
19
19
  const SAFE_SCHEMES = ['http:', 'https:', 'mailto:'];
20
- function isSafeUrl(url: string): boolean {
21
- try { return SAFE_SCHEMES.includes(new URL(url, 'http://_invalid_base').protocol); } catch { return false; }
20
+ const SCRIPT_SCHEMES = ['javascript:', 'vbscript:'];
21
+
22
+ /** The scheme the WHATWG parser reads out of `url`, resolved against a base so a
23
+ * relative input inherits `http:`. `undefined` when it will not parse at all.
24
+ *
25
+ * ONE parser for the two questions below, deliberately: the parsing is where
26
+ * the subtlety lives (it strips embedded tabs/newlines and trims surrounding
27
+ * whitespace before reading the scheme, so `java\nscript:` and
28
+ * ` javascript:... ` are both read as `javascript:` -- which a regex over the
29
+ * raw string would miss). Two predicates over one parse cannot drift on that;
30
+ * two hand-rolled parsers would. */
31
+ function schemeOf(url: string): string | undefined {
32
+ try { return new URL(url, 'http://_invalid_base').protocol; } catch { return undefined; }
33
+ }
34
+
35
+ /** True when `url` is safe to put in an href / hand to `window.open`.
36
+ *
37
+ * Exported so the MARKDOWN renderer and the ARTIFACT viewer reuse this exact
38
+ * guard rather than growing a second scheme list that can drift from this one.
39
+ *
40
+ * Resolving against a base is deliberate and is what makes it correct for
41
+ * markdown: a relative or fragment link (`/docs`, `#section`, `./rel`) is
42
+ * ordinary markdown, and resolving it inherits the base's `http:` so it
43
+ * passes, while `javascript:`/`data:`/`vbscript:` keep their own protocol and
44
+ * fail. It is correct for the artifact viewer for the same reason: a file with
45
+ * no `src` to resolve against yields a bare relative path (`docs/report.pdf`),
46
+ * which is a legitimate artifact address. Contrast `isRenderableLink` in
47
+ * `primitives/link-preview.ts`, which takes NO base and so demands an absolute
48
+ * http(s) URL: that is the right guard for a model-supplied citation -- a
49
+ * reference to a page on the public web -- this one for markdown body links and
50
+ * artifact file addresses. */
51
+ export function isSafeUrl(url: string): boolean {
52
+ const scheme = schemeOf(url);
53
+ return scheme !== undefined && SAFE_SCHEMES.includes(scheme);
54
+ }
55
+
56
+ /** True when navigating to `url` would EXECUTE it as script in the initiating
57
+ * document's origin.
58
+ *
59
+ * Strictly narrower than `!isSafeUrl(url)`, and not a competing policy: it
60
+ * answers a different question, for the one sink where the allowlist is the
61
+ * wrong tool. An `<iframe src>` legitimately takes `data:` and `blob:` (a
62
+ * `data:` blob artifact is a documented `<kai-artifact>` use -- it is what
63
+ * `displayUrl` exists for) and both get an OPAQUE origin in every modern
64
+ * browser, so neither can reach the host page. `javascript:`/`vbscript:` are
65
+ * the schemes that run in the EMBEDDER's origin, and they are the whole risk:
66
+ * the default sandbox (no `allow-same-origin`) already makes the browser refuse
67
+ * them, but a consumer who sets `allow-same-origin` turns a model-supplied
68
+ * `src` into host-origin script execution. The artifact viewer used to suggest
69
+ * that setting for "an artifact you trust"; it no longer does, and this guard
70
+ * is what protects the consumers who took the old advice. */
71
+ export function isScriptUrl(url: string): boolean {
72
+ const scheme = schemeOf(url);
73
+ return scheme !== undefined && SCRIPT_SCHEMES.includes(scheme);
22
74
  }
23
75
  function warnNoHandler(kind: string): void {
24
76
  // eslint-disable-next-line no-console
@@ -84,6 +84,19 @@ function detailField(details: unknown[], key: 'index' | 'signature'): unknown {
84
84
  return undefined;
85
85
  }
86
86
 
87
+ /** The sibling reasoning STRINGS, in precedence order. See `applyReasoning`. */
88
+ const REASONING_KEYS = ['reasoning', 'reasoning_content'] as const;
89
+
90
+ /** First non-empty sibling string wins. These are alternative SPELLINGS of the
91
+ * same text, never two halves of it, so this coalesces and never concatenates. */
92
+ function siblingReasoning(delta: Record<string, unknown>): string | undefined {
93
+ for (const key of REASONING_KEYS) {
94
+ const raw = str(delta[key]);
95
+ if (raw !== undefined && raw !== '') return raw;
96
+ }
97
+ return undefined;
98
+ }
99
+
87
100
  /**
88
101
  * FINDINGS: OpenRouter frequently puts the SAME text in `reasoning` AND in
89
102
  * `reasoning_details` on the same delta. Concatenating both doubles every
@@ -92,11 +105,34 @@ function detailField(details: unknown[], key: 'index' | 'signature'): unknown {
92
105
  * `reasoning_details` is still read in BOTH cases, for `reasoningRaw`, the block
93
106
  * index and the signature. It is the provider's own block list, and dropping it
94
107
  * is exactly the Anthropic 400 this entry exists to avoid.
108
+ *
109
+ * SIBLING NAMES. Chat-completions says nothing about reasoning, so every vendor
110
+ * extended it on its own and each parser in the wild reads a different subset:
111
+ * `reasoning` is what OpenRouter normalises to, `reasoning_content` is what
112
+ * DeepSeek's own API emits and what LiteLLM forwards. Nobody is ahead here and
113
+ * there is no canonical spelling. A stream that only ever went through
114
+ * OpenRouter never shows the other name, which is the whole reason this one
115
+ * stayed invisible: reading `reasoning` alone loses a DeepSeek-direct stream's
116
+ * reasoning completely, and silently.
117
+ *
118
+ * PRECEDENCE. The siblings coalesce, first non-empty wins, and both outrank the
119
+ * details text. `reasoning` is checked first, which is what keeps this additive:
120
+ * any stream that already carried `reasoning` parses byte for byte as it did
121
+ * before, so the order is only observable on a provider that disagrees with
122
+ * itself. A gateway that aliases one spelling onto the other sends identical
123
+ * text in both, and summing them would double every token exactly the way
124
+ * summing `reasoning` and `reasoning_details` does.
125
+ *
126
+ * SCOPE, and it is NOT a closed class. This covers the NAMING axis only.
127
+ * Reasoning on this wire is fragmented three ways STRUCTURALLY: a sibling string
128
+ * (here), a block array (`reasoning_details`), and reasoning carried inside a
129
+ * polymorphic `content` array. Aliasing bridges names, not shapes. A fourth
130
+ * spelling is one entry in REASONING_KEYS; a fourth SHAPE is not, and needs its
131
+ * own branch plus its own fixture.
95
132
  */
96
133
  function applyReasoning(delta: Record<string, unknown>, out: ModelStreamChunk): void {
97
134
  const details = Array.isArray(delta.reasoning_details) ? delta.reasoning_details : undefined;
98
- const primaryRaw = str(delta.reasoning);
99
- const primary = primaryRaw !== undefined && primaryRaw !== '' ? primaryRaw : undefined;
135
+ const primary = siblingReasoning(delta);
100
136
  const fallback = details ? detailText(details) : undefined;
101
137
  const hasDetails = details !== undefined && details.length > 0;
102
138
  const text = primary ?? fallback;
@@ -1 +0,0 @@
1
- import{c as a,m as P,s as ce,g as k,k as C,l as F,x as G,o as Z,q as ue,i as h,S as g,t as v,d as p,a as y,b as J,v as Q,j as he,h as fe,e as ge}from"./define-B_tXMYRm.js";import{c as N}from"./cn-CU7UAWzk.js";import{B as z}from"./button-B9okqG73.js";import{C as be,a as me}from"./code-block-ClhQO9RW.js";import{F as ve}from"./file-tree-yGWNK0Js.js";import{L as pe}from"./loader-B5QzK_8_.js";import{a as we}from"./arrow-left-iJVnQmxA.js";import{I as X}from"./Icon-b5A8hlHs.js";import{r as xe,h as ke,c as ye}from"./rotate-cw-DzVDJOA1.js";import{d as ze}from"./download-DnCyWFJE.js";import{e as Y}from"./external-link-DG2sRdQL.js";import{f as Te}from"./file-text-D0J9F6M7.js";import{m as Ce,a as Fe}from"./minimize-2-SegJ_oku.js";var Pe=[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"m12 5 7 7-7 7",key:"xquz4c"}]],Se=r=>a(X,P(r,{name:"ArrowRight",iconNode:Pe})),Oe=Se,Re=[["path",{d:"M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z",key:"rwhkz3"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]],je=r=>a(X,P(r,{name:"Eye",iconNode:Re})),Ie=je;const Ne="6.0.227",ee=`https://cdn.jsdelivr.net/npm/pdfjs-dist@${Ne}/build`,He=()=>import(/*@vite-ignore*/`${ee}/pdf.min.mjs`),Me=`${ee}/pdf.worker.min.mjs`;let Ee=He,Le=Me,H=null;function Ue(){if(!H){const r=Ee;H=(async()=>{const t=await r(),e=t.default??t;return e.GlobalWorkerOptions.workerSrc=Le,e})()}return H}async function $e(r,t,e){const o=await(await Ue()).getDocument({url:r}).promise;t.replaceChildren();const l=typeof window<"u"&&window.devicePixelRatio?window.devicePixelRatio:1;for(let s=1;s<=o.numPages;s++){const d=await o.getPage(s),c=d.getViewport({scale:1}),u=c.width>0?e/c.width:1,f=d.getViewport({scale:u*l}),b=document.createElement("canvas");b.width=Math.max(1,Math.floor(f.width)),b.height=Math.max(1,Math.floor(f.height)),b.setAttribute("role","img"),b.setAttribute("aria-label",`Page ${s}`),b.style.width="100%",b.style.height="auto",b.style.display="block";const w=b.getContext("2d");t.appendChild(b),await d.render({canvasContext:w,viewport:f}).promise}return{pages:o.numPages}}var Be=v('<div><div class="relative min-h-0 flex-1">'),De=v('<form class="min-w-0 flex-1"><label class=sr-only for=kai-artifact-path>Address</label><input id=kai-artifact-path name=kai-artifact-path type=text autocomplete=off placeholder="Enter a path or URL\u2026">'),Ae=v("<div class=flex-1 aria-hidden=true>"),Ve=v('<div role=tablist aria-label=View class="flex shrink-0 items-center gap-0.5 rounded-md bg-muted p-0.5">'),We=v('<div data-artifact-toolbar class="flex shrink-0 items-center gap-1.5 border-b border-border bg-surface px-2 py-1.5">'),qe=v("<button type=button role=tab>"),Ke=v('<iframe class="absolute inset-0 h-full w-full border-0 bg-white">'),_e=v('<div class="absolute inset-0 flex"><div class="w-56 shrink-0 overflow-auto border-r border-border bg-surface-sunken py-1.5 scrollbar-thin"></div><div class="min-w-0 flex-1 overflow-auto scrollbar-thin">'),Ge=v('<div class="flex h-full items-center justify-center p-6 text-sm text-muted-foreground">Select a file to view its source.'),Ze=v('<div class="flex h-full items-center justify-center p-6 text-sm text-muted-foreground">No source \u2014 this file (<!>) has no code view.'),Je=v(`<div role=region aria-label="PDF preview unavailable"class="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-card p-6 text-center"><div class="text-sm font-medium text-foreground"></div><div class="text-xs text-muted-foreground">Can't preview this PDF inline.</div><div class="flex flex-wrap items-center justify-center gap-2"><a class="inline-flex h-8 items-center gap-1.5 rounded-md border border-border bg-background px-3 text-xs font-medium text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"target=_blank rel="noopener noreferrer">Open in new tab</a><a class="inline-flex h-8 items-center gap-1.5 rounded-md border border-border bg-background px-3 text-xs font-medium text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"download>Download`),Qe=v('<div class="pointer-events-none absolute inset-0 flex items-center justify-center">'),Xe=v('<div class="absolute inset-0"><div role=region aria-label="PDF preview"tabindex=0 class="absolute inset-0 flex flex-col items-center gap-3 overflow-auto bg-surface-sunken p-3 scrollbar-thin focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset">');const Ye="allow-scripts allow-forms";function er(r,t){if(r.url)return r.url;if(!t)return r.path;try{return new URL(r.path,t).href}catch{return r.path}}function rr(r,t){if(!r)return!1;const e=t.find(l=>l.url===r||l.path===r);if(e?.type==="pdf")return!0;const o=r.split(/[?#]/)[0];return/\.pdf$/i.test(o)}function nr(r){const t=P({files:[],defaultTab:"preview",sandbox:Ye,showNav:!0,showReload:!0,showHome:!0,showPathField:!0,showTabs:!0,expandable:!1,openInTab:!1,standalone:!1,readonlyPath:!1,maximized:!1},r),[e,o]=ce(t,["src","files","tab","defaultTab","activeFile","sandbox","iframeTitle","onNavigate","onTabChange","onFileSelect","maximized","onMaximizeChange","showNav","showReload","showHome","showPathField","showTabs","expandable","openInTab","standalone","readonlyPath","displayUrl","controllerRef","class"]),[l,s]=k(e.src?[e.src]:[]),[d,c]=k(e.src?0:-1),u=()=>l()[d()]??"",f=()=>d()>0,b=()=>d()<l().length-1,[w,M]=k(e.tab??e.defaultTab),[E,L]=k(e.activeFile),[ne,te]=k(0),[S,O]=k(e.maximized??!1);C(()=>O(e.maximized??!1));const ae=()=>{var n;const i=!S();O(i),(n=e.onMaximizeChange)==null||n.call(e,i)},U=F(()=>{const n=u();return!!n&&n!=="about:blank"}),$=()=>{U()&&window.open(u(),"_blank","noopener,noreferrer")},B=F(()=>e.expandable&&!e.standalone),ie=F(()=>e.showNav||e.showReload||e.showHome||e.showPathField||e.showTabs||B()||e.openInTab);let x;C(()=>{e.tab!==void 0&&M(e.tab)}),C(()=>L(e.activeFile)),C(G(()=>e.src,n=>{n&&n!==u()&&T(n)},{defer:!0}));const D=n=>n===void 0?void 0:e.files.find(i=>i.path===n),A=F(()=>D(E())),oe=F(()=>{const n=A();return!!n&&n.type!=="image"&&n.type!=="pdf"&&typeof n.code=="string"});function T(n){if(n===u()){j();return}s(i=>[...i.slice(0,d()+1),n]),c(i=>i+1),R()}function R(){var n;const i=u();x&&(x.src=i||"about:blank"),(n=e.onNavigate)==null||n.call(e,i)}function le(n){var i;n!==w()&&(M(n),(i=e.onTabChange)==null||i.call(e,n))}function V(n,i){var m;L(n),(m=e.onFileSelect)==null||m.call(e,n),T(er(i,e.src))}const W=()=>{f()&&(c(n=>n-1),R())},q=()=>{b()&&(c(n=>n+1),R())};function j(){var n;const i=u();x&&(x.src="about:blank",x.src=i||"about:blank"),te(m=>m+1),(n=e.onNavigate)==null||n.call(e,i)}const K=()=>{e.src&&T(e.src)},_=n=>{var i;S()!==n&&(O(n),(i=e.onMaximizeChange)==null||i.call(e,n))};Z(()=>{var n;(n=e.controllerRef)==null||n.call(e,{back:()=>W(),forward:()=>q(),reload:()=>j(),home:()=>K(),navigate:i=>T(i),selectFile:i=>{const m=D(i);m&&V(i,m)},openExternal:()=>$(),maximize:()=>_(!0),restore:()=>_(!1)})});const se=()=>{var n,i;try{const m=(n=x?.contentWindow)==null?void 0:n.location.href;m&&m!=="about:blank"&&m!==u()&&(s(I=>[...I.slice(0,d()+1),m]),c(I=>I+1),(i=e.onNavigate)==null||i.call(e,m))}catch{}},de=n=>{n.preventDefault();const i=n.currentTarget.elements.namedItem("kai-artifact-path");if(e.readonlyPath||e.displayUrl!=null){i&&(i.value=e.displayUrl??u());return}i&&i.value&&T(i.value)};return(()=>{var n=Be(),i=n.firstChild;return ue(n,P({get class(){return N("flex h-full w-full flex-col overflow-hidden bg-card text-card-foreground",e.standalone&&"rounded-xl border border-border",e.class)}},o),!1,!0),h(n,a(g,{get when(){return ie()},get children(){return a(tr,{url:()=>e.displayUrl??u(),tab:w,canBack:f,canForward:b,canHome:()=>!!e.src,onBack:W,onForward:q,onReload:j,onHome:K,onSubmitPath:de,onTab:le,showNav:()=>e.showNav,showReload:()=>e.showReload,showHome:()=>e.showHome,showPathField:()=>e.showPathField,showTabs:()=>e.showTabs,showExpand:B,showOpenInTab:()=>e.openInTab,maximized:S,onToggleMaximize:ae,canOpenInTab:U,onOpenInTab:$,readonlyPath:()=>e.readonlyPath||e.displayUrl!=null})}}),i),h(i,a(g,{get when(){return w()==="preview"},get fallback(){return a(ir,{get files(){return e.files},activeFile:E,activeFileObj:A,hasSource:oe,onSelect:V})},get children(){return a(g,{get when(){return rr(u(),e.files)},get fallback(){return a(ar,{ref:m=>x=m,src:u,get sandbox(){return e.sandbox},get title(){return e.iframeTitle??"Artifact preview"},onLoad:se})},get children(){return a(lr,{get url(){return u()},get reloadKey(){return ne()}})}})}})),n})()}function tr(r){return(()=>{var t=We();return h(t,a(g,{get when(){return r.showNav()},get children(){return[a(z,{variant:"ghost",size:"icon-sm","aria-label":"Back",get disabled(){return!r.canBack()},onClick:()=>r.onBack(),get children(){return a(we,{size:16,"aria-hidden":"true"})}}),a(z,{variant:"ghost",size:"icon-sm","aria-label":"Forward",get disabled(){return!r.canForward()},onClick:()=>r.onForward(),get children(){return a(Oe,{size:16,"aria-hidden":"true"})}})]}}),null),h(t,a(g,{get when(){return r.showReload()},get children(){return a(z,{variant:"ghost",size:"icon-sm","aria-label":"Reload",onClick:()=>r.onReload(),get children(){return a(xe,{size:15,"aria-hidden":"true"})}})}}),null),h(t,a(g,{get when(){return r.showHome()},get children(){return a(z,{variant:"ghost",size:"icon-sm","aria-label":"Home",get disabled(){return!r.canHome()},onClick:()=>r.onHome(),get children(){return a(ke,{size:15,"aria-hidden":"true"})}})}}),null),h(t,a(g,{get when(){return r.showPathField()},get children(){var e=De(),o=e.firstChild,l=o.nextSibling;return e.addEventListener("submit",s=>r.onSubmitPath(s)),p(l,"spellcheck",!1),y(s=>{var d=r.readonlyPath()||void 0,c=r.readonlyPath()?"true":void 0,u=N("h-7 w-full rounded-md border border-border px-2.5 text-xs text-foreground font-mono outline-none",r.readonlyPath()?"bg-muted/40 cursor-default":"bg-background focus-visible:ring-2 focus-visible:ring-ring");return d!==s.e&&(l.readOnly=s.e=d),c!==s.t&&p(l,"aria-readonly",s.t=c),u!==s.a&&J(l,s.a=u),s},{e:void 0,t:void 0,a:void 0}),y(()=>l.value=r.url()),e}}),null),h(t,a(g,{get when(){return!r.showPathField()},get children(){return Ae()}}),null),h(t,a(g,{get when(){return r.showExpand()},get children(){return a(z,{variant:"ghost",size:"icon-sm",get"aria-label"(){return r.maximized()?"Collapse":"Expand"},get"aria-expanded"(){return r.maximized()},onClick:()=>r.onToggleMaximize(),get children(){return a(g,{get when(){return r.maximized()},get fallback(){return a(Fe,{size:15,"aria-hidden":"true"})},get children(){return a(Ce,{size:15,"aria-hidden":"true"})}})}})}}),null),h(t,a(g,{get when(){return r.showOpenInTab()},get children(){return a(z,{variant:"ghost",size:"icon-sm","aria-label":"Open in new tab",get disabled(){return!r.canOpenInTab()},onClick:()=>r.onOpenInTab(),get children(){return a(Y,{size:15,"aria-hidden":"true"})}})}}),null),h(t,a(g,{get when(){return r.showTabs()},get children(){var e=Ve();return h(e,a(re,{label:"Preview",get icon(){return a(Ie,{size:14,"aria-hidden":"true"})},get selected(){return r.tab()==="preview"},onClick:()=>r.onTab("preview")}),null),h(e,a(re,{label:"Code",get icon(){return a(ye,{size:14,"aria-hidden":"true"})},get selected(){return r.tab()==="code"},onClick:()=>r.onTab("code")}),null),e}}),null),t})()}function re(r){return(()=>{var t=qe();return t.$$click=()=>r.onClick(),h(t,()=>r.icon,null),h(t,()=>r.label,null),y(e=>{var o=r.selected,l=N("inline-flex h-6 items-center gap-1.5 rounded px-2 text-xs font-medium transition-colors outline-none","focus-visible:ring-2 focus-visible:ring-ring",r.selected?"bg-background text-foreground shadow-sm":"text-muted-foreground hover:text-foreground");return o!==e.e&&p(t,"aria-selected",e.e=o),l!==e.t&&J(t,e.t=l),e},{e:void 0,t:void 0}),t})()}function ar(r){return(()=>{var t=Ke();t.addEventListener("load",()=>r.onLoad());var e=r.ref;return typeof e=="function"?Q(e,t):r.ref=t,y(o=>{var l=r.src()||"about:blank",s=r.sandbox,d=r.title;return l!==o.e&&p(t,"src",o.e=l),s!==o.t&&p(t,"sandbox",o.t=s),d!==o.a&&p(t,"title",o.a=d),o},{e:void 0,t:void 0,a:void 0}),t})()}function ir(r){return(()=>{var t=_e(),e=t.firstChild,o=e.nextSibling;return h(e,a(ve,{get files(){return r.files},get activeFile(){return r.activeFile()},onSelect:(l,s)=>r.onSelect(l,s)})),h(o,a(g,{get when(){return r.activeFileObj()},get fallback(){return Ge()},get children(){return a(g,{get when(){return r.hasSource()},get fallback(){return(()=>{var l=Ze(),s=l.firstChild,d=s.nextSibling;return d.nextSibling,h(l,()=>r.activeFileObj().type??"binary",d),l})()},get children(){return a(be,{class:"h-full rounded-none border-0",get children(){return a(me,{get code(){return r.activeFileObj().code??""},get language(){return r.activeFileObj().language}})}})}})}})),t})()}function or(r){const t=()=>{const e=r.url.split(/[?#]/)[0];return e.slice(e.lastIndexOf("/")+1)||"document.pdf"};return(()=>{var e=Je(),o=e.firstChild,l=o.nextSibling,s=l.nextSibling,d=s.firstChild;d.firstChild;var c=d.nextSibling,u=c.firstChild;return h(e,a(Te,{size:40,class:"text-muted-foreground","aria-hidden":"true"}),o),h(o,t),h(d,a(Y,{size:13,"aria-hidden":"true"}),null),h(c,a(ze,{size:13,"aria-hidden":"true"}),u),y(f=>{var b=r.url,w=r.url;return b!==f.e&&p(d,"href",f.e=b),w!==f.t&&p(c,"href",f.t=w),f},{e:void 0,t:void 0}),e})()}function lr(r){const[t,e]=k("loading");let o,l=0,s;const d=async()=>{if(!o){e("error");return}const c=++l;e("loading");try{const u=o.clientWidth||600;await $e(r.url,o,u),c===l&&e("success")}catch{c===l&&e("error")}};return C(G(()=>[r.url,r.reloadKey],()=>{d()})),Z(()=>{if(!o||typeof ResizeObserver>"u")return;const c=new ResizeObserver(()=>{t()==="success"&&(clearTimeout(s),s=setTimeout(()=>{d()},200))});c.observe(o),fe(()=>{c.disconnect(),clearTimeout(s),l++})}),(()=>{var c=Xe(),u=c.firstChild;return Q(f=>o=f,u),h(c,a(g,{get when(){return he(()=>!0)()&&t()==="loading"},get children(){var f=Qe();return h(f,a(pe,{variant:"circular",size:"md"})),f}}),null),h(c,a(g,{get when(){return t()==="error"},get children(){return a(or,{get url(){return r.url}})}}),null),y(()=>p(u,"aria-busy",t()==="loading")),c})()}ge(["click"]);export{nr as A};
@@ -1 +0,0 @@
1
- const o="kai-card";function c(t,e){t.dispatchEvent(new CustomEvent(o,{detail:e,bubbles:!0,composed:!0}))}const d=["http:","https:","mailto:"];function i(t){try{return d.includes(new URL(t,"http://_invalid_base").protocol)}catch{return!1}}function r(t){console.warn(`[kai-card] no policy handler for "${t}"`)}function p(t,e){const n=t??{};switch(e.kind){case"ready":break;case"submit":n.onSubmit?n.onSubmit(e.cardId,e.data):r("submit");break;case"action":n.onAction?n.onAction(e.cardId,e.action,e.payload):r("action");break;case"send-prompt":{const a=e.mode??"compose",s=n.maxSendPromptMode==="send"?a:"compose";n.onSendPrompt?n.onSendPrompt(e.text,{mode:s,context:e.context}):r("send-prompt");break}case"open":{if(!i(e.url)){n.onError?n.onError(e.cardId,`Blocked unsafe url: ${e.url}`):r("open(unsafe)");break}const a=e.target??"tab";n.onOpen?n.onOpen(e.url,a):typeof window<"u"&&window.open(e.url,"_blank","noopener,noreferrer");break}case"state":n.onState?n.onState(e.cardId,e.patch):r("state");break;case"dismiss":n.onDismiss?n.onDismiss(e.cardId):r("dismiss");break;case"reopen":n.onReopen?n.onReopen(e.cardId):r("reopen");break;case"error":n.onError?n.onError(e.cardId,e.message):r("error");break}}export{o as C,c as e,p as r};
@@ -1 +0,0 @@
1
- import{s as q,g as x,l as v,o as B,k as L,x as C,c as n,i as a,S as g,a as m,d as f,b as S,t as c,e as F}from"./define-B_tXMYRm.js";import{c as z}from"./cn-CU7UAWzk.js";import{S as w}from"./skeleton-N8BuWYmf.js";import{l as D}from"./link-BdsfLB7b.js";function G(d){try{return new URL(d).hostname.replace(/^www\./,"")}catch{return}}const H=["http:","https:"];function J(d){try{return H.includes(new URL(d).protocol)}catch{return!1}}var K=c('<div class="space-y-2 p-3">'),M=c('<img role=img class="aspect-[16/9] w-full object-cover">'),P=c('<img alt class="h-3.5 w-3.5 shrink-0 rounded-sm">'),Q=c('<div class="line-clamp-3 text-xs text-muted-foreground">'),T=c('<div class="space-y-1 p-3"><div class="flex items-center gap-1.5 text-xs text-muted-foreground"><span class=truncate></span></div><div class="line-clamp-2 text-sm font-semibold text-foreground">'),V=c('<a rel="noopener noreferrer">'),W=c('<div role=img aria-label="Invalid link"><span class=truncate>Invalid link');function X(d){return!!(d.title||d.description||d.image)}function Y(d){const[i]=q(d,["cardId","data","onEmit","class"]),b=r=>{var e;return(e=i.onEmit)==null?void 0:e.call(i,r)},[R,_]=x(),[y,Z]=x(!1),[U,I]=x(!1),u=()=>i.data.url,h=v(()=>J(u())),s=v(()=>({...i.data,...R()}));B(()=>b({kind:"ready",cardId:i.cardId})),L(C(h,r=>{r||b({kind:"error",cardId:i.cardId,message:`Invalid link url: ${u()}`})})),L(C(()=>[i.data,h()],([r,e])=>{I(!1),!e||X(r)}));const k=v(()=>s().domain??G(u())??u()),j=v(()=>s().siteName??k()),$=v(()=>s().title??k()),A=v(()=>!!s().image&&!U()),E=()=>{h()&&b({kind:"open",cardId:i.cardId,url:u(),target:"tab"})},N=r=>{r.preventDefault(),E()},O=r=>{(r.key==="Enter"||r.key===" ")&&(r.preventDefault(),E())};return n(g,{get when(){return h()},get fallback(){return(()=>{var r=W(),e=r.firstChild;return a(r,n(D,{size:16,"aria-hidden":"true"}),e),m(()=>S(r,z("flex items-center gap-2 rounded-xl border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive dark:border-red-400/50 dark:bg-destructive/20 dark:text-red-400",i.class))),r})()},get children(){var r=V();return r.$$keydown=O,r.$$click=N,a(r,n(g,{get when(){return y()},get children(){return[n(w,{class:"aspect-[16/9] w-full rounded-none"}),(()=>{var e=K();return a(e,n(w,{class:"h-3 w-1/3"}),null),a(e,n(w,{class:"h-4 w-2/3"}),null),a(e,n(w,{class:"h-3 w-full"}),null),e})()]}}),null),a(r,n(g,{get when(){return!y()},get children(){return[n(g,{get when(){return A()},get children(){var e=M();return e.addEventListener("error",()=>I(!0)),m(t=>{var l=s().image,o=s().imageAlt??"";return l!==t.e&&f(e,"src",t.e=l),o!==t.t&&f(e,"alt",t.t=o),t},{e:void 0,t:void 0}),e}}),(()=>{var e=T(),t=e.firstChild,l=t.firstChild,o=t.nextSibling;return a(t,n(g,{get when(){return s().favicon},get fallback(){return n(D,{size:12,class:"shrink-0 opacity-70","aria-hidden":"true"})},get children(){var p=P();return m(()=>f(p,"src",s().favicon)),p}}),l),a(l,j),a(o,$),a(e,n(g,{get when(){return s().description},get children(){var p=Q();return a(p,()=>s().description),p}}),null),e})()]}}),null),m(e=>{var t=u(),l=`Open ${$()} on ${k()}`,o=z("group block overflow-hidden rounded-xl border border-border bg-card text-card-foreground no-underline outline-none transition-shadow","cursor-pointer hover:shadow-md focus-visible:ring-2 focus-visible:ring-ring",i.class);return t!==e.e&&f(r,"href",e.e=t),l!==e.t&&f(r,"aria-label",e.t=l),o!==e.a&&S(r,e.a=o),e},{e:void 0,t:void 0,a:void 0}),r}})}F(["click","keydown"]);export{Y as L};
@@ -1 +0,0 @@
1
- import{i as o,a as c,d as u,b as m,c as a,S as d,j as w,t as l,z as x,A as b}from"./define-B_tXMYRm.js";import{c as f}from"./cn-CU7UAWzk.js";import{H as y,a as z,b as S}from"./hover-card-6KtXpLwT.js";var C=l('<img alt=favicon width=14 height=14 class="size-3.5 rounded-full">'),_=l('<a target=_blank rel="noopener noreferrer"><span class="truncate tabular-nums text-center font-normal">'),k=l('<div class="flex items-center gap-1.5"><img alt=favicon class="size-4 rounded-full"width=16 height=16><div class="text-primary truncate text-sm">'),R=l('<div class="line-clamp-2 text-sm font-medium">'),F=l('<div class="text-muted-foreground line-clamp-2 text-sm">'),I=l('<a target=_blank rel="noopener noreferrer"class="flex flex-col gap-2 p-3">'),U=l("<div>");const v=x();function p(){const r=b(v);if(!r)throw new Error("Source.* must be used inside <Source>");return r}function $(r){const n=()=>r.href??"",t=()=>{const e=n();if(!e)return"";try{return new URL(e).hostname}catch{return e.split("/").pop()||e}};return a(v.Provider,{value:{get href(){return n()},get domain(){return t()}},get children(){return a(y,{openDelay:150,get children(){return r.children}})}})}function j(r){const n=p(),t=()=>r.label??n.domain.replace("www.","");return a(z,{get children(){var e=_(),s=e.firstChild;return o(e,a(d,{get when(){return w(()=>!!r.showFavicon)()&&n.href},get children(){var i=C();return c(()=>u(i,"src",`https://www.google.com/s2/favicons?sz=64&domain_url=${encodeURIComponent(n.href)}`)),i}}),s),o(s,t),c(i=>{var h=n.href||void 0,g=f("bg-muted text-foreground/80 hover:bg-muted-foreground/30 hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background inline-flex h-5 max-w-32 items-center gap-1 overflow-hidden rounded-full py-0 text-xs no-underline transition-colors duration-150",r.showFavicon?"pr-2 pl-1":"px-2",r.class);return h!==i.e&&u(e,"href",i.e=h),g!==i.t&&m(e,i.t=g),i},{e:void 0,t:void 0}),e}})}function H(r){const n=p();return a(S,{get class(){return f("w-80 p-0 shadow-xs",r.class)},get children(){var t=I();return o(t,a(d,{get when(){return n.href},get children(){var e=k(),s=e.firstChild,i=s.nextSibling;return o(i,()=>n.domain.replace("www.","")),c(()=>u(s,"src",`https://www.google.com/s2/favicons?sz=64&domain_url=${encodeURIComponent(n.href)}`)),e}}),null),o(t,a(d,{get when(){return r.title},get children(){var e=R();return o(e,()=>r.title),e}}),null),o(t,a(d,{get when(){return r.description},get children(){var e=F();return o(e,()=>r.description),e}}),null),c(()=>u(t,"href",n.href||void 0)),t}})}function L(r){return(()=>{var n=U();return o(n,()=>r.children),c(t=>{var e=r.part,s=f("flex flex-wrap gap-1.5 mt-3",r.class);return e!==t.e&&u(n,"part",t.e=e),s!==t.t&&m(n,t.t=s),t},{e:void 0,t:void 0}),n})()}export{L as S,$ as a,j as b,H as c};