Skip to content

Commit dbeec4b

Browse files
committed
fixing rnadom bugs
1 parent 913c2c1 commit dbeec4b

17 files changed

Lines changed: 865 additions & 161 deletions

File tree

.ade/ade.yaml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
version: 1
2-
processes: []
2+
processes:
3+
- id: xsi8oj88
4+
name: dogfood onboardinign fixes
5+
command:
6+
- ./scripts/dogfood.sh
7+
- onboarding-fixes
8+
cwd: apps/desktop
9+
gracefulShutdownMs: 7000
10+
readiness:
11+
type: none
312
stackButtons: []
413
testSuites: []
514
laneOverlayPolicies: []

apps/desktop/src/main/services/files/fileService.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,64 @@ describe("fileService", () => {
5050
}
5151
});
5252

53+
it("returns an inline image preview for image files", () => {
54+
const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), "ade-file-service-image-"));
55+
const laneService = createLaneServiceStub(rootPath);
56+
const service = createFileService({ laneService });
57+
58+
try {
59+
const pngBytes = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
60+
fs.writeFileSync(path.join(rootPath, "logo.png"), pngBytes);
61+
62+
const result = service.readFile({
63+
workspaceId: "workspace-1",
64+
path: "logo.png",
65+
});
66+
67+
expect(result).toMatchObject({
68+
content: pngBytes.toString("base64"),
69+
encoding: "base64",
70+
size: pngBytes.length,
71+
languageId: "image",
72+
isBinary: true,
73+
previewKind: "image",
74+
mimeType: "image/png",
75+
dataUrl: `data:image/png;base64,${pngBytes.toString("base64")}`,
76+
});
77+
} finally {
78+
fs.rmSync(rootPath, { recursive: true, force: true });
79+
}
80+
});
81+
82+
it("treats invalid non-null bytes as unsupported binary content", () => {
83+
const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), "ade-file-service-binary-"));
84+
const laneService = createLaneServiceStub(rootPath);
85+
const service = createFileService({ laneService });
86+
87+
try {
88+
const bytes = Buffer.from([0xff, 0xfe, 0xfd, 0xfc]);
89+
fs.writeFileSync(path.join(rootPath, "payload.bin"), bytes);
90+
91+
const result = service.readFile({
92+
workspaceId: "workspace-1",
93+
path: "payload.bin",
94+
});
95+
96+
expect(result).toMatchObject({
97+
content: bytes.toString("base64"),
98+
encoding: "base64",
99+
size: bytes.length,
100+
languageId: "plaintext",
101+
isBinary: true,
102+
previewKind: "binary",
103+
mimeType: null,
104+
});
105+
expect(result.dataUrl).toBeUndefined();
106+
} finally {
107+
fs.rmSync(rootPath, { recursive: true, force: true });
108+
}
109+
});
110+
53111
it("includes ignored files in quick open and search when requested", async () => {
54112
const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), "ade-file-service-search-"));
55113
const { execSync } = await import("node:child_process");

apps/desktop/src/main/services/files/fileService.ts

Lines changed: 124 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,53 @@ import { createFileSearchIndexService } from "./fileSearchIndexService";
3737

3838
const MAX_EDITOR_READ_BYTES = 5 * 1024 * 1024;
3939
const GIT_STATUS_CACHE_TTL_MS = 5_000;
40+
const TEXT_EXTENSIONS = new Set([
41+
".bash",
42+
".c",
43+
".cc",
44+
".cfg",
45+
".cjs",
46+
".conf",
47+
".cpp",
48+
".cs",
49+
".css",
50+
".csv",
51+
".cts",
52+
".env",
53+
".fish",
54+
".go",
55+
".h",
56+
".hpp",
57+
".html",
58+
".ini",
59+
".java",
60+
".js",
61+
".json",
62+
".jsonc",
63+
".jsx",
64+
".less",
65+
".log",
66+
".md",
67+
".mdx",
68+
".mjs",
69+
".mts",
70+
".py",
71+
".rb",
72+
".rs",
73+
".sass",
74+
".scss",
75+
".sh",
76+
".sql",
77+
".swift",
78+
".toml",
79+
".ts",
80+
".tsx",
81+
".txt",
82+
".xml",
83+
".yaml",
84+
".yml",
85+
".zsh",
86+
]);
4087

4188
function containsDotGit(absPath: string): boolean {
4289
const parts = absPath.split(path.sep);
@@ -45,6 +92,7 @@ function containsDotGit(absPath: string): boolean {
4592

4693
function languageIdFromPath(relPath: string): string {
4794
const ext = path.extname(relPath).toLowerCase();
95+
if (isImagePath(relPath)) return "image";
4896
if (ext === ".ts" || ext === ".tsx") return "typescript";
4997
if (ext === ".js" || ext === ".jsx" || ext === ".mjs" || ext === ".cjs") return "javascript";
5098
if (ext === ".json") return "json";
@@ -61,6 +109,62 @@ function languageIdFromPath(relPath: string): string {
61109
return "plaintext";
62110
}
63111

112+
function isImagePath(relPath: string): boolean {
113+
return inferImageMimeType(relPath) !== null;
114+
}
115+
116+
function inferImageMimeType(relPath: string): string | null {
117+
const ext = path.extname(relPath).toLowerCase();
118+
switch (ext) {
119+
case ".avif":
120+
return "image/avif";
121+
case ".bmp":
122+
return "image/bmp";
123+
case ".gif":
124+
return "image/gif";
125+
case ".ico":
126+
case ".cur":
127+
return "image/x-icon";
128+
case ".jpg":
129+
case ".jpeg":
130+
case ".jfif":
131+
case ".pjpeg":
132+
case ".pjp":
133+
return "image/jpeg";
134+
case ".png":
135+
return "image/png";
136+
case ".svg":
137+
return "image/svg+xml";
138+
case ".webp":
139+
return "image/webp";
140+
default:
141+
return null;
142+
}
143+
}
144+
145+
function looksLikeBinary(buf: Buffer, relPath: string): boolean {
146+
if (hasNullByte(buf)) return true;
147+
if (TEXT_EXTENSIONS.has(path.extname(relPath).toLowerCase())) return false;
148+
149+
const sample = buf.subarray(0, Math.min(buf.length, 8192));
150+
if (sample.length === 0) return false;
151+
152+
const decoded = sample.toString("utf8");
153+
const replacementChars = decoded.match(/\uFFFD/g)?.length ?? 0;
154+
if (replacementChars > 0) {
155+
return replacementChars / decoded.length > 0.01;
156+
}
157+
158+
let suspiciousControlChars = 0;
159+
for (const byte of sample) {
160+
const isAllowedWhitespace = byte === 9 || byte === 10 || byte === 12 || byte === 13;
161+
if (byte < 32 && !isAllowedWhitespace) {
162+
suspiciousControlChars += 1;
163+
}
164+
}
165+
return suspiciousControlChars / sample.length > 0.3;
166+
}
167+
64168
function isAlwaysIgnoredPath(normalized: string): boolean {
65169
return (
66170
normalized.startsWith(".git/") ||
@@ -366,7 +470,6 @@ export function createFileService({
366470
if (await isIgnoredPath(rootPath, rel, includeIgnored)) continue;
367471
if (entry.name === ".git") continue;
368472

369-
const childAbs = path.join(dirPath, entry.name);
370473
const node: FileTreeNode = {
371474
name: entry.name,
372475
path: rel,
@@ -443,13 +546,29 @@ export function createFileService({
443546
);
444547
}
445548
const buf = fs.readFileSync(absPath);
446-
const isBinary = hasNullByte(buf);
549+
const imageMimeType = inferImageMimeType(normalizedRel);
550+
if (imageMimeType) {
551+
const base64 = buf.toString("base64");
552+
return {
553+
content: base64,
554+
encoding: "base64",
555+
size: stat.size,
556+
languageId: languageIdFromPath(normalizedRel),
557+
isBinary: true,
558+
previewKind: "image",
559+
mimeType: imageMimeType,
560+
dataUrl: `data:${imageMimeType};base64,${base64}`,
561+
};
562+
}
563+
const isBinary = looksLikeBinary(buf, normalizedRel);
447564
return {
448-
content: isBinary ? "" : buf.toString("utf8"),
449-
encoding: "utf-8",
565+
content: isBinary ? buf.toString("base64") : buf.toString("utf8"),
566+
encoding: isBinary ? "base64" : "utf-8",
450567
size: stat.size,
451568
languageId: languageIdFromPath(normalizedRel),
452-
isBinary
569+
isBinary,
570+
previewKind: isBinary ? "binary" : "text",
571+
mimeType: null,
453572
};
454573
},
455574

0 commit comments

Comments
 (0)