|
93 | 93 | // Executables & Binaries |
94 | 94 | 'exe', 'dll', 'so', 'dylib', 'a', 'o', 'obj', |
95 | 95 |
|
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', |
98 | 98 |
|
99 | 99 | // Python Compiled |
100 | 100 | 'pyc', 'pyo', 'pyd', |
101 | 101 |
|
102 | | - // Images (optional - you can show them) |
103 | | - // 'png', 'jpg', 'jpeg', 'gif', 'svg', 'ico', 'webp', |
104 | | - |
105 | 102 | // Media |
106 | 103 | 'mp4', 'mp3', 'wav', 'avi', 'mov', 'flv', 'wmv', 'ogg', |
107 | 104 |
|
|
111 | 108 | // Fonts |
112 | 109 | 'ttf', 'woff', 'woff2', 'eot', 'otf', |
113 | 110 |
|
114 | | - // Lock files (optional) |
115 | | - // 'lock', // If you want to hide lock files |
116 | | - |
117 | 111 | // Other |
118 | 112 | 'log', 'cache', 'swp', 'swo', 'bak', 'tmp' |
119 | 113 | ]) |
120 | 114 | }; |
121 | 115 |
|
| 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 | + |
122 | 136 |
|
123 | 137 | // VS Code Material Icon Theme - Professional file icons |
124 | 138 | const ICONS = { |
|
330 | 344 | // STATE |
331 | 345 | // ============================================================================ |
332 | 346 |
|
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 }; |
334 | 348 |
|
335 | 349 | const $ = id => document.getElementById(id); |
336 | 350 | const D = { |
|
423 | 437 | return convertHundreds(n); |
424 | 438 | }; |
425 | 439 |
|
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 | + }; |
427 | 457 |
|
| 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 | + }; |
428 | 463 | // Get icon for file or folder |
429 | 464 | const ico = (name, isFolder) => { |
430 | 465 | if (isFolder) { |
|
862 | 897 |
|
863 | 898 | // Fast path: convert FileList to Array |
864 | 899 | const all = Array.from(list); |
865 | | - |
| 900 | + |
866 | 901 | if (loadingText) loadingText.textContent = 'Filtering files...'; |
867 | 902 | await new Promise(r => setTimeout(r, 0)); |
868 | 903 |
|
|
875 | 910 | file: f |
876 | 911 | })); |
877 | 912 |
|
| 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 | + |
878 | 942 | if (S.files.length === 0) { toast('No valid files', 'warning'); load(false); return; } |
879 | 943 | if (S.files.length > 5000) { toast(`Large directory (${S.files.length} files) - rendering first 1000`, 'warning'); } |
880 | 944 |
|
|
938 | 1002 | // CONTEXT GENERATION - STREAMING |
939 | 1003 | // ============================================================================ |
940 | 1004 |
|
941 | | - const gen = async () => { |
| 1005 | + const gen = async () => { |
942 | 1006 | if (S.busy) return; |
943 | 1007 |
|
944 | 1008 | const cbs = Array.from(document.querySelectorAll('.file-checkbox:checked:not([disabled])')); |
945 | 1009 | if (!cbs.length) { toast('Select files first', 'warning'); return; } |
946 | 1010 |
|
947 | 1011 | 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. |
949 | 1015 |
|
950 | 1016 | try { |
951 | 1017 | const paths = cbs.map(c => c.dataset.path); |
952 | 1018 | const files = S.files.filter(f => paths.includes(f.path)); |
953 | 1019 |
|
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); |
955 | 1033 | const isHuge = totalSize > 500 * 1024 * 1024; |
956 | 1034 |
|
957 | 1035 | if (totalSize > 100 * 1024 * 1024) { |
|
962 | 1040 |
|
963 | 1041 | if (isHuge) { |
964 | 1042 | 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); |
966 | 1044 | toast('Ready for download!', 'success'); |
967 | 1045 | return; |
968 | 1046 | } |
969 | 1047 |
|
970 | 1048 | const isLarge = totalSize > 50 * 1024 * 1024; |
971 | 1049 | 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 | + }); |
982 | 1073 |
|
983 | 1074 | 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 | + } |
987 | 1087 | } |
988 | 1088 |
|
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>'; |
990 | 1090 | await new Promise(r => setTimeout(r, 10)); |
991 | 1091 |
|
992 | 1092 | const parts = []; |
993 | 1093 | parts.push('<folder-structure>\n', struct, '</folder-structure>\n\n'); |
994 | 1094 |
|
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 | + }); |
1005 | 1103 |
|
1006 | 1104 | let ctx; |
1007 | 1105 | try { |
|
1027 | 1125 | 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>`; |
1028 | 1126 | } |
1029 | 1127 |
|
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'); |
1031 | 1134 | } catch (e) { |
1032 | 1135 | console.error('Gen error:', e); |
1033 | 1136 | toast('Failed: ' + e.message, 'error'); |
|
1037 | 1140 | } |
1038 | 1141 | }; |
1039 | 1142 |
|
1040 | | - const genAndDL = async (struct, files) => { |
| 1143 | +const genAndDL = async (struct, textFiles, binaryFiles) => { |
1041 | 1144 | const parts = []; |
1042 | 1145 | 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 | + |
1043 | 1149 | 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 | + |
1052 | 1169 | 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 | + } |
1056 | 1182 | } |
| 1183 | + |
1057 | 1184 | const blob = new Blob(parts, { type: 'text/plain' }); |
1058 | 1185 | const url = URL.createObjectURL(blob); |
1059 | 1186 | const a = document.createElement('a'); |
1060 | 1187 | a.href = url; |
1061 | 1188 | a.download = `${S.root}-context.txt`; |
1062 | 1189 | a.click(); |
1063 | 1190 | 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>`; |
1065 | 1200 | }; |
1066 | 1201 |
|
1067 | 1202 | const genStruct = (nodes, pfx = '') => { |
|
1135 | 1270 | // Optimize file input change handler |
1136 | 1271 | inp.onchange = e => { |
1137 | 1272 | if (!e.target.files.length) return; |
1138 | | - |
| 1273 | + |
1139 | 1274 | // Show loader IMMEDIATELY |
1140 | 1275 | load(true); |
1141 | | - |
| 1276 | + |
1142 | 1277 | // Process files in next tick to let loader render |
1143 | 1278 | setTimeout(() => loadFiles(e.target.files), 0); |
1144 | 1279 | }; |
|
0 commit comments