Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/desktop/src/ipc/methods/window.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ describe("pickProjectFavicon", () => {
"gif",
"ico",
"heic",
"heif",
"jpeg",
"jpg",
"png",
Expand Down
10 changes: 9 additions & 1 deletion apps/mobile/src/features/threads/fileChipMenu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ describe("file chip downloads", () => {
["/tmp/take%2520%23one.mp4:12", "/tmp/take%20#one.mp4", "video/mp4"],
["/tmp/report.pdf", "/tmp/report.pdf", "application/pdf"],
["screens/image.PNG", "/repo/screens/image.PNG", "image/png"],
["/tmp/archive.zip", "/tmp/archive.zip", "application/zip"],
["/tmp/archive.jar", "/tmp/archive.jar", "application/java-archive"],
["/tmp/archive.tar", "/tmp/archive.tar", "application/x-tar"],
["/tmp/archive.gz", "/tmp/archive.gz", "application/gzip"],
["/tmp/archive.tgz", "/tmp/archive.tgz", "application/gzip"],
["/tmp/data.sqlite", "/tmp/data.sqlite", "application/vnd.sqlite3"],
["/tmp/data.sqlite3", "/tmp/data.sqlite3", "application/vnd.sqlite3"],
["/tmp/data.db", "/tmp/data.db", "application/vnd.sqlite3"],
])("offers a host download for %s", (href, path, mimeType) => {
const target = resolveFileChipTarget(href, "/repo")!;
expect(fileChipMenu(target).actions).toContainEqual({
Expand All @@ -78,7 +86,7 @@ describe("file chip downloads", () => {
});

it("does not offer downloads the host asset endpoint cannot serve", () => {
for (const href of ["src/app.ts", "/tmp/archive.zip", "/tmp/clip.mp4.txt"]) {
for (const href of ["src/app.ts", "/tmp/archive.7z", "/tmp/clip.mp4.txt"]) {
const target = resolveFileChipTarget(href, "/repo")!;
expect(fileChipShareSource(target, threadId)).toBeNull();
expect(fileChipMenu(target).actions.some(({ id }) => id === "save")).toBe(false);
Expand Down
24 changes: 20 additions & 4 deletions apps/server/src/assets/AssetAccess.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,14 @@ describe("AssetAccess", () => {
["report.pdf", "application/pdf"],
["tone.wav", "audio/wav"],
["tone.webm-audio", "audio/webm"],
["archive.zip", "application/zip"],
["archive.jar", "application/java-archive"],
["archive.tar", "application/x-tar"],
["archive.gz", "application/gzip"],
["archive.tgz", "application/gzip"],
["data.sqlite", "application/vnd.sqlite3"],
["data.sqlite3", "application/vnd.sqlite3"],
["data.db", "application/vnd.sqlite3"],
] as const) {
const filePath = path.join(outside, name);
yield* fs.writeFileString(filePath, "media");
Expand Down Expand Up @@ -1188,6 +1196,14 @@ describe("AssetAccess", () => {
["woff", "font/woff"],
["woff2", "font/woff2"],
["weba", "audio/webm"],
["zip", "application/zip"],
["jar", "application/java-archive"],
["tar", "application/x-tar"],
["gz", "application/gzip"],
["tgz", "application/gzip"],
["sqlite", "application/vnd.sqlite3"],
["sqlite3", "application/vnd.sqlite3"],
["db", "application/vnd.sqlite3"],
] as const) {
const attachmentId = `thread-1-00000000-0000-4000-8000-000000000001-${extension}`;
const attachmentPath = path.join(config.attachmentsDir, `${attachmentId}.${extension}`);
Expand All @@ -1198,7 +1214,7 @@ describe("AssetAccess", () => {
_tag: "attachment",
attachmentId,
fileName: `report.${extension}`,
mimeType: "application/octet-stream",
mimeType: "text/html",
disposition: "inline",
},
});
Expand Down Expand Up @@ -1256,16 +1272,16 @@ describe("AssetAccess", () => {
const config = yield* ServerConfig.ServerConfig;
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const attachmentId = "thread-1-00000000-0000-4000-8000-000000000002-zip";
const attachmentPath = path.join(config.attachmentsDir, `${attachmentId}.zip`);
const attachmentId = "thread-1-00000000-0000-4000-8000-000000000002-7z";
const attachmentPath = path.join(config.attachmentsDir, `${attachmentId}.7z`);
yield* fileSystem.makeDirectory(config.attachmentsDir, { recursive: true });
yield* fileSystem.writeFile(attachmentPath, new Uint8Array([1, 2, 3]));

const result = yield* issueAssetUrl({
resource: {
_tag: "attachment",
attachmentId,
fileName: "archive.zip",
fileName: "archive.7z",
mimeType: "text/html",
disposition: "inline",
},
Expand Down
3 changes: 3 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@
"react-markdown": "^10.1.0",
"read-excel-file": "9.3.12",
"remark-gfm": "catalog:",
"sql.js": "1.15.0",
"tailwind-merge": "^3.4.0",
"tiff": "7.1.3",
"unbash": "^5.0.0",
"use-sync-external-store": "^1.6.0",
"zustand": "^5.0.11"
Expand All @@ -88,6 +90,7 @@
"@types/mdast": "^4.0.4",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@types/sql.js": "1.4.11",
"@types/use-sync-external-store": "^0.0.6",
"@vercel/config": "^0.3.0",
"@vitejs/plugin-react": "^6.1.2",
Expand Down
14 changes: 5 additions & 9 deletions apps/web/src/components/files/AttachmentFilePreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { useCopyToClipboard } from "~/hooks/useCopyToClipboard";
import { useClientSettings, updateClientSettings } from "~/hooks/useSettings";
import { cn } from "~/lib/utils";

import { BinaryFilePreview } from "./BinaryFilePreview";
import { PreviewImage } from "./PreviewImage";
import { AudioPreview } from "./AudioPreview";
import { BrowserDocumentFrame } from "./BrowserDocumentFrame";
import { DelimitedTablePreview } from "./DelimitedTablePreview";
Expand Down Expand Up @@ -263,6 +265,8 @@ export function AttachmentFilePreview(props: {
) : (
<ReadOnlySourcePreview name={props.name} text={content.text} />
)
) : kind === "archive" || kind === "sqlite" ? (
<BinaryFilePreview src={url} name={props.name} kind={kind} />
) : kind === "font" ? (
<FontFilePreview src={url} name={props.name} />
) : officeKind ? (
Expand All @@ -289,15 +293,7 @@ export function AttachmentFilePreview(props: {
/>
</div>
) : kind === "image" ? (
// oxlint-disable-next-line t3code/require-centered-scroll-gutter -- The image is capped at max-h-full max-w-full, so this never scrolls.
<div className="flex min-h-0 flex-1 items-center justify-center overflow-auto p-4">
<img
src={url}
alt={props.name}
className="max-h-full max-w-full object-contain"
onError={() => setError("Unable to load image.")}
/>
</div>
<PreviewImage key={url} src={url} name={props.name} mimeType={props.mimeType} />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium files/AttachmentFilePreview.tsx:281

When an image fails to load, PreviewImage shows its local failure surface instead of setting this component’s error, so the parent FileSurfaceFailure and its retry action never appear. Report image load failures through setError so retry can refresh the asset URL and reload the image.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/files/AttachmentFilePreview.tsx around line 281:

When an image fails to load, `PreviewImage` shows its local failure surface instead of setting this component’s `error`, so the parent `FileSurfaceFailure` and its retry action never appear. Report image load failures through `setError` so retry can refresh the asset URL and reload the image.

) : (
<div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-1 px-6 text-center">
<p className="text-sm font-medium">No preview for this file</p>
Expand Down
17 changes: 17 additions & 0 deletions apps/web/src/components/files/BinaryFilePreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { lazy, Suspense } from "react";

import { FileSurfaceLoading } from "./fileSurfaceChrome";

const BinaryViewer = lazy(() => import("./BinaryViewer"));

export function BinaryFilePreview(props: {
readonly src: string;
readonly name: string;
readonly kind: "archive" | "sqlite";
}) {
return (
<Suspense fallback={<FileSurfaceLoading />}>
<BinaryViewer key={props.src} {...props} />
</Suspense>
);
}
Loading
Loading