Skip to content

Commit 0d9aef2

Browse files
authored
Show image files in code viewer (#93)
- Return image data URLs for binary image files - Render images in the web file viewer - Allow larger read limits for image previews
1 parent 78e26d9 commit 0d9aef2

3 files changed

Lines changed: 67 additions & 6 deletions

File tree

apps/server/src/wsServer.ts

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -844,7 +844,17 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return<
844844
relativePath: body.relativePath,
845845
path,
846846
});
847-
const MAX_READ_SIZE = 1_048_576; // 1MB
847+
const MAX_READ_SIZE_TEXT = 1_048_576; // 1MB
848+
const MAX_READ_SIZE_IMAGE = 10_485_760; // 10MB
849+
const IMAGE_EXTENSIONS_SET = new Set([
850+
".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp",
851+
".ico", ".tiff", ".tif", ".avif", ".svg",
852+
]);
853+
const fileExt = target.relativePath
854+
.substring(target.relativePath.lastIndexOf("."))
855+
.toLowerCase();
856+
const isImageExt = IMAGE_EXTENSIONS_SET.has(fileExt);
857+
const MAX_READ_SIZE = isImageExt ? MAX_READ_SIZE_IMAGE : MAX_READ_SIZE_TEXT;
848858
const fileStat = yield* fileSystem.stat(target.absolutePath).pipe(
849859
Effect.mapError(
850860
(cause) =>
@@ -861,7 +871,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return<
861871
const sizeBytes = Number(fileStat.size);
862872
if (sizeBytes > MAX_READ_SIZE) {
863873
return yield* new RouteRequestError({
864-
message: `File is too large to display (${(sizeBytes / 1024 / 1024).toFixed(1)}MB). Maximum supported size is 1MB.`,
874+
message: `File is too large to display (${(sizeBytes / 1024 / 1024).toFixed(1)}MB). Maximum supported size is ${(MAX_READ_SIZE / 1024 / 1024).toFixed(0)}MB.`,
865875
});
866876
}
867877
// Read raw bytes to detect binary files
@@ -875,13 +885,48 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return<
875885
);
876886
// Check for null bytes in the first 8KB to detect binary files
877887
const checkLength = Math.min(rawBytes.length, 8192);
888+
let isBinary = false;
878889
for (let i = 0; i < checkLength; i++) {
879890
if (rawBytes[i] === 0) {
880-
return yield* new RouteRequestError({
881-
message: `File appears to be binary and cannot be displayed: ${target.relativePath}`,
882-
});
891+
isBinary = true;
892+
break;
883893
}
884894
}
895+
if (isBinary) {
896+
// Check if the file is an image by extension
897+
const IMAGE_EXTENSIONS: Record<string, string> = {
898+
".png": "image/png",
899+
".jpg": "image/jpeg",
900+
".jpeg": "image/jpeg",
901+
".gif": "image/gif",
902+
".bmp": "image/bmp",
903+
".webp": "image/webp",
904+
".ico": "image/x-icon",
905+
".tiff": "image/tiff",
906+
".tif": "image/tiff",
907+
".avif": "image/avif",
908+
".svg": "image/svg+xml",
909+
};
910+
const ext = target.relativePath
911+
.substring(target.relativePath.lastIndexOf("."))
912+
.toLowerCase();
913+
const mimeType = IMAGE_EXTENSIONS[ext];
914+
if (mimeType) {
915+
// Return image as base64 data URL
916+
const base64 = Buffer.from(rawBytes).toString("base64");
917+
const imageDataUrl = `data:${mimeType};base64,${base64}`;
918+
return {
919+
relativePath: target.relativePath,
920+
contents: "",
921+
sizeBytes,
922+
truncated: false,
923+
imageDataUrl,
924+
};
925+
}
926+
return yield* new RouteRequestError({
927+
message: `File appears to be binary and cannot be displayed: ${target.relativePath}`,
928+
});
929+
}
885930
const contents = new TextDecoder().decode(rawBytes);
886931
return {
887932
relativePath: target.relativePath,

apps/web/src/components/CodeViewerPanel.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,28 @@ const CodeViewerFileContent = memo(function CodeViewerFileContent(props: {
8686
);
8787
}
8888

89-
if (!query.data?.contents && query.data?.contents !== "") {
89+
if (!query.data?.contents && query.data?.contents !== "" && !query.data?.imageDataUrl) {
9090
return (
9191
<div className="flex flex-1 items-center justify-center px-5 text-center text-xs text-muted-foreground/70">
9292
No content available.
9393
</div>
9494
);
9595
}
9696

97+
// Render image files
98+
if (query.data?.imageDataUrl) {
99+
return (
100+
<div className="flex min-h-0 flex-1 items-center justify-center overflow-auto p-4">
101+
<img
102+
src={query.data.imageDataUrl}
103+
alt={props.relativePath}
104+
className="max-h-full max-w-full object-contain"
105+
draggable={false}
106+
/>
107+
</div>
108+
);
109+
}
110+
97111
return (
98112
<div className="min-h-0 flex-1 overflow-y-auto">
99113
{query.data.truncated && (

packages/contracts/src/project.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,5 +79,7 @@ export const ProjectReadFileResult = Schema.Struct({
7979
contents: Schema.String,
8080
sizeBytes: Schema.Number,
8181
truncated: Schema.Boolean,
82+
/** Base64 data URL for image files (e.g. "data:image/png;base64,...") */
83+
imageDataUrl: Schema.optional(Schema.String),
8284
});
8385
export type ProjectReadFileResult = typeof ProjectReadFileResult.Type;

0 commit comments

Comments
 (0)