@ai-matrx/rich-content 0.2.9 → 0.2.15

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 (124) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/copy/copy-commands.d.ts +42 -0
  3. package/dist/copy/copy-commands.js +119 -0
  4. package/dist/copy/markdown-copy-html.d.ts +3 -0
  5. package/dist/copy/markdown-copy-html.js +8 -0
  6. package/dist/copy/markdown-readable-text.d.ts +4 -0
  7. package/dist/copy/markdown-readable-text.js +62 -0
  8. package/dist/display/blocks/json/json-tabular-utils.d.ts +7 -5
  9. package/dist/display/blocks/json/json-tabular-utils.js +26 -17
  10. package/dist/display/blocks/table/StreamingTableRenderer.js +11 -16
  11. package/dist/display/chat-markdown/BasicMarkdownContent.js +8 -6
  12. package/dist/display/chat-markdown/ConfigurableMarkdownContent.js +8 -6
  13. package/dist/display/chat-markdown/block-registry/BlockComponentRegistry.js +2 -0
  14. package/dist/display/chat-markdown/block-registry/block-dispatch.js +2 -0
  15. package/dist/display/tables/MarkdownTable.js +11 -16
  16. package/dist/display/tables/TableWithSeparatedControls.js +11 -16
  17. package/dist/display/tables/table-csv.d.ts +7 -0
  18. package/dist/display/tables/table-csv.js +19 -0
  19. package/dist/host/app-bindings.d.ts +32 -0
  20. package/dist/host/app-bindings.js +40 -0
  21. package/dist/host/feedback-subject.d.ts +16 -0
  22. package/dist/host/feedback-subject.js +0 -0
  23. package/dist/host/manual-copy.d.ts +7 -0
  24. package/dist/host/manual-copy.js +11 -0
  25. package/dist/host/registry.d.ts +2 -0
  26. package/dist/host/registry.js +10 -0
  27. package/dist/host/session-knob.d.ts +5 -0
  28. package/dist/host/session-knob.js +7 -0
  29. package/dist/host/types.d.ts +24 -0
  30. package/dist/rich-document/RegistryContextMenu.d.ts +29 -0
  31. package/dist/rich-document/RegistryContextMenu.js +40 -0
  32. package/dist/rich-document/RichDocument.d.ts +78 -0
  33. package/dist/rich-document/RichDocument.js +218 -0
  34. package/dist/rich-document/RichDocumentActionProvider.d.ts +18 -0
  35. package/dist/rich-document/RichDocumentActionProvider.js +18 -0
  36. package/dist/rich-document/RichDocumentActionSurface.d.ts +11 -0
  37. package/dist/rich-document/RichDocumentActionSurface.js +59 -0
  38. package/dist/rich-document/RichDocumentActions.d.ts +12 -0
  39. package/dist/rich-document/RichDocumentActions.js +29 -0
  40. package/dist/rich-document/actions/hostActions.d.ts +5 -0
  41. package/dist/rich-document/actions/hostActions.js +31 -0
  42. package/dist/rich-document/actions/labels.d.ts +9 -0
  43. package/dist/rich-document/actions/labels.js +8 -0
  44. package/dist/rich-document/actions/markdownTable.d.ts +9 -0
  45. package/dist/rich-document/actions/markdownTable.js +39 -0
  46. package/dist/rich-document/actions/provider.d.ts +58 -0
  47. package/dist/rich-document/actions/provider.js +245 -0
  48. package/dist/rich-document/actions/resumePendingAuthAction.d.ts +3 -0
  49. package/dist/rich-document/actions/resumePendingAuthAction.js +31 -0
  50. package/dist/rich-document/actions/sources/index.d.ts +9 -0
  51. package/dist/rich-document/actions/sources/index.js +22 -0
  52. package/dist/rich-document/actions/sources/raw.d.ts +4 -0
  53. package/dist/rich-document/actions/sources/raw.js +35 -0
  54. package/dist/rich-document/actions/useRichDocumentProvider.d.ts +1 -0
  55. package/dist/rich-document/actions/useRichDocumentProvider.js +13 -0
  56. package/dist/rich-document/host-types.d.ts +51 -0
  57. package/dist/rich-document/host-types.js +0 -0
  58. package/dist/rich-document/registrations.d.ts +1 -0
  59. package/dist/rich-document/registrations.js +13 -0
  60. package/dist/rich-document/review/applyTargets.d.ts +16 -0
  61. package/dist/rich-document/review/applyTargets.js +32 -0
  62. package/dist/rich-document/review/proposedEdit.d.ts +22 -0
  63. package/dist/rich-document/review/proposedEdit.js +48 -0
  64. package/dist/rich-document/review/protectedSpans.d.ts +14 -0
  65. package/dist/rich-document/review/protectedSpans.js +61 -0
  66. package/dist/rich-document/runtime/actionSurfacesStore.d.ts +30 -0
  67. package/dist/rich-document/runtime/actionSurfacesStore.js +79 -0
  68. package/dist/rich-document/runtime/contextMenuFor.d.ts +6 -0
  69. package/dist/rich-document/runtime/contextMenuFor.js +7 -0
  70. package/dist/rich-document/runtime/providerBridge.d.ts +20 -0
  71. package/dist/rich-document/runtime/providerBridge.js +23 -0
  72. package/dist/rich-document/runtime/useActionSurfaceProvider.d.ts +33 -0
  73. package/dist/rich-document/runtime/useActionSurfaceProvider.js +214 -0
  74. package/dist/rich-document/search/ContentFindControl.d.ts +8 -0
  75. package/dist/rich-document/search/ContentFindControl.js +46 -0
  76. package/dist/rich-document/search/RenderedFindBar.d.ts +8 -0
  77. package/dist/rich-document/search/RenderedFindBar.js +110 -0
  78. package/dist/rich-document/search/renderedFind.d.ts +1 -0
  79. package/dist/rich-document/search/renderedFind.js +1 -0
  80. package/dist/rich-document/types.d.ts +384 -0
  81. package/dist/rich-document/types.js +0 -0
  82. package/dist/rich-document/variants/ActionBar.d.ts +17 -0
  83. package/dist/rich-document/variants/ActionBar.js +48 -0
  84. package/dist/rich-document/variants/MenuVariant.d.ts +15 -0
  85. package/dist/rich-document/variants/MenuVariant.js +17 -0
  86. package/dist/rich-document/variants/MiniActionBar.d.ts +5 -0
  87. package/dist/rich-document/variants/MiniActionBar.js +11 -0
  88. package/dist/rich-document/variants/RegistryActionList.d.ts +17 -0
  89. package/dist/rich-document/variants/RegistryActionList.js +94 -0
  90. package/dist/rich-document/variants/RegistryActionMenu.d.ts +24 -0
  91. package/dist/rich-document/variants/RegistryActionMenu.js +74 -0
  92. package/dist/rich-document/variants/shared/AlchemyDocumentMenu.d.ts +7 -0
  93. package/dist/rich-document/variants/shared/AlchemyDocumentMenu.js +83 -0
  94. package/dist/rich-document/variants/shared/OpenOneMenuButton.d.ts +9 -0
  95. package/dist/rich-document/variants/shared/OpenOneMenuButton.js +70 -0
  96. package/dist/rich-document/variants/shared/StandaloneOneMenu.d.ts +8 -0
  97. package/dist/rich-document/variants/shared/StandaloneOneMenu.js +32 -0
  98. package/dist/rich-document/variants/shared/categories.d.ts +4 -0
  99. package/dist/rich-document/variants/shared/categories.js +31 -0
  100. package/dist/rich-document/variants/shared/iconMap.d.ts +2 -0
  101. package/dist/rich-document/variants/shared/iconMap.js +73 -0
  102. package/dist/rich-document/variants/shared/menuStructure.d.ts +81 -0
  103. package/dist/rich-document/variants/shared/menuStructure.js +268 -0
  104. package/dist/rich-document/variants/shared/runAction.d.ts +17 -0
  105. package/dist/rich-document/variants/shared/runAction.js +24 -0
  106. package/dist/selection-toolbar/SelectionToolbarFrame.d.ts +64 -0
  107. package/dist/selection-toolbar/SelectionToolbarFrame.js +191 -0
  108. package/dist/selection-toolbar/common-actions.d.ts +2 -0
  109. package/dist/selection-toolbar/common-actions.js +103 -0
  110. package/dist/selection-toolbar/selection-actions.d.ts +126 -0
  111. package/dist/selection-toolbar/selection-actions.js +240 -0
  112. package/dist/selection-toolbar/selection-copy.d.ts +19 -0
  113. package/dist/selection-toolbar/selection-copy.js +98 -0
  114. package/dist/selection-toolbar/selection-shape.d.ts +1 -0
  115. package/dist/selection-toolbar/selection-shape.js +15 -0
  116. package/dist/selection-toolbar/selection-zones.d.ts +90 -0
  117. package/dist/selection-toolbar/selection-zones.js +85 -0
  118. package/dist/selection-toolbar/textarea-caret.d.ts +8 -0
  119. package/dist/selection-toolbar/textarea-caret.js +69 -0
  120. package/dist/utils/alchemy-icon-keys.d.ts +18 -0
  121. package/dist/utils/alchemy-icon-keys.js +29 -0
  122. package/dist/utils/rendered-find.d.ts +9 -0
  123. package/dist/utils/rendered-find.js +68 -0
  124. package/package.json +21 -5
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.10
4
+
5
+ - Markdown copy goes through the host's `app.CopyButtons` (the copy menu; matrx-frontend ALC-19).
6
+ - Table CSV/TSV and workbook export through Alchemy (`toDelimitedText`, `buildFile`); `display/tables/table-csv`.
7
+
8
+ ### Consumer action
9
+
10
+ - Register `app.CopyButtons` (matrx-frontend: `components/agent-copy/CopyButtons`).
11
+
3
12
  ## 0.2.9
4
13
 
5
14
  AP-2 (2026-10-07) — the kit and Alchemy doors are the only doors in this package (aidream guard `apps/shared/scripts/check-shared-doors.mjs`). New dependency `@ai-matrx/alchemy`; `html2canvas` dependency removed. Every table/code/SVG/JSON/mermaid/diagram download saves through `@ai-matrx/kit/download` (`downloadFile` / `downloadUrl`) — the tables' "Download Again" toast action now really downloads again (it clicked a revoked URL before). Link/URL/mermaid copies go through `@ai-matrx/kit/clipboard` `copyText` (copied state from its boolean; `copyMermaidSource`/`copyMermaidSvg` throw when refused, as before); ImageBlock's image copy uses `useClipboard().copyImage`. The diagram PNG export captures through `@ai-matrx/alchemy/operate/capture` `captureElement` with `safeColors` (the local getComputedStyle patch is gone). No consumer action.
@@ -0,0 +1,42 @@
1
+ import { markdownToReadableText } from "./markdown-readable-text.js";
2
+ interface CopyOptions {
3
+ isMarkdown?: boolean;
4
+ formatForGoogleDocs?: boolean;
5
+ formatForWordPress?: boolean;
6
+ formatJson?: boolean;
7
+ showHtmlPreview?: boolean;
8
+ includeThinking?: boolean;
9
+ onSuccess?: () => void;
10
+ onError?: (err: unknown) => void;
11
+ onShowHtmlPreview?: (html: string) => boolean | void | Promise<boolean | void>;
12
+ }
13
+ /**
14
+ * Copy CONTENT (markdown, text, or any JSON-able value) through Alchemy's serialization and browser transport.
15
+ * Not the plain-string copy — that is `copyToClipboard` in `@/lib/clipboard/copy`.
16
+ */
17
+ export declare function copyContent(content: unknown, options?: CopyOptions): Promise<boolean>;
18
+ export type CopyFlavor = "default" | "markdown" | "text";
19
+ export { markdownToReadableText };
20
+ /** The knob that decides what a single Copy click puts in a plain field. */
21
+ export declare const COPY_DEFAULT_FLAVOR_KNOB: {
22
+ readonly feature: "copy";
23
+ readonly key: "default_flavor";
24
+ };
25
+ /** The plain-text flavor of a single Copy click for this session ("markdown" until the knob answers). */
26
+ export declare function defaultCopyFlavor(): "markdown" | "text";
27
+ export interface RichCopyOptions {
28
+ /** Keep `<thinking>` / reasoning blocks (default: removed). */
29
+ includeThinking?: boolean;
30
+ /** Toast on success: a label, or false for none. Default: the flavor's label. */
31
+ toast?: string | false;
32
+ }
33
+ /** The bytes a flavor writes — pure enough to test; the HTML is loaded at click time. */
34
+ export declare function richCopyPlainText(markdown: string, flavor: CopyFlavor, defaultFlavor?: "markdown" | "text"): string;
35
+ /**
36
+ * Write the plain flavor, or html + plain in one clipboard item (kit's `copyRich`; `html` may be a
37
+ * promise so the item is created inside the click). A refused rich write degrades to plain text and
38
+ * says so; when nothing lands the person is handed the text to copy by hand.
39
+ */
40
+ export declare function writeClipboardFlavors(plain: string, html?: Promise<string> | string): Promise<boolean>;
41
+ /** THE copy of rich content. Returns true when the clipboard holds it. */
42
+ export declare function copyRichContent(markdown: string, flavor?: CopyFlavor, options?: RichCopyOptions): Promise<boolean>;
@@ -0,0 +1,119 @@
1
+ import {
2
+ createBrowserTransport,
3
+ createDraft,
4
+ capture,
5
+ formatAdapter,
6
+ normalizeTransferJson,
7
+ serialize
8
+ } from "@ai-matrx/alchemy/operate";
9
+ import { copyRich, copyText } from "@ai-matrx/kit/clipboard";
10
+ import { showManualCopy } from "../host/manual-copy.js";
11
+ import { toast } from "../host/toast.js";
12
+ import { getSessionKnob } from "../host/session-knob.js";
13
+ import { markdownToReadableText } from "./markdown-readable-text.js";
14
+ import { formattedCopyHtml, removeThinkingContent } from "./markdown-copy-html.js";
15
+ async function copyContent(content, options = {}) {
16
+ let capturedText;
17
+ try {
18
+ let payload;
19
+ if (typeof content === "string") {
20
+ const text = options.includeThinking ? content : removeThinkingContent(content);
21
+ payload = { kind: options.isMarkdown ? "markdown" : "text", text };
22
+ if (options.formatJson !== false) {
23
+ let parsed;
24
+ try {
25
+ parsed = JSON.parse(text);
26
+ } catch {
27
+ parsed = void 0;
28
+ }
29
+ if (parsed !== void 0)
30
+ payload = { kind: "json", value: normalizeTransferJson(parsed) };
31
+ }
32
+ } else {
33
+ payload = { kind: "json", value: normalizeTransferJson(content) };
34
+ }
35
+ const signal = new AbortController().signal;
36
+ const { snapshot } = await capture(payload, signal);
37
+ const draft = createDraft(snapshot);
38
+ const rich = payload.kind === "markdown" && (options.formatForGoogleDocs || options.formatForWordPress);
39
+ const artifact = rich ? await (await formatAdapter("rich-html").load()).build(
40
+ draft,
41
+ { profile: options.formatForGoogleDocs ? "google-docs" : "generic" },
42
+ signal
43
+ ) : serialize(
44
+ draft,
45
+ payload.kind === "json" ? options.formatJson === false ? "compact-json" : "json" : "plain"
46
+ );
47
+ capturedText = artifact.plainText;
48
+ if (options.showHtmlPreview) {
49
+ if (!options.onShowHtmlPreview || !artifact.html)
50
+ throw new Error(
51
+ "HTML preview requires a formatted Markdown source and a preview callback."
52
+ );
53
+ const previewDelivered = await options.onShowHtmlPreview(artifact.html);
54
+ if (previewDelivered === false) return false;
55
+ options.onSuccess?.();
56
+ return true;
57
+ }
58
+ const outcome = await createBrowserTransport().copy(artifact, signal);
59
+ if (outcome.status === "success") {
60
+ options.onSuccess?.();
61
+ return true;
62
+ }
63
+ if (outcome.status === "degraded") {
64
+ toast.info(`Copied as plain text: ${outcome.reason}`);
65
+ return false;
66
+ }
67
+ if (outcome.status === "cancelled") return false;
68
+ throw new Error(outcome.message);
69
+ } catch (error) {
70
+ if (capturedText !== void 0) {
71
+ showManualCopy({ text: capturedText });
72
+ return false;
73
+ }
74
+ options.onError?.(error);
75
+ if (!options.onError)
76
+ toast.error(
77
+ error instanceof Error ? error.message : "Could not copy content"
78
+ );
79
+ return false;
80
+ }
81
+ }
82
+ const COPY_DEFAULT_FLAVOR_KNOB = { feature: "copy", key: "default_flavor" };
83
+ function defaultCopyFlavor() {
84
+ return getSessionKnob(COPY_DEFAULT_FLAVOR_KNOB) === "text" ? "text" : "markdown";
85
+ }
86
+ const FLAVOR_TOAST = {
87
+ default: "Copied",
88
+ markdown: "Markdown copied",
89
+ text: "Text copied"
90
+ };
91
+ function richCopyPlainText(markdown, flavor, defaultFlavor = "markdown") {
92
+ const plainFlavor = flavor === "default" ? defaultFlavor : flavor;
93
+ return plainFlavor === "text" ? markdownToReadableText(markdown) : markdown;
94
+ }
95
+ async function writeClipboardFlavors(plain, html) {
96
+ const landed = html === void 0 ? await copyText(plain) : await copyRich(
97
+ { text: plain, html },
98
+ { onDegraded: (reason) => toast.info(`Copied without formatting: ${reason}.`) }
99
+ );
100
+ if (!landed) showManualCopy({ text: plain });
101
+ return landed;
102
+ }
103
+ async function copyRichContent(markdown, flavor = "default", options = {}) {
104
+ const source = options.includeThinking ? markdown : removeThinkingContent(markdown);
105
+ const plain = richCopyPlainText(source, flavor, defaultCopyFlavor());
106
+ const ok = await writeClipboardFlavors(plain, flavor === "default" ? formattedCopyHtml(source) : void 0);
107
+ const label = options.toast === void 0 ? FLAVOR_TOAST[flavor] : options.toast;
108
+ if (ok && label) toast.success(label);
109
+ return ok;
110
+ }
111
+ export {
112
+ COPY_DEFAULT_FLAVOR_KNOB,
113
+ copyContent,
114
+ copyRichContent,
115
+ defaultCopyFlavor,
116
+ markdownToReadableText,
117
+ richCopyPlainText,
118
+ writeClipboardFlavors
119
+ };
@@ -0,0 +1,3 @@
1
+ export { removeThinkingContent } from "@ai-matrx/print/markdown";
2
+ /** The formatted HTML for a copy (loaded at click time), or `undefined` when this host writes plain text only. */
3
+ export declare function formattedCopyHtml(markdown: string): Promise<string> | undefined;
@@ -0,0 +1,8 @@
1
+ import { removeThinkingContent } from "@ai-matrx/print/markdown";
2
+ function formattedCopyHtml(markdown) {
3
+ return import("@ai-matrx/print/markdown").then(({ markdownToHtml }) => markdownToHtml(markdown));
4
+ }
5
+ export {
6
+ formattedCopyHtml,
7
+ removeThinkingContent
8
+ };
@@ -0,0 +1,4 @@
1
+ /** Inline markup → its words. Code spans are kept as their text. */
2
+ export declare function readableInline(line: string): string;
3
+ /** Markdown → readable plain text (envelopes unwrapped first). */
4
+ export declare function markdownToReadableText(markdown: string): string;
@@ -0,0 +1,62 @@
1
+ import { unwrapKindEnvelopes } from "@ai-matrx/content-ir/source";
2
+ import {
3
+ findTableEnd,
4
+ rowCells,
5
+ tableStartsAt,
6
+ unescapeCellPipes
7
+ } from "../display/markdown-classification/processors/utils/gfm-table-lines.js";
8
+ const FENCE = /^( {0,3})(`{3,}|~{3,})/;
9
+ function readableInline(line) {
10
+ const codes = [];
11
+ let out = line.replace(/(`+)([\s\S]*?[^`])\1(?!`)/g, (_m, _ticks, body) => {
12
+ codes.push(body.replace(/^ (.*) $/, "$1"));
13
+ return `\uE000${codes.length - 1}\uE000`;
14
+ });
15
+ out = out.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, (_m, alt, url) => alt ? `${alt} (${url})` : url).replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, (_m, text, url) => {
16
+ const bare = url.replace(/^mailto:/, "");
17
+ return text === url || text === bare ? bare : `${text} (${bare})`;
18
+ }).replace(/<((?:https?:\/\/|mailto:)[^>\s]+)>/g, (_m, url) => url.replace(/^mailto:/, "")).replace(/(\*\*\*|___)(?=\S)([\s\S]*?\S)\1/g, "$2").replace(/(\*\*|__)(?=\S)([\s\S]*?\S)\1/g, "$2").replace(/~~(?=\S)([\s\S]*?\S)~~/g, "$1").replace(/(^|[^\w*])\*(?=\S)([^*]*?\S)\*(?!\w)/g, "$1$2").replace(/(^|[^\w])_(?=\S)([^_]*?\S)_(?!\w)/g, "$1$2").replace(/ {2,}$|\\$/g, "").replace(/\\([!-/:-@[-`{-~])/g, "$1");
19
+ return out.replace(/\uE000(\d+)\uE000/g, (_m, i) => codes[Number(i)] ?? "");
20
+ }
21
+ function markdownToReadableText(markdown) {
22
+ const lines = unwrapKindEnvelopes(markdown).replace(/\r\n?/g, "\n").split("\n");
23
+ const out = [];
24
+ for (let i = 0; i < lines.length; i += 1) {
25
+ const line = lines[i];
26
+ const fence = FENCE.exec(line);
27
+ if (fence) {
28
+ const marker = fence[2];
29
+ let j = i + 1;
30
+ while (j < lines.length && !new RegExp(`^ {0,3}${marker[0] === "`" ? "`" : "~"}{${marker.length},}\\s*$`).test(lines[j])) {
31
+ out.push(lines[j]);
32
+ j += 1;
33
+ }
34
+ i = j;
35
+ continue;
36
+ }
37
+ if (tableStartsAt(lines, i)) {
38
+ const end = findTableEnd(lines, i);
39
+ for (let j = i; j < end; j += 1) {
40
+ if (j === i + 1) continue;
41
+ out.push(rowCells(lines[j]).map((c) => readableInline(unescapeCellPipes(c).trim())).join(" "));
42
+ }
43
+ i = end - 1;
44
+ continue;
45
+ }
46
+ if (/^ {0,3}([-*_])( *\1){2,} *$/.test(line)) {
47
+ out.push("");
48
+ continue;
49
+ }
50
+ if (/^ {0,3}(=+|-+) *$/.test(line) && out.length && out[out.length - 1].trim()) continue;
51
+ let rest = line;
52
+ rest = rest.replace(/^ {0,3}(?:> ?)+/, "");
53
+ rest = rest.replace(/^ {0,3}#{1,6}[ \t]+/, "").replace(/[ \t]+#+[ \t]*$/, "");
54
+ rest = rest.replace(/^(\s*)[-*+][ \t]+\[[ ]\][ \t]+/, "$1\u2610 ").replace(/^(\s*)[-*+][ \t]+\[[xX]\][ \t]+/, "$1\u2611 ").replace(/^(\s*)[-*+][ \t]+/, "$1\u2022 ").replace(/^(\s*)(\d{1,9})[.)][ \t]+/, "$1$2. ");
55
+ out.push(readableInline(rest));
56
+ }
57
+ return out.join("\n").replace(/\n{3,}/g, "\n\n").trim();
58
+ }
59
+ export {
60
+ markdownToReadableText,
61
+ readableInline
62
+ };
@@ -49,16 +49,18 @@ export declare function detectTabular(value: unknown): TabularShape;
49
49
  * `null` and `undefined` become empty strings (Excel-friendly).
50
50
  */
51
51
  export declare function cellToCsv(value: unknown): string;
52
+ /**
53
+ * Rows -> CSV/TSV text through Alchemy's one writer (quoting, line breaks, `'` before a leading
54
+ * = + - @ text; real numbers stay numbers, so -5 is never altered).
55
+ */
52
56
  export declare function rowsToCsv(rows: Record<string, unknown>[], columns: string[], delimiter?: string): string;
53
57
  export declare function rowsToNdjson(rows: unknown[]): string;
54
58
  /** Browser file-download for a text payload. */
55
59
  export declare function downloadText(filename: string, content: string, mime?: string): void;
56
60
  /**
57
- * XLSX export uses the same `xlsx` package as `ImportTableModal`.
58
- * Dynamically imported to keep it out of the initial chat bundle.
59
- *
60
- * Cell values are pre-stringified for objects/arrays so spreadsheets
61
- * receive plain text instead of "[object Object]".
61
+ * XLSX export through Alchemy's workbook format (the one spreadsheet engine).
62
+ * Header row = `columns`; object/array cells are pre-stringified so
63
+ * spreadsheets receive plain text instead of "[object Object]".
62
64
  */
63
65
  export declare function rowsToXlsx(rows: Record<string, unknown>[], columns: string[], filename: string): Promise<void>;
64
66
  /** Default filename helper (no extension). */
@@ -1,5 +1,6 @@
1
+ import { buildFile } from "@ai-matrx/alchemy/operate";
2
+ import { toDelimitedText } from "@ai-matrx/alchemy/operate/read";
1
3
  import { downloadFile } from "@ai-matrx/kit/download";
2
- import { announceBareHost, hostCapability } from "../../../host/registry.js";
3
4
  function parseJsonSafe(text) {
4
5
  if (!text || !text.trim()) {
5
6
  return { ok: false, error: "Empty input" };
@@ -73,30 +74,38 @@ function cellToCsv(value) {
73
74
  }
74
75
  return String(value);
75
76
  }
76
- function escapeCsv(value, delimiter) {
77
- const needsQuote = value.includes(delimiter) || value.includes('"') || value.includes("\n") || value.includes("\r");
78
- if (!needsQuote) return value;
79
- return `"${value.replace(/"/g, '""')}"`;
80
- }
81
77
  function rowsToCsv(rows, columns, delimiter = ",") {
82
- const header = columns.map((c) => escapeCsv(c, delimiter)).join(delimiter);
83
- const body = rows.map(
84
- (row) => columns.map((c) => escapeCsv(cellToCsv(row[c]), delimiter)).join(delimiter)
85
- ).join("\n");
86
- return body ? `${header}
87
- ${body}` : header;
78
+ return toDelimitedText(
79
+ columns,
80
+ rows.map((row) => columns.map((c) => cellToCsvValue(row[c]))),
81
+ { format: delimiter === " " ? "tsv" : "csv", spreadsheetSafe: true }
82
+ );
83
+ }
84
+ function cellToCsvValue(value) {
85
+ if (value === null || value === void 0) return "";
86
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
87
+ return cellToCsv(value);
88
88
  }
89
89
  function rowsToNdjson(rows) {
90
90
  return rows.map((r) => JSON.stringify(r)).join("\n");
91
91
  }
92
92
  function downloadText(filename, content, mime = "text/plain") {
93
- downloadFile(filename, content, `${mime};charset=utf-8`);
93
+ const blob = new Blob([content], { type: `${mime};charset=utf-8` });
94
+ downloadFile(filename, blob, blob.type);
94
95
  }
95
96
  async function rowsToXlsx(rows, columns, filename) {
96
- const impl = hostCapability("app")?.rowsToXlsx;
97
- if (impl) return impl(rows, columns, filename);
98
- announceBareHost("app.rowsToXlsx", "spreadsheets download as CSV");
99
- downloadText(rowsToCsv(rows, columns), filename.replace(/\.xlsx$/i, "") + ".csv", "text/csv");
97
+ const cell = (v) => v === null || v === void 0 ? null : typeof v === "object" ? JSON.stringify(v) : typeof v === "string" || typeof v === "number" || typeof v === "boolean" ? v : String(v);
98
+ const built = await buildFile(
99
+ {
100
+ kind: "registered",
101
+ format: "workbook",
102
+ value: { sheets: [{ name: "Data", rows: [columns, ...rows.map((row) => columns.map((c) => cell(row[c])))] }] }
103
+ },
104
+ "xlsx-workbook",
105
+ { filename }
106
+ );
107
+ const blob = built.blob();
108
+ downloadFile(built.filename, blob, blob.type);
100
109
  }
101
110
  function defaultJsonFilename() {
102
111
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
@@ -1,6 +1,5 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { downloadFile } from "@ai-matrx/kit/download";
4
3
  import { useClipboard } from "@ai-matrx/kit/clipboard";
5
4
  import { toast as copyToast } from "../../../host/toast.js";
6
5
  import { rewriteTableSource } from "../../../utils/table-source.js";
@@ -77,6 +76,8 @@ import {
77
76
  parseMarkdownTable,
78
77
  cleanTableHeaderKey
79
78
  } from "./parseMarkdownTable.js";
79
+ import { downloadFile } from "@ai-matrx/kit/download";
80
+ import { tableToCsv } from "../../tables/table-csv.js";
80
81
  const NO_HIDDEN_COLUMNS = /* @__PURE__ */ new Set();
81
82
  const ExportDropdownMenu = ({
82
83
  tableData,
@@ -292,21 +293,13 @@ const StreamingTableRendererCore = ({
292
293
  };
293
294
  const downloadCSV = () => {
294
295
  try {
295
- const csvContent = [
296
- headers.map((h) => h.replace(/"/g, '""')).join(","),
297
- ...rows.map(
298
- (row) => row.map((cell) => {
299
- const escaped = cell.replace(/"/g, '""');
300
- return cell.includes(",") ? `"${escaped}"` : escaped;
301
- }).join(",")
302
- )
303
- ].join("\n");
304
- const saveCsv = () => downloadFile("table_data.csv", csvContent, "text/csv;charset=utf-8");
305
- saveCsv();
296
+ const csvContent = tableToCsv(headers, rows);
297
+ const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
298
+ downloadFile("table_data.csv", blob, blob.type);
306
299
  toast.success("Table exported to CSV", {
307
300
  action: {
308
301
  label: "Download Again",
309
- onClick: saveCsv,
302
+ onClick: () => downloadFile("table_data.csv", blob, blob.type),
310
303
  className: "font-medium"
311
304
  }
312
305
  });
@@ -320,12 +313,14 @@ const StreamingTableRendererCore = ({
320
313
  try {
321
314
  const markdownContent = content || "";
322
315
  const fileName = headers && headers[0] ? `${headers[0].replace(/[^a-z0-9]/gi, "_").toLowerCase()}.md` : "table_data.md";
323
- const saveMarkdown = () => downloadFile(fileName, markdownContent, "text/markdown;charset=utf-8");
324
- saveMarkdown();
316
+ const blob = new Blob([markdownContent], {
317
+ type: "text/markdown;charset=utf-8"
318
+ });
319
+ downloadFile(fileName, blob, blob.type);
325
320
  toast.success("Markdown file downloaded", {
326
321
  action: {
327
322
  label: "Download Again",
328
- onClick: saveMarkdown,
323
+ onClick: () => downloadFile(fileName, blob, blob.type),
329
324
  className: "font-medium"
330
325
  }
331
326
  });
@@ -7,7 +7,7 @@ import {
7
7
  reportDelimiterViolations
8
8
  } from "@ai-matrx/kit/delimiter-guard";
9
9
  import { captureError } from "../../host/diagnostics.js";
10
- import { InlineCopyButton } from "../../host/app-bindings.js";
10
+ import { CopyButtons } from "../../host/app-bindings.js";
11
11
  import {
12
12
  TableRenderPathDiagnostic
13
13
  } from "../blocks/table/TableRenderPathDiagnostic.js";
@@ -102,12 +102,14 @@ const BasicMarkdownContentBody = ({
102
102
  /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: PROSE_FRAME_CSS } }),
103
103
  /* @__PURE__ */ jsx(MarkdownCore, { preset: "chat", components, children: processedContent }),
104
104
  !isStreamActive && isHovering && /* @__PURE__ */ jsx("div", { className: "absolute top-0 right-0 p-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200", children: showCopyButton && /* @__PURE__ */ jsx(
105
- InlineCopyButton,
105
+ CopyButtons,
106
106
  {
107
- markdownContent: content,
108
- position: "top-right",
109
- className: "mt-1 mr-1",
110
- isMarkdown: true
107
+ label: "Content",
108
+ human: content,
109
+ contentFlavor: "markdown",
110
+ stripThinking: true,
111
+ size: "sm",
112
+ className: "absolute top-1 right-1 z-10 mt-1 mr-1"
111
113
  }
112
114
  ) })
113
115
  ]
@@ -12,7 +12,7 @@ import {
12
12
  } from "@ai-matrx/kit/delimiter-guard";
13
13
  import { captureError } from "../../host/diagnostics.js";
14
14
  import { LinkComponent } from "../blocks/links/LinkComponent.js";
15
- import { InlineCopyButton } from "../../host/app-bindings.js";
15
+ import { CopyButtons } from "../../host/app-bindings.js";
16
16
  import {
17
17
  useMediaBlob,
18
18
  useMediaLoadRecovery
@@ -614,12 +614,14 @@ const ConfigurableMarkdownContentBody = ({
614
614
  /* @__PURE__ */ jsx(MarkdownCore, { preset: "rich", components, children: processedContent }),
615
615
  !isStreamActive && isHovering && /* @__PURE__ */ jsxs("div", { className: "absolute top-0 right-0 p-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200", children: [
616
616
  showCopyButton && /* @__PURE__ */ jsx(
617
- InlineCopyButton,
617
+ CopyButtons,
618
618
  {
619
- markdownContent: content,
620
- position: "top-right",
621
- className: "mt-1 mr-1",
622
- isMarkdown: true
619
+ label: "Content",
620
+ human: content,
621
+ contentFlavor: "markdown",
622
+ stripThinking: true,
623
+ size: "sm",
624
+ className: "absolute top-1 right-1 z-10 mt-1 mr-1"
623
625
  }
624
626
  ),
625
627
  onEditRequest && /* @__PURE__ */ jsx(
@@ -69,8 +69,10 @@ const BUILTIN_BLOCK_COMPONENTS = {
69
69
  const BUILTIN_LOADING_COMPONENTS = {
70
70
  DiagramLoading: () => /* @__PURE__ */ jsx(LazyBlockWrapper, { children: /* @__PURE__ */ jsx(DiagramLoadingVisualization, {}) })
71
71
  };
72
+ import { ensureHostBlockRegistrations } from "../../../host/registry.js";
72
73
  const REGISTRY_KEY = /* @__PURE__ */ Symbol.for("ai-matrx.rich-content.block-components");
73
74
  function registry() {
75
+ ensureHostBlockRegistrations();
74
76
  const g = globalThis;
75
77
  return g[REGISTRY_KEY] ??= { blocks: {}, loading: {} };
76
78
  }
@@ -472,6 +472,7 @@ const BLOCK_DISPATCH_CLASSIFICATION = {
472
472
  const DISPATCH_MAP = new Map(
473
473
  Object.entries(BLOCK_DISPATCH)
474
474
  );
475
+ import { ensureHostBlockRegistrations } from "../../../host/registry.js";
475
476
  const REGISTERED_KEY = /* @__PURE__ */ Symbol.for("ai-matrx.rich-content.block-dispatch");
476
477
  function registered() {
477
478
  const g = globalThis;
@@ -481,6 +482,7 @@ function registerBlockDispatch(entries) {
481
482
  for (const [type, fn] of Object.entries(entries)) if (fn) registered().set(type, fn);
482
483
  }
483
484
  function resolveBlockDispatch(type) {
485
+ ensureHostBlockRegistrations();
484
486
  return registered().get(type) ?? DISPATCH_MAP.get(type) ?? null;
485
487
  }
486
488
  export {
@@ -1,6 +1,5 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { downloadFile } from "@ai-matrx/kit/download";
4
3
  import { useClipboard } from "@ai-matrx/kit/clipboard";
5
4
  import { toast as copyToast } from "../../host/toast.js";
6
5
  import { rewriteTableSource } from "../../utils/table-source.js";
@@ -61,6 +60,8 @@ import {
61
60
  clearColumn,
62
61
  duplicateColumn
63
62
  } from "./editing/tableMutations.js";
63
+ import { downloadFile } from "@ai-matrx/kit/download";
64
+ import { tableToCsv } from "./table-csv.js";
64
65
  const ExportDropdownMenu = ({
65
66
  tableData,
66
67
  content,
@@ -291,21 +292,13 @@ const MarkdownTable = ({
291
292
  };
292
293
  const downloadCSV = () => {
293
294
  try {
294
- const csvContent = [
295
- internalTableData.headers.map((h) => h.replace(/"/g, '""')).join(","),
296
- ...internalTableData.rows.map(
297
- (row) => row.map((cell) => {
298
- const escaped = cell.replace(/"/g, '""');
299
- return cell.includes(",") ? `"${escaped}"` : escaped;
300
- }).join(",")
301
- )
302
- ].join("\n");
303
- const saveCsv = () => downloadFile("table_data.csv", csvContent, "text/csv;charset=utf-8");
304
- saveCsv();
295
+ const csvContent = tableToCsv(internalTableData.headers, internalTableData.rows);
296
+ const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
297
+ downloadFile("table_data.csv", blob, blob.type);
305
298
  toast.success("Table exported to CSV", {
306
299
  action: {
307
300
  label: "Download Again",
308
- onClick: saveCsv,
301
+ onClick: () => downloadFile("table_data.csv", blob, blob.type),
309
302
  className: "font-medium"
310
303
  }
311
304
  });
@@ -317,12 +310,14 @@ const MarkdownTable = ({
317
310
  try {
318
311
  const markdownContent = content || "";
319
312
  const fileName = internalTableData.headers && internalTableData.headers[0] ? `${internalTableData.headers[0].replace(/[^a-z0-9]/gi, "_").toLowerCase()}.md` : "table_data.md";
320
- const saveMarkdown = () => downloadFile(fileName, markdownContent, "text/markdown;charset=utf-8");
321
- saveMarkdown();
313
+ const blob = new Blob([markdownContent], {
314
+ type: "text/markdown;charset=utf-8"
315
+ });
316
+ downloadFile(fileName, blob, blob.type);
322
317
  toast.success("Markdown file downloaded", {
323
318
  action: {
324
319
  label: "Download Again",
325
- onClick: saveMarkdown,
320
+ onClick: () => downloadFile(fileName, blob, blob.type),
326
321
  className: "font-medium"
327
322
  }
328
323
  });
@@ -1,6 +1,5 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { downloadFile } from "@ai-matrx/kit/download";
4
3
  import { useClipboard } from "@ai-matrx/kit/clipboard";
5
4
  import { toast as copyToast } from "../../host/toast.js";
6
5
  import { rewriteTableSource } from "../../utils/table-source.js";
@@ -31,6 +30,8 @@ import {
31
30
  import { useToastManager } from "../../utils/lifted/useToastManager.js";
32
31
  import { THEMES } from "../themes.js";
33
32
  import { useOpenSaveToTable } from "../../host/tables.js";
33
+ import { downloadFile } from "@ai-matrx/kit/download";
34
+ import { tableToCsv } from "./table-csv.js";
34
35
  function useDebounce(value, delay) {
35
36
  const [debouncedValue, setDebouncedValue] = useState(value);
36
37
  useEffect(() => {
@@ -108,21 +109,13 @@ const TableControls = ({
108
109
  }, [content, copyTableToClipboard, toast]);
109
110
  const downloadCSV = useCallback(() => {
110
111
  try {
111
- const csvContent = [
112
- tableData.headers.map((h) => h.replace(/"/g, '""')).join(","),
113
- ...tableData.rows.map(
114
- (row) => row.map((cell) => {
115
- const escaped = cell.replace(/"/g, '""');
116
- return cell.includes(",") ? `"${escaped}"` : escaped;
117
- }).join(",")
118
- )
119
- ].join("\n");
120
- const saveCsv = () => downloadFile("table_data.csv", csvContent, "text/csv;charset=utf-8");
121
- saveCsv();
112
+ const csvContent = tableToCsv(tableData.headers, tableData.rows);
113
+ const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
114
+ downloadFile("table_data.csv", blob, blob.type);
122
115
  toast.success("Table exported to CSV", {
123
116
  action: {
124
117
  label: "Download Again",
125
- onClick: saveCsv,
118
+ onClick: () => downloadFile("table_data.csv", blob, blob.type),
126
119
  className: "font-medium"
127
120
  }
128
121
  });
@@ -134,12 +127,14 @@ const TableControls = ({
134
127
  try {
135
128
  const markdownContent = content || "";
136
129
  const fileName = tableData.headers && tableData.headers[0] ? `${tableData.headers[0].replace(/[^a-z0-9]/gi, "_").toLowerCase()}.md` : "table_data.md";
137
- const saveMarkdown = () => downloadFile(fileName, markdownContent, "text/markdown;charset=utf-8");
138
- saveMarkdown();
130
+ const blob = new Blob([markdownContent], {
131
+ type: "text/markdown;charset=utf-8"
132
+ });
133
+ downloadFile(fileName, blob, blob.type);
139
134
  toast.success("Markdown file downloaded", {
140
135
  action: {
141
136
  label: "Download Again",
142
- onClick: saveMarkdown,
137
+ onClick: () => downloadFile(fileName, blob, blob.type),
143
138
  className: "font-medium"
144
139
  }
145
140
  });
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The CSV text of a markdown/streaming table, written by Alchemy's one writer. Quotes, commas and
3
+ * line breaks are quoted; text that opens with = + - @ is prefixed `'` so a spreadsheet never runs
4
+ * it (alchemy `spreadsheetSafe`, on). A cell that is a plain number is written as a number, so a
5
+ * negative amount stays `-5`.
6
+ */
7
+ export declare function tableToCsv(headers: readonly string[], rows: readonly (readonly string[])[]): string;
@@ -0,0 +1,19 @@
1
+ import { toDelimitedText } from "@ai-matrx/alchemy/operate/read";
2
+ const PLAIN_NUMBER = /^-?\d+(\.\d+)?$/;
3
+ function cellValue(cell) {
4
+ if (PLAIN_NUMBER.test(cell)) {
5
+ const n = Number(cell);
6
+ if (String(n) === cell) return n;
7
+ }
8
+ return cell;
9
+ }
10
+ function tableToCsv(headers, rows) {
11
+ return toDelimitedText(
12
+ headers,
13
+ rows.map((row) => row.map(cellValue)),
14
+ { spreadsheetSafe: true }
15
+ );
16
+ }
17
+ export {
18
+ tableToCsv
19
+ };