Skip to content

Commit dd7b5e9

Browse files
committed
feat: Refactor file handling to separate binary and text files, improve path handling, and enhance download process
1 parent c5f3b5c commit dd7b5e9

File tree

1 file changed

+190
-55
lines changed

1 file changed

+190
-55
lines changed

index.js

Lines changed: 190 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -93,15 +93,12 @@
9393
// Executables & Binaries
9494
'exe', 'dll', 'so', 'dylib', 'a', 'o', 'obj',
9595

96-
// Java Compiled (we'll handle .class specially for icons)
97-
'class', // REMOVED - we want to show .class files with special icons
96+
// Java Compiled
97+
'class',
9898

9999
// Python Compiled
100100
'pyc', 'pyo', 'pyd',
101101

102-
// Images (optional - you can show them)
103-
// 'png', 'jpg', 'jpeg', 'gif', 'svg', 'ico', 'webp',
104-
105102
// Media
106103
'mp4', 'mp3', 'wav', 'avi', 'mov', 'flv', 'wmv', 'ogg',
107104

@@ -111,14 +108,31 @@
111108
// Fonts
112109
'ttf', 'woff', 'woff2', 'eot', 'otf',
113110

114-
// Lock files (optional)
115-
// 'lock', // If you want to hide lock files
116-
117111
// Other
118112
'log', 'cache', 'swp', 'swo', 'bak', 'tmp'
119113
])
120114
};
121115

116+
// Binary file extensions - show in structure but don't read content
117+
const BINARY_EXTS = new Set([
118+
// Images
119+
'png', 'jpg', 'jpeg', 'gif', 'bmp', 'ico', 'webp', 'svg', 'tiff', 'psd',
120+
// Media
121+
'mp4', 'mp3', 'wav', 'avi', 'mov', 'flv', 'wmv', 'ogg', 'webm', 'mkv',
122+
// Documents
123+
'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
124+
// Archives
125+
'zip', 'tar', 'gz', 'rar', '7z', 'bz2', 'xz', 'tgz',
126+
// Executables
127+
'exe', 'dll', 'so', 'dylib', 'bin',
128+
// Fonts
129+
'ttf', 'woff', 'woff2', 'eot', 'otf',
130+
// Database
131+
'db', 'sqlite', 'sqlite3',
132+
// Compiled
133+
'class', 'pyc', 'o', 'obj'
134+
]);
135+
122136

123137
// VS Code Material Icon Theme - Professional file icons
124138
const ICONS = {
@@ -330,7 +344,7 @@
330344
// STATE
331345
// ============================================================================
332346

333-
const S = { files: [], tree: [], root: 'project', ctx: '', busy: false, rendered: 0 };
347+
const S = { files: [], tree: [], root: 'project', ctx: '', busy: false, rendered: 0, basePath: '', basePathSet: false };
334348

335349
const $ = id => document.getElementById(id);
336350
const D = {
@@ -423,8 +437,29 @@
423437
return convertHundreds(n);
424438
};
425439

426-
const ign = p => { const pts = p.split('/'); if (pts.some(x => IGNORED.folders.has(x))) return true; const e = pts[pts.length - 1].split('.').pop().toLowerCase(); return IGNORED.exts.has(e); };
440+
// Build a full path string by optionally prepending a user-supplied base path.
441+
// If no base path is provided, return the relative path unchanged.
442+
const getFullPath = rel => {
443+
if (!rel) return rel;
444+
const r = String(rel);
445+
if (S.basePath) {
446+
const base = S.basePath.replace(/[\\/]+$/, '');
447+
const relNorm = r.replace(/^[\\/]+/, '');
448+
// If base looks like a Windows path (contains backslash) join with backslashes
449+
if (base.indexOf('\\') !== -1) {
450+
return base + '\\' + relNorm.replace(/\//g, '\\');
451+
}
452+
// Default to POSIX-style join
453+
return base + '/' + relNorm.replace(/\\/g, '/');
454+
}
455+
return r;
456+
};
427457

458+
const ign = p => { const pts = p.split('/'); if (pts.some(x => IGNORED.folders.has(x))) return true; const e = pts[pts.length - 1].split('.').pop().toLowerCase(); return IGNORED.exts.has(e); };
459+
const isBinary = filename => {
460+
const ext = filename.split('.').pop().toLowerCase();
461+
return BINARY_EXTS.has(ext);
462+
};
428463
// Get icon for file or folder
429464
const ico = (name, isFolder) => {
430465
if (isFolder) {
@@ -862,7 +897,7 @@
862897

863898
// Fast path: convert FileList to Array
864899
const all = Array.from(list);
865-
900+
866901
if (loadingText) loadingText.textContent = 'Filtering files...';
867902
await new Promise(r => setTimeout(r, 0));
868903

@@ -875,6 +910,35 @@
875910
file: f
876911
}));
877912

913+
// Auto-detect absolute base path when running in desktop/Electron environments
914+
// where File objects may expose a non-standard `path` property.
915+
if (!S.basePathSet) {
916+
const cand = S.files.find(ff => ff.file && ff.file.path);
917+
if (cand && cand.file && cand.file.path) {
918+
try {
919+
const full = String(cand.file.path);
920+
const rel = String(cand.path);
921+
const sep = full.indexOf('\\') !== -1 ? '\\' : '/';
922+
const relConv = rel.split('/').join(sep);
923+
let base = '';
924+
if (full.endsWith(relConv)) {
925+
base = full.slice(0, full.length - relConv.length);
926+
base = base.replace(/[\\/]+$/, '');
927+
} else {
928+
const idx = full.indexOf(relConv);
929+
if (idx !== -1) base = full.slice(0, idx).replace(/[\\/]+$/, '');
930+
}
931+
if (base) {
932+
S.basePath = base;
933+
S.basePathSet = true;
934+
console.info('Auto-detected base path:', base);
935+
}
936+
} catch (e) {
937+
// ignore; leave base path unset
938+
}
939+
}
940+
}
941+
878942
if (S.files.length === 0) { toast('No valid files', 'warning'); load(false); return; }
879943
if (S.files.length > 5000) { toast(`Large directory (${S.files.length} files) - rendering first 1000`, 'warning'); }
880944

@@ -938,20 +1002,34 @@
9381002
// CONTEXT GENERATION - STREAMING
9391003
// ============================================================================
9401004

941-
const gen = async () => {
1005+
const gen = async () => {
9421006
if (S.busy) return;
9431007

9441008
const cbs = Array.from(document.querySelectorAll('.file-checkbox:checked:not([disabled])'));
9451009
if (!cbs.length) { toast('Select files first', 'warning'); return; }
9461010

9471011
load(true);
948-
D.ed.innerHTML = '<div class="editor-placeholder"><span class="material-symbols-outlined">hourglass_empty</span><p>Generating...</p></div>';
1012+
D.ed.innerHTML = '<div class="editor-placeholder"><span class="material-symbols-outlined">hourglass_empty</span><p>Preparing...</p></div>';
1013+
1014+
// Use auto-detected base path if available (set in loadFiles). If none was detected, continue using relative paths.
9491015

9501016
try {
9511017
const paths = cbs.map(c => c.dataset.path);
9521018
const files = S.files.filter(f => paths.includes(f.path));
9531019

954-
const totalSize = files.reduce((s, f) => s + f.size, 0);
1020+
// Separate binary and text files
1021+
const textFiles = [];
1022+
const binaryFiles = [];
1023+
1024+
files.forEach(f => {
1025+
if (isBinary(f.name)) {
1026+
binaryFiles.push(f);
1027+
} else {
1028+
textFiles.push(f);
1029+
}
1030+
});
1031+
1032+
const totalSize = textFiles.reduce((s, f) => s + f.size, 0);
9551033
const isHuge = totalSize > 500 * 1024 * 1024;
9561034

9571035
if (totalSize > 100 * 1024 * 1024) {
@@ -962,46 +1040,66 @@
9621040

9631041
if (isHuge) {
9641042
D.ed.innerHTML = `<div class="editor-placeholder"><span class="material-symbols-outlined">warning</span><h3 style="color:#fbbf24;margin:10px 0">Huge Project!</h3><p>Size: <strong>${bytes(totalSize)}</strong></p><p>Too large for preview. Use download.</p></div>`;
965-
await genAndDL(struct, files);
1043+
await genAndDL(struct, textFiles, binaryFiles);
9661044
toast('Ready for download!', 'success');
9671045
return;
9681046
}
9691047

9701048
const isLarge = totalSize > 50 * 1024 * 1024;
9711049
const contents = [];
972-
let done = 0, failed = 0;
973-
974-
for (let i = 0; i < files.length; i += BATCH) {
975-
const batch = files.slice(i, i + BATCH);
976-
await Promise.all(batch.map(async f => {
977-
try {
978-
const txt = await f.file.text();
979-
contents.push({ path: f.path, content: txt });
980-
} catch (e) { console.warn('Skip', f.name, e); failed++; }
981-
}));
1050+
let done = 0, failed = 0, skipped = binaryFiles.length;
1051+
1052+
// Process text files in larger batches with better progress
1053+
const FAST_BATCH = 100;
1054+
for (let i = 0; i < textFiles.length; i += FAST_BATCH) {
1055+
const batch = textFiles.slice(i, i + FAST_BATCH);
1056+
1057+
// Read all files in parallel for speed
1058+
const results = await Promise.allSettled(
1059+
batch.map(f => f.file.text())
1060+
);
1061+
1062+
results.forEach((result, idx) => {
1063+
if (result.status === 'fulfilled') {
1064+
contents.push({
1065+
path: batch[idx].path,
1066+
content: result.value
1067+
});
1068+
} else {
1069+
console.warn('Skip', batch[idx].name, result.reason);
1070+
failed++;
1071+
}
1072+
});
9821073

9831074
done += batch.length;
984-
const pct = Math.round(done / files.length * 100);
985-
D.ed.innerHTML = `<div class="editor-placeholder"><span class="material-symbols-outlined">hourglass_empty</span><p>Processing: ${pct}%</p><small>${done} / ${files.length} files</small>${failed > 0 ? `<small style="color:#fbbf24">${failed} skipped</small>` : ''}</div>`;
986-
await new Promise(r => setTimeout(r, 0));
1075+
const pct = Math.round(done / textFiles.length * 100);
1076+
const status = [];
1077+
status.push(`<strong>${done}/${textFiles.length}</strong> files`);
1078+
if (skipped > 0) status.push(`<span style="color:#3b82f6">${skipped} binary</span>`);
1079+
if (failed > 0) status.push(`<span style="color:#fbbf24">${failed} failed</span>`);
1080+
1081+
D.ed.innerHTML = `<div class="editor-placeholder"><span class="material-symbols-outlined">hourglass_empty</span><p>Reading files: ${pct}%</p><small>${status.join(' • ')}</small></div>`;
1082+
1083+
// Yield to UI only every 5 batches for better performance
1084+
if (i % (FAST_BATCH * 5) === 0) {
1085+
await new Promise(r => setTimeout(r, 0));
1086+
}
9871087
}
9881088

989-
D.ed.innerHTML = '<div class="editor-placeholder"><span class="material-symbols-outlined">hourglass_empty</span><p>Building...</p></div>';
1089+
D.ed.innerHTML = '<div class="editor-placeholder"><span class="material-symbols-outlined">hourglass_empty</span><p>Building output...</p></div>';
9901090
await new Promise(r => setTimeout(r, 10));
9911091

9921092
const parts = [];
9931093
parts.push('<folder-structure>\n', struct, '</folder-structure>\n\n');
9941094

995-
const CHUNK = 100;
996-
for (let i = 0; i < contents.length; i += CHUNK) {
997-
const chunk = contents.slice(i, i + CHUNK);
998-
chunk.forEach(({ path, content }) => {
999-
parts.push(`<document path="${path}">\n`, content, '\n</document>\n\n');
1000-
});
1001-
const pct = Math.round(((i + chunk.length) / contents.length) * 100);
1002-
D.ed.innerHTML = `<div class="editor-placeholder"><span class="material-symbols-outlined">hourglass_empty</span><p>Building: ${pct}%</p></div>`;
1003-
await new Promise(r => setTimeout(r, 0));
1004-
}
1095+
// Binary files are intentionally excluded from the generated output (they remain visible in the tree)
1096+
1097+
// Build document sections faster - no UI updates during build
1098+
contents.forEach(({ path, content }) => {
1099+
const full = getFullPath(path);
1100+
parts.push(`=== FILE: ${full} ===\n`);
1101+
parts.push(`<document path="${path}">\n`, content, '\n</document>\n\n');
1102+
});
10051103

10061104
let ctx;
10071105
try {
@@ -1027,7 +1125,12 @@
10271125
D.ed.innerHTML = `<pre style="margin:0;padding:12px;white-space:pre-wrap;word-wrap:break-word;font-size:11px;line-height:1.3;max-height:100%;overflow:auto">${esc(ctx)}</pre>`;
10281126
}
10291127

1030-
toast(`Done! (${bytes(ctx.length)})`, 'success');
1128+
const stats = [];
1129+
stats.push(`${textFiles.length} files`);
1130+
if (binaryFiles.length > 0) stats.push(`${binaryFiles.length} binary excluded`);
1131+
stats.push(`${bytes(ctx.length)}`);
1132+
1133+
toast(`✓ Done! ${stats.join(' • ')}`, 'success');
10311134
} catch (e) {
10321135
console.error('Gen error:', e);
10331136
toast('Failed: ' + e.message, 'error');
@@ -1037,31 +1140,63 @@
10371140
}
10381141
};
10391142

1040-
const genAndDL = async (struct, files) => {
1143+
const genAndDL = async (struct, textFiles, binaryFiles) => {
10411144
const parts = [];
10421145
parts.push('<folder-structure>\n', struct, '</folder-structure>\n\n');
1146+
1147+
// Binary files are intentionally excluded from the generated download (they remain visible in the tree)
1148+
10431149
let done = 0;
1044-
for (let i = 0; i < files.length; i += BATCH) {
1045-
const batch = files.slice(i, i + BATCH);
1046-
for (const f of batch) {
1047-
try {
1048-
const txt = await f.file.text();
1049-
parts.push(`<document path="${f.path}">\n`, txt, '\n</document>\n\n');
1050-
} catch (e) { console.warn('Skip', f.name, e); }
1051-
}
1150+
const FAST_BATCH = 100;
1151+
1152+
for (let i = 0; i < textFiles.length; i += FAST_BATCH) {
1153+
const batch = textFiles.slice(i, i + FAST_BATCH);
1154+
1155+
const results = await Promise.allSettled(
1156+
batch.map(f => f.file.text())
1157+
);
1158+
1159+
results.forEach((result, idx) => {
1160+
if (result.status === 'fulfilled') {
1161+
const p = batch[idx].path;
1162+
parts.push(`=== FILE: ${getFullPath(p)} ===\n`);
1163+
parts.push(`<document path="${p}">\n`, result.value, '\n</document>\n\n');
1164+
} else {
1165+
console.warn('Skip', batch[idx].name, result.reason);
1166+
}
1167+
});
1168+
10521169
done += batch.length;
1053-
const pct = Math.round(done / files.length * 100);
1054-
D.ed.innerHTML = `<div class="editor-placeholder"><span class="material-symbols-outlined">download</span><p>Preparing: ${pct}%</p><small>${done} / ${files.length}</small></div>`;
1055-
await new Promise(r => setTimeout(r, 0));
1170+
const pct = Math.round(done / textFiles.length * 100);
1171+
const status = [];
1172+
status.push(`${done}/${textFiles.length}`);
1173+
if (binaryFiles && binaryFiles.length > 0) {
1174+
status.push(`${binaryFiles.length} binary`);
1175+
}
1176+
1177+
D.ed.innerHTML = `<div class="editor-placeholder"><span class="material-symbols-outlined">download</span><p>Preparing: ${pct}%</p><small>${status.join(' • ')}</small></div>`;
1178+
1179+
if (i % (FAST_BATCH * 5) === 0) {
1180+
await new Promise(r => setTimeout(r, 0));
1181+
}
10561182
}
1183+
10571184
const blob = new Blob(parts, { type: 'text/plain' });
10581185
const url = URL.createObjectURL(blob);
10591186
const a = document.createElement('a');
10601187
a.href = url;
10611188
a.download = `${S.root}-context.txt`;
10621189
a.click();
10631190
URL.revokeObjectURL(url);
1064-
D.ed.innerHTML = `<div class="editor-placeholder"><span class="material-symbols-outlined">check_circle</span><p>Downloaded!</p><small>File: ${S.root}-context.txt</small><small>Size: ${bytes(blob.size)}</small></div>`;
1191+
1192+
const stats = [];
1193+
stats.push(`${textFiles.length} files`);
1194+
if (binaryFiles && binaryFiles.length > 0) {
1195+
stats.push(`${binaryFiles.length} binary`);
1196+
}
1197+
stats.push(bytes(blob.size));
1198+
1199+
D.ed.innerHTML = `<div class="editor-placeholder"><span class="material-symbols-outlined">check_circle</span><p>Downloaded!</p><small>File: ${S.root}-context.txt</small><small>${stats.join(' • ')}</small></div>`;
10651200
};
10661201

10671202
const genStruct = (nodes, pfx = '') => {
@@ -1135,10 +1270,10 @@
11351270
// Optimize file input change handler
11361271
inp.onchange = e => {
11371272
if (!e.target.files.length) return;
1138-
1273+
11391274
// Show loader IMMEDIATELY
11401275
load(true);
1141-
1276+
11421277
// Process files in next tick to let loader render
11431278
setTimeout(() => loadFiles(e.target.files), 0);
11441279
};

0 commit comments

Comments
 (0)