@tangle-network/ui 11.7.0 → 11.8.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.
package/dist/files.js CHANGED
@@ -1,23 +1,29 @@
1
1
  import {
2
+ CSV_PREVIEW_ROW_LIMIT,
2
3
  FileArtifactPane,
3
4
  FilePreview,
4
5
  FileTabs,
5
6
  FileTree,
6
7
  RichFileTree,
7
- filterFileTree
8
- } from "./chunk-PLWRAS5Z.js";
8
+ filterFileTree,
9
+ parseCsv
10
+ } from "./chunk-XGOWUDDW.js";
11
+ import "./chunk-4CLN43XT.js";
9
12
  import {
10
13
  detectFileFormat,
11
14
  fileExtension,
12
15
  getCodeLanguage,
13
16
  getFormatLabel,
14
- getSyntaxLanguage
15
- } from "./chunk-ZRVH3WCA.js";
17
+ getSyntaxLanguage,
18
+ resolveFilePreviewKind
19
+ } from "./chunk-XU3HB5QL.js";
20
+ import "./chunk-MKTSMWVD.js";
16
21
  import "./chunk-LTEKI7YJ.js";
17
22
  import "./chunk-P5F43A65.js";
18
23
  import "./chunk-WUQDUBJG.js";
19
24
  import "./chunk-RQHJBTEU.js";
20
25
  export {
26
+ CSV_PREVIEW_ROW_LIMIT,
21
27
  FileArtifactPane,
22
28
  FilePreview,
23
29
  FileTabs,
@@ -28,5 +34,7 @@ export {
28
34
  filterFileTree,
29
35
  getCodeLanguage,
30
36
  getFormatLabel,
31
- getSyntaxLanguage
37
+ getSyntaxLanguage,
38
+ parseCsv,
39
+ resolveFilePreviewKind
32
40
  };
package/dist/hooks.js CHANGED
@@ -11,19 +11,20 @@ import {
11
11
  useSSEStream,
12
12
  useSdkSession,
13
13
  useToolCallStream
14
- } from "./chunk-4ENFLPWQ.js";
14
+ } from "./chunk-BWOUSEHM.js";
15
15
  import "./chunk-OEX7NZE3.js";
16
16
  import {
17
17
  useAutoScroll,
18
18
  useRunCollapseState,
19
19
  useRunGroups
20
- } from "./chunk-UYTRNR7Z.js";
21
- import "./chunk-6RIX7M27.js";
22
- import "./chunk-XT4JMYVB.js";
23
- import "./chunk-EIUE5GV2.js";
24
- import "./chunk-AAUNOHVL.js";
20
+ } from "./chunk-565EATZZ.js";
21
+ import "./chunk-Q7ZR6LSD.js";
22
+ import "./chunk-KNYSKNZX.js";
23
+ import "./chunk-ND3ZFXQK.js";
24
+ import "./chunk-NWECTSOH.js";
25
25
  import "./chunk-L57WRIND.js";
26
- import "./chunk-ZRVH3WCA.js";
26
+ import "./chunk-4CLN43XT.js";
27
+ import "./chunk-XU3HB5QL.js";
27
28
  import "./chunk-P5F43A65.js";
28
29
  import "./chunk-WUQDUBJG.js";
29
30
  import "./chunk-RQHJBTEU.js";
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@ export { AgentTimeline, AgentTimelineArtifactItem, AgentTimelineCustomItem, Agen
6
6
  export { ExpandedToolDetail, ExpandedToolDetailProps, InlineThinkingItem, InlineThinkingItemProps, InlineToolItem, InlineToolItemProps, LiveDuration, RunGroup, RunGroupProps, RunRowShell, RunRowShellProps, RunRowStatus, RunRowStatusDot } from './run.js';
7
7
  export { F as FeedSegment, T as ToolCallData, a as ToolCallFeed, b as ToolCallFeedProps, c as ToolCallStatus, d as ToolCallType, p as parseToolEvent } from './tool-call-feed-D9iofJgW.js';
8
8
  export { OpenUIAction, OpenUIActionsNode, OpenUIArtifactRenderer, OpenUIArtifactRendererProps, OpenUIBadgeNode, OpenUICardNode, OpenUICodeNode, OpenUIComponentNode, OpenUIGridNode, OpenUIHeadingNode, OpenUIKeyValueNode, OpenUIMarkdownNode, OpenUIPrimitive, OpenUISeparatorNode, OpenUIStackNode, OpenUIStatNode, OpenUITableNode, OpenUITextNode } from './openui.js';
9
- export { FileArtifactPane, FileArtifactPaneProps, FileFormat, FileNode, FilePreview, FilePreviewProps, FileTabData, FileTabs, FileTabsProps, FileTree, FileTreeProps, FileTreeVisibilityOptions, RichFileTree, RichFileTreeGitEntry, RichFileTreeGitStatus, RichFileTreeProps, RichFileTreeThemeVars, detectFileFormat, fileExtension, filterFileTree, getCodeLanguage, getFormatLabel, getSyntaxLanguage } from './files.js';
9
+ export { CSV_PREVIEW_ROW_LIMIT, FileArtifactPane, FileArtifactPaneProps, FileFormat, FileNode, FilePreview, FilePreviewKind, FilePreviewProps, FileTabData, FileTabs, FileTabsProps, FileTree, FileTreeProps, FileTreeVisibilityOptions, RichFileTree, RichFileTreeGitEntry, RichFileTreeGitStatus, RichFileTreeProps, RichFileTreeThemeVars, detectFileFormat, fileExtension, filterFileTree, getCodeLanguage, getFormatLabel, getSyntaxLanguage, parseCsv, resolveFilePreviewKind } from './files.js';
10
10
  export { Markdown, MarkdownProps } from './markdown.js';
11
11
  export { C as CodeBlock, a as CodeBlockProps, b as CopyButton } from './code-block-0kSpWMnf.js';
12
12
  export { AuthHeader, AuthHeaderProps, GitHubLoginButton, GitHubLoginButtonProps, LoginLayout, LoginLayoutProps, SessionUser, UserMenu, UserMenuProps } from './auth.js';
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  useSSEStream,
21
21
  useSdkSession,
22
22
  useToolCallStream
23
- } from "./chunk-4ENFLPWQ.js";
23
+ } from "./chunk-BWOUSEHM.js";
24
24
  import {
25
25
  addMessage,
26
26
  addParts,
@@ -146,39 +146,35 @@ import {
146
146
  MessageList,
147
147
  ThinkingIndicator,
148
148
  UserMessage
149
- } from "./chunk-VDCZHOOO.js";
149
+ } from "./chunk-X53RZSX6.js";
150
150
  import {
151
151
  useAutoScroll,
152
152
  useRunCollapseState,
153
153
  useRunGroups
154
- } from "./chunk-UYTRNR7Z.js";
154
+ } from "./chunk-565EATZZ.js";
155
155
  import {
156
156
  LiveDuration
157
- } from "./chunk-LFVF2HFB.js";
157
+ } from "./chunk-YTO5V6QS.js";
158
158
  import {
159
159
  ToolCallFeed,
160
160
  parseToolEvent
161
- } from "./chunk-6RIX7M27.js";
161
+ } from "./chunk-Q7ZR6LSD.js";
162
162
  import {
163
163
  InlineThinkingItem,
164
164
  RunGroup
165
- } from "./chunk-CVX53NCC.js";
165
+ } from "./chunk-OXZRCXJK.js";
166
166
  import {
167
167
  ExpandedToolDetail,
168
168
  InlineToolItem,
169
169
  RunRowShell,
170
170
  RunRowStatusDot
171
- } from "./chunk-XT4JMYVB.js";
171
+ } from "./chunk-KNYSKNZX.js";
172
172
  import {
173
173
  TOOL_CATEGORY_ICONS,
174
- formatBytes,
175
- formatDuration,
176
- formatUptime,
177
174
  getToolCategory,
178
175
  getToolDisplayMetadata,
179
- getToolErrorText,
180
- truncateText
181
- } from "./chunk-EIUE5GV2.js";
176
+ getToolErrorText
177
+ } from "./chunk-ND3ZFXQK.js";
182
178
  import {
183
179
  CommandPreview,
184
180
  DiffPreview,
@@ -187,7 +183,7 @@ import {
187
183
  QuestionPreview,
188
184
  WebSearchPreview,
189
185
  WriteFilePreview
190
- } from "./chunk-AAUNOHVL.js";
186
+ } from "./chunk-NWECTSOH.js";
191
187
  import {
192
188
  TextShimmer
193
189
  } from "./chunk-L57WRIND.js";
@@ -215,24 +211,33 @@ import {
215
211
  badgeVariants
216
212
  } from "./chunk-GYPQXTJU.js";
217
213
  import {
218
- Button,
219
- buttonVariants
220
- } from "./chunk-MKTSMWVD.js";
221
- import {
214
+ CSV_PREVIEW_ROW_LIMIT,
222
215
  FileArtifactPane,
223
216
  FilePreview,
224
217
  FileTabs,
225
218
  FileTree,
226
219
  RichFileTree,
227
- filterFileTree
228
- } from "./chunk-PLWRAS5Z.js";
220
+ filterFileTree,
221
+ parseCsv
222
+ } from "./chunk-XGOWUDDW.js";
223
+ import {
224
+ formatBytes,
225
+ formatDuration,
226
+ formatUptime,
227
+ truncateText
228
+ } from "./chunk-4CLN43XT.js";
229
229
  import {
230
230
  detectFileFormat,
231
231
  fileExtension,
232
232
  getCodeLanguage,
233
233
  getFormatLabel,
234
- getSyntaxLanguage
235
- } from "./chunk-ZRVH3WCA.js";
234
+ getSyntaxLanguage,
235
+ resolveFilePreviewKind
236
+ } from "./chunk-XU3HB5QL.js";
237
+ import {
238
+ Button,
239
+ buttonVariants
240
+ } from "./chunk-MKTSMWVD.js";
236
241
  import {
237
242
  Tabs,
238
243
  TabsContent,
@@ -262,6 +267,7 @@ export {
262
267
  AvatarImage,
263
268
  Badge,
264
269
  Button,
270
+ CSV_PREVIEW_ROW_LIMIT,
265
271
  Card,
266
272
  CardContent,
267
273
  CardDescription,
@@ -413,10 +419,12 @@ export {
413
419
  hasBackgroundRunningSessions,
414
420
  isStreamingAtom,
415
421
  messagesAtom,
422
+ parseCsv,
416
423
  parseToolEvent,
417
424
  partMapAtom,
418
425
  registerActiveSession,
419
426
  resetActiveSessions,
427
+ resolveFilePreviewKind,
420
428
  setActiveSessionAttention,
421
429
  setActiveSessionConnection,
422
430
  setActiveSessionError,
package/dist/run.js CHANGED
@@ -1,27 +1,28 @@
1
1
  import {
2
2
  LiveDuration
3
- } from "./chunk-LFVF2HFB.js";
3
+ } from "./chunk-YTO5V6QS.js";
4
4
  import {
5
5
  ToolCallFeed,
6
6
  parseToolEvent
7
- } from "./chunk-6RIX7M27.js";
7
+ } from "./chunk-Q7ZR6LSD.js";
8
8
  import {
9
9
  InlineThinkingItem,
10
10
  RunGroup
11
- } from "./chunk-CVX53NCC.js";
11
+ } from "./chunk-OXZRCXJK.js";
12
12
  import {
13
13
  ExpandedToolDetail,
14
14
  InlineToolItem,
15
15
  RunRowShell,
16
16
  RunRowStatusDot
17
- } from "./chunk-XT4JMYVB.js";
18
- import "./chunk-EIUE5GV2.js";
19
- import "./chunk-AAUNOHVL.js";
17
+ } from "./chunk-KNYSKNZX.js";
18
+ import "./chunk-ND3ZFXQK.js";
19
+ import "./chunk-NWECTSOH.js";
20
20
  import "./chunk-L57WRIND.js";
21
21
  import "./chunk-3M7BPSSW.js";
22
22
  import "./chunk-GYPQXTJU.js";
23
+ import "./chunk-4CLN43XT.js";
24
+ import "./chunk-XU3HB5QL.js";
23
25
  import "./chunk-MKTSMWVD.js";
24
- import "./chunk-ZRVH3WCA.js";
25
26
  import "./chunk-P5F43A65.js";
26
27
  import "./chunk-WUQDUBJG.js";
27
28
  import "./chunk-RQHJBTEU.js";
package/dist/sdk-hooks.js CHANGED
@@ -5,19 +5,20 @@ import {
5
5
  useSSEStream,
6
6
  useSdkSession,
7
7
  useToolCallStream
8
- } from "./chunk-4ENFLPWQ.js";
8
+ } from "./chunk-BWOUSEHM.js";
9
9
  import "./chunk-OEX7NZE3.js";
10
10
  import {
11
11
  useAutoScroll,
12
12
  useRunCollapseState,
13
13
  useRunGroups
14
- } from "./chunk-UYTRNR7Z.js";
15
- import "./chunk-6RIX7M27.js";
16
- import "./chunk-XT4JMYVB.js";
17
- import "./chunk-EIUE5GV2.js";
18
- import "./chunk-AAUNOHVL.js";
14
+ } from "./chunk-565EATZZ.js";
15
+ import "./chunk-Q7ZR6LSD.js";
16
+ import "./chunk-KNYSKNZX.js";
17
+ import "./chunk-ND3ZFXQK.js";
18
+ import "./chunk-NWECTSOH.js";
19
19
  import "./chunk-L57WRIND.js";
20
- import "./chunk-ZRVH3WCA.js";
20
+ import "./chunk-4CLN43XT.js";
21
+ import "./chunk-XU3HB5QL.js";
21
22
  import "./chunk-P5F43A65.js";
22
23
  import "./chunk-WUQDUBJG.js";
23
24
  import "./chunk-RQHJBTEU.js";
@@ -7,8 +7,8 @@ import {
7
7
  QuestionPreview,
8
8
  WebSearchPreview,
9
9
  WriteFilePreview
10
- } from "./chunk-AAUNOHVL.js";
11
- import "./chunk-ZRVH3WCA.js";
10
+ } from "./chunk-NWECTSOH.js";
11
+ import "./chunk-XU3HB5QL.js";
12
12
  import "./chunk-WUQDUBJG.js";
13
13
  import "./chunk-RQHJBTEU.js";
14
14
  export {
package/dist/utils.js CHANGED
@@ -4,14 +4,16 @@ import {
4
4
  } from "./chunk-XGKULLYE.js";
5
5
  import {
6
6
  TOOL_CATEGORY_ICONS,
7
+ getToolCategory,
8
+ getToolDisplayMetadata,
9
+ getToolErrorText
10
+ } from "./chunk-ND3ZFXQK.js";
11
+ import {
7
12
  formatBytes,
8
13
  formatDuration,
9
14
  formatUptime,
10
- getToolCategory,
11
- getToolDisplayMetadata,
12
- getToolErrorText,
13
15
  truncateText
14
- } from "./chunk-EIUE5GV2.js";
16
+ } from "./chunk-4CLN43XT.js";
15
17
  import {
16
18
  cn
17
19
  } from "./chunk-RQHJBTEU.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/ui",
3
- "version": "11.7.0",
3
+ "version": "11.8.0",
4
4
  "description": "Generic React UI components for Tangle products — primitives, chat, run, files, editor, markdown.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,149 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { FileArtifactPane } from "./file-artifact-pane";
3
+
4
+ const TS_SOURCE = `interface SandboxConfig {
5
+ model: string
6
+ timeout: number
7
+ env: Record<string, string>
8
+ }
9
+
10
+ // Create a new sandbox and stream its output.
11
+ export async function run(config: SandboxConfig) {
12
+ const sandbox = await Sandbox.create(config)
13
+ for await (const chunk of sandbox.stream()) {
14
+ process.stdout.write(chunk)
15
+ }
16
+ }`;
17
+
18
+ const MARKDOWN_SOURCE = `# Run report
19
+
20
+ The agent finished **3 of 3** tasks.
21
+
22
+ | Task | Result |
23
+ | --- | --- |
24
+ | Build | pass |
25
+ | Test | 315 / 315 |
26
+ | Deploy | staging |
27
+
28
+ \`\`\`ts
29
+ export const ready = true
30
+ \`\`\`
31
+ `;
32
+
33
+ // A transparent SVG, larger than the pane, so the checker ground shows through
34
+ // and the fit-to-pane / natural-size toggle has a visible effect.
35
+ const IMAGE_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="900" viewBox="0 0 640 360">
36
+ <defs>
37
+ <linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
38
+ <stop offset="0" stop-color="#8263FF"/>
39
+ <stop offset="1" stop-color="#10b981"/>
40
+ </linearGradient>
41
+ </defs>
42
+ <circle cx="220" cy="180" r="120" fill="url(#g)"/>
43
+ <rect x="360" y="80" width="200" height="200" rx="28" fill="#FFB347" opacity="0.9"/>
44
+ <text x="320" y="330" text-anchor="middle" font-family="sans-serif" font-size="22" fill="#E8E6F6">chart.svg · 1600 × 900</text>
45
+ </svg>`;
46
+ const IMAGE_URL = `data:image/svg+xml;utf8,${encodeURIComponent(IMAGE_SVG)}`;
47
+
48
+ // One-page PDF (Helvetica text and a rule) so the viewer has real content.
49
+ const PDF_URL =
50
+ "data:application/pdf;base64,JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSAvQ29udGVudHMgNCAwIFIgL1Jlc291cmNlcyA8PCAvRm9udCA8PCAvRjEgNSAwIFIgPj4gPj4gPj4KZW5kb2JqCjQgMCBvYmoKPDwgL0xlbmd0aCAyODkgPj4Kc3RyZWFtCkJUIC9GMSAyMiBUZiA1NiA3MDAgVGQgKEZpbGVBcnRpZmFjdFBhbmUpIFRqIEVUCkJUIC9GMSAxMiBUZiA1NiA2NzIgVGQgKFBERiBwcmV2aWV3IHJlbmRlcnMgdGhlIGJsb2JVcmwgYXQgZnVsbCBwYW5lIGhlaWdodC4pIFRqIEVUCkJUIC9GMSAxMiBUZiA1NiA2NTIgVGQgKEV2ZXJ5IGNvbnN1bWVyIG9mIEB0YW5nbGUtbmV0d29yay91aS9maWxlcyBnZXRzIHRoaXMgZm9yIGZyZWUuKSBUaiBFVAo1NiA2MjAgbSA1NTYgNjIwIGwgUwpCVCAvRjEgMTAgVGYgNTYgNTk2IFRkIChQYWdlIDEgb2YgMSkgVGogRVQKZW5kc3RyZWFtCmVuZG9iago1IDAgb2JqCjw8IC9UeXBlIC9Gb250IC9TdWJ0eXBlIC9UeXBlMSAvQmFzZUZvbnQgL0hlbHZldGljYSA+PgplbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAowMDAwMDAwMjQxIDAwMDAwIG4gCjAwMDAwMDA1ODEgMDAwMDAgbiAKdHJhaWxlcgo8PCAvU2l6ZSA2IC9Sb290IDEgMCBSID4+CnN0YXJ0eHJlZgo2NTEKJSVFT0YK";
51
+
52
+ const CSV_SOURCE = [
53
+ "id,name,role,note,active",
54
+ ...Array.from({ length: 520 }, (_, i) =>
55
+ [
56
+ i + 1,
57
+ `"Lovelace, Ada ${i + 1}"`,
58
+ ["engineer", "maintainer", "architect"][i % 3],
59
+ `"Handles ""quoted"" values, and commas"`,
60
+ i % 4 === 0 ? "false" : "true",
61
+ ].join(","),
62
+ ),
63
+ ].join("\n");
64
+
65
+ // CC0 sample media hosted by MDN; the story only needs a playable src.
66
+ const VIDEO_URL = "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4";
67
+ const AUDIO_URL = "https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3";
68
+
69
+ const meta: Meta<typeof FileArtifactPane> = {
70
+ title: "Files/FileArtifactPane",
71
+ component: FileArtifactPane,
72
+ parameters: { layout: "fullscreen" },
73
+ argTypes: {
74
+ onDownload: { action: "download" },
75
+ onClose: { action: "close" },
76
+ },
77
+ decorators: [
78
+ (Story) => (
79
+ <div className="h-[560px] w-[760px] overflow-hidden rounded-xl border border-border bg-card text-foreground">
80
+ <Story />
81
+ </div>
82
+ ),
83
+ ],
84
+ };
85
+
86
+ export default meta;
87
+ type Story = StoryObj<typeof FileArtifactPane>;
88
+
89
+ export const Code: Story = {
90
+ args: { filename: "run.ts", path: "src/run.ts", content: TS_SOURCE },
91
+ };
92
+
93
+ export const Image: Story = {
94
+ args: {
95
+ filename: "chart.svg",
96
+ path: "out/chart.svg",
97
+ blobUrl: IMAGE_URL,
98
+ mimeType: "image/svg+xml",
99
+ size: 1_204,
100
+ },
101
+ };
102
+
103
+ export const Pdf: Story = {
104
+ name: "PDF",
105
+ args: {
106
+ filename: "report.pdf",
107
+ path: "out/report.pdf",
108
+ blobUrl: PDF_URL,
109
+ mimeType: "application/pdf",
110
+ size: 834,
111
+ },
112
+ };
113
+
114
+ export const Csv: Story = {
115
+ name: "CSV (520 rows, capped at 500)",
116
+ args: { filename: "people.csv", path: "data/people.csv", content: CSV_SOURCE },
117
+ };
118
+
119
+ export const Video: Story = {
120
+ args: {
121
+ filename: "flower.mp4",
122
+ path: "media/flower.mp4",
123
+ blobUrl: VIDEO_URL,
124
+ mimeType: "video/mp4",
125
+ },
126
+ };
127
+
128
+ export const Audio: Story = {
129
+ args: {
130
+ filename: "t-rex-roar.mp3",
131
+ path: "media/t-rex-roar.mp3",
132
+ blobUrl: AUDIO_URL,
133
+ mimeType: "audio/mpeg",
134
+ },
135
+ };
136
+
137
+ export const Spreadsheet: Story = {
138
+ args: { filename: "ledger.xlsx", path: "finance/ledger.xlsx", size: 48_213 },
139
+ };
140
+
141
+ export const MarkdownProse: Story = {
142
+ name: "Markdown",
143
+ args: { filename: "REPORT.md", path: "out/REPORT.md", content: MARKDOWN_SOURCE },
144
+ };
145
+
146
+ export const BinaryWithoutLink: Story = {
147
+ name: "Binary without a download link",
148
+ args: { filename: "weights.bin", path: "model/weights.bin", size: 402_653_184 },
149
+ };
@@ -0,0 +1,192 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { fireEvent, render, screen } from "@testing-library/react";
3
+ import { FileArtifactPane } from "./file-artifact-pane";
4
+ import { CSV_PREVIEW_ROW_LIMIT, parseCsv } from "./file-preview";
5
+
6
+ const BLOB_URL = "blob:https://app.tangle.tools/1f2e3d4c";
7
+
8
+ function csvWithRows(rowCount: number): string {
9
+ const rows = Array.from({ length: rowCount }, (_, i) => `"Ada, Countess ${i}",x,${i}`);
10
+ return ["name,note,n", ...rows].join("\n");
11
+ }
12
+
13
+ describe("FileArtifactPane previews", () => {
14
+ it("renders an image from blobUrl and toggles natural size on click", () => {
15
+ const { container } = render(
16
+ <FileArtifactPane filename="chart.png" blobUrl={BLOB_URL} mimeType="image/png" />,
17
+ );
18
+
19
+ const img = container.querySelector("img");
20
+ expect(img).not.toBeNull();
21
+ expect(img).toHaveAttribute("src", BLOB_URL);
22
+ expect(img).toHaveAttribute("alt", "chart.png");
23
+ expect(img).toHaveClass("object-contain");
24
+
25
+ const toggle = screen.getByRole("button", { name: "Show image at natural size" });
26
+ expect(toggle).toHaveAttribute("aria-pressed", "false");
27
+ fireEvent.click(toggle);
28
+ expect(screen.getByRole("button", { name: "Fit image to pane" })).toHaveAttribute(
29
+ "aria-pressed",
30
+ "true",
31
+ );
32
+ expect(container.querySelector("img")).toHaveClass("max-w-none");
33
+ });
34
+
35
+ it("renders a PDF as an <object> of the blobUrl", () => {
36
+ const { container } = render(
37
+ <FileArtifactPane filename="report.pdf" blobUrl={BLOB_URL} mimeType="application/pdf" />,
38
+ );
39
+
40
+ const viewer = container.querySelector("object");
41
+ expect(viewer).not.toBeNull();
42
+ expect(viewer).toHaveAttribute("data", BLOB_URL);
43
+ expect(viewer).toHaveAttribute("type", "application/pdf");
44
+ expect(viewer).toHaveAttribute("title", "report.pdf");
45
+ });
46
+
47
+ it("renders video and audio with native controls", () => {
48
+ const video = render(<FileArtifactPane filename="demo.mp4" blobUrl={BLOB_URL} />);
49
+ const videoElement = video.container.querySelector("video");
50
+ expect(videoElement).not.toBeNull();
51
+ expect(videoElement).toHaveAttribute("src", BLOB_URL);
52
+ expect(videoElement).toHaveAttribute("controls");
53
+ video.unmount();
54
+
55
+ const audio = render(<FileArtifactPane filename="voice.m4a" blobUrl={BLOB_URL} />);
56
+ const audioElement = audio.container.querySelector("audio");
57
+ expect(audioElement).not.toBeNull();
58
+ expect(audioElement).toHaveAttribute("src", BLOB_URL);
59
+ expect(audioElement).toHaveAttribute("controls");
60
+ expect(audio.getAllByText("voice.m4a").length).toBeGreaterThan(1); // header + player label
61
+ });
62
+
63
+ it("parses quoted commas in CSV and caps the table at the row limit", () => {
64
+ const total = CSV_PREVIEW_ROW_LIMIT + 1;
65
+ const { container } = render(
66
+ <FileArtifactPane filename="people.csv" content={csvWithRows(total)} />,
67
+ );
68
+
69
+ const headers = Array.from(container.querySelectorAll("thead th")).map((th) => th.textContent);
70
+ expect(headers).toEqual(["name", "note", "n"]);
71
+ expect(container.querySelectorAll("thead th")[0]).toHaveClass("sticky");
72
+
73
+ const bodyRows = container.querySelectorAll("tbody tr");
74
+ expect(bodyRows).toHaveLength(CSV_PREVIEW_ROW_LIMIT);
75
+ expect(bodyRows[0].querySelectorAll("td")[0]).toHaveTextContent("Ada, Countess 0");
76
+ expect(bodyRows[0].querySelectorAll("td")).toHaveLength(3);
77
+
78
+ expect(
79
+ screen.getByText(`Showing ${CSV_PREVIEW_ROW_LIMIT} of ${total} rows · 3 columns`),
80
+ ).toBeInTheDocument();
81
+ });
82
+
83
+ it("reports the row count when the CSV is under the cap", () => {
84
+ render(<FileArtifactPane filename="people.csv" content={csvWithRows(3)} />);
85
+ expect(screen.getByText("3 rows · 3 columns")).toBeInTheDocument();
86
+ });
87
+
88
+ it("renders a spreadsheet as a download card and calls onDownload", () => {
89
+ const onDownload = vi.fn();
90
+ render(
91
+ <FileArtifactPane filename="book.xlsx" size={1536} onDownload={onDownload} />,
92
+ );
93
+
94
+ expect(screen.getByText("1.5 KB")).toBeInTheDocument();
95
+ expect(
96
+ screen.getByText("Download to open this workbook in a spreadsheet app."),
97
+ ).toBeInTheDocument();
98
+
99
+ fireEvent.click(screen.getByRole("button", { name: "Download" }));
100
+ expect(onDownload).toHaveBeenCalledTimes(1);
101
+
102
+ // The header keeps its own download control.
103
+ fireEvent.click(screen.getByRole("button", { name: "Download book.xlsx" }));
104
+ expect(onDownload).toHaveBeenCalledTimes(2);
105
+ });
106
+
107
+ it("renders markdown through the package Markdown renderer", () => {
108
+ const { container } = render(
109
+ <FileArtifactPane filename="README.md" content={"# Title\n\nBody with **bold**."} />,
110
+ );
111
+ expect(container.querySelector(".tangle-prose")).not.toBeNull();
112
+ expect(container.querySelector("h1")).toHaveTextContent("Title");
113
+ expect(container.querySelector("strong")).toHaveTextContent("bold");
114
+ });
115
+
116
+ it("renders a binary file with no download link as one muted sentence", () => {
117
+ const onDownload = vi.fn();
118
+ const withDownload = render(
119
+ <FileArtifactPane filename="weights.bin" onDownload={onDownload} />,
120
+ );
121
+ expect(
122
+ withDownload.getByText("This file needs a download link to preview."),
123
+ ).toBeInTheDocument();
124
+ fireEvent.click(withDownload.getByRole("button", { name: "Download" }));
125
+ expect(onDownload).toHaveBeenCalledTimes(1);
126
+ withDownload.unmount();
127
+
128
+ render(<FileArtifactPane filename="weights.bin" />);
129
+ expect(screen.getByText("This file needs a download link to preview.")).toBeInTheDocument();
130
+ expect(screen.queryByRole("button", { name: "Download" })).toBeNull();
131
+ });
132
+
133
+ it("renders the same sentence for an image, PDF, or video without a blobUrl", () => {
134
+ for (const filename of ["chart.png", "report.pdf", "demo.mp4"]) {
135
+ const { getByText, unmount } = render(<FileArtifactPane filename={filename} />);
136
+ expect(getByText("This file needs a download link to preview.")).toBeInTheDocument();
137
+ unmount();
138
+ }
139
+ });
140
+
141
+ it("renders an unknown file's text content as text", () => {
142
+ const { container } = render(
143
+ <FileArtifactPane filename="Makefile.custom" content={"build:\n\tgo build"} />,
144
+ );
145
+ expect(container.querySelector("pre")).toHaveTextContent("go build");
146
+ expect(container.querySelector("code")).toBeNull();
147
+ });
148
+
149
+ it("keeps code on the syntax highlighter and text on a plain pre", () => {
150
+ const code = render(
151
+ <FileArtifactPane filename="server.ts" content={"const x = 1;\nexport { x };"} />,
152
+ );
153
+ expect(code.container.querySelector("code")).not.toBeNull();
154
+ expect(code.container.querySelector("table")).toBeNull();
155
+ expect(code.getByText(/ts · 2 lines/)).toBeInTheDocument();
156
+ code.unmount();
157
+
158
+ const text = render(<FileArtifactPane filename="notes.txt" content="just words" />);
159
+ expect(text.container.querySelector("pre")).toHaveTextContent("just words");
160
+ expect(text.container.querySelector("code")).toBeNull();
161
+ });
162
+
163
+ it("renders SVG source as code when the host read it as text", () => {
164
+ const { container } = render(
165
+ <FileArtifactPane filename="icon.svg" content={'<svg xmlns="http://www.w3.org/2000/svg"/>'} />,
166
+ );
167
+ expect(container.querySelector("code")).not.toBeNull();
168
+ expect(container.querySelector("img")).toBeNull();
169
+ });
170
+ });
171
+
172
+ describe("parseCsv", () => {
173
+ it("keeps commas, line breaks, and doubled quotes inside quoted cells", () => {
174
+ expect(parseCsv('a,b\n"x, y","line 1\nline 2"\n"say ""hi""",z')).toEqual([
175
+ ["a", "b"],
176
+ ["x, y", "line 1\nline 2"],
177
+ ['say "hi"', "z"],
178
+ ]);
179
+ });
180
+
181
+ it("trims unquoted cells and accepts CRLF, blank lines, and a trailing line break", () => {
182
+ expect(parseCsv("a, b\r\n\r\n1 ,2\r\n")).toEqual([
183
+ ["a", "b"],
184
+ ["1", "2"],
185
+ ]);
186
+ });
187
+
188
+ it("returns no rows for empty input", () => {
189
+ expect(parseCsv("")).toEqual([]);
190
+ expect(parseCsv("\n\n")).toEqual([]);
191
+ });
192
+ });