Skip to content

Instantly share code, notes, and snippets.

@minanagehsalalma
Created August 8, 2025 19:51
Show Gist options
  • Select an option

  • Save minanagehsalalma/c5894be53240fd05fe34d1d94704162a to your computer and use it in GitHub Desktop.

Select an option

Save minanagehsalalma/c5894be53240fd05fe34d1d94704162a to your computer and use it in GitHub Desktop.
(async () => {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// ---------- Deep selectors (shadow roots + same-origin iframes) ----------
const deepAll = (sel) => {
const out = [];
const visit = (root) => {
if (!root) return;
out.push(...root.querySelectorAll(sel));
for (const el of root.querySelectorAll('*')) if (el.shadowRoot) visit(el.shadowRoot);
};
visit(document);
for (const f of document.querySelectorAll('iframe')) {
try { if (f.contentDocument) visit(f.contentDocument); } catch {}
}
return out;
};
// ---------- Best-way reader: CodeMirror API, else Monaco, else DOM -------
const readFromCodeMirrorAPI = () => {
for (const el of deepAll('.cm-editor')) {
const view = el.cmView || el.view;
if (view?.state?.doc) {
try { return view.state.doc.toString(); } catch {}
}
}
return '';
};
const readFromMonacoAPI = () => {
try {
const models = window.monaco?.editor?.getModels?.() || [];
if (!models.length) return '';
// Choose the model with the largest text (usually the active one)
const m = models.reduce((a,b)=> (a?.getValue().length||0) > (b?.getValue().length||0) ? a : b);
return m?.getValue() || '';
} catch { return ''; }
};
const readVisibleText = () => {
// fallback DOM text
const monaco = deepAll('.view-lines')[0];
if (monaco) {
return [...monaco.querySelectorAll('.view-line')].map(el => (el.textContent||'').replace(/\u00A0/g,' ')).join('\n');
}
const cm = deepAll('.cm-content')[0];
if (cm) {
return [...cm.querySelectorAll('.cm-line')].map(el => el.textContent||'').join('\n');
}
let best = '';
deepAll('pre, code').forEach(n => { const t = (n.innerText||n.textContent||''); if (t.length>best.length) best = t; });
return best;
};
// Scroller helper (for virtualization forcing; API path usually avoids this)
const getScroller = () =>
deepAll('.monaco-scrollable-element')[0]?.parentElement ||
deepAll('.cm-scroller')[0] ||
deepAll('.overflow-guard, [class*="scroll"]')[0];
// Smarter merge that avoids duplicated “file starts over” issues
function smartMerge(acc, chunk) {
if (!acc) return chunk || '';
if (!chunk) return acc;
acc = acc.replace(/\u00A0/g,' ');
chunk = chunk.replace(/\u00A0/g,' ');
if (chunk.includes(acc)) return chunk; // chunk is superset
if (acc.includes(chunk)) return acc; // chunk adds nothing
// Try to find any suffix of acc inside chunk (not just at start)
const window = acc.slice(-4000);
for (let len = Math.min(2000, window.length, chunk.length); len >= 200; len -= 50) {
const needle = window.slice(-len);
const idx = chunk.indexOf(needle);
if (idx !== -1) return acc + chunk.slice(idx + len);
}
// If chunk looks like it restarted from top and is large, prefer the longer
if (chunk.slice(0, 1000) === acc.slice(0, 1000) && chunk.length > acc.length * 0.9) {
return chunk;
}
// Last resort: append with a newline (better than interleaving)
return acc + (acc.endsWith('\n') ? '' : '\n') + chunk;
}
// Read entire open file, preferring APIs; scroll only if needed
async function readWholeFile() {
let txt = readFromCodeMirrorAPI() || readFromMonacoAPI();
if (txt && txt.length > 0) return txt;
// DOM fallback with scrolling
const scroller = getScroller();
if (!scroller) return readVisibleText();
scroller.scrollTop = 0; await sleep(120);
let acc = '', lastLen = -1, guard = 0;
const step = Math.max(40, Math.floor(scroller.clientHeight * 0.7));
const endBy = performance.now() + 15000;
while (performance.now() < endBy && guard++ < 1000) {
const chunk = readVisibleText();
acc = smartMerge(acc, chunk);
if (acc.length === lastLen && scroller.scrollTop + scroller.clientHeight + 2 >= scroller.scrollHeight) break;
lastLen = acc.length;
const next = Math.min(scroller.scrollTop + step, scroller.scrollHeight);
scroller.scrollTo(0, next);
await sleep(70);
}
// One last capture at bottom and then back to top to fill head
acc = smartMerge(acc, readVisibleText());
scroller.scrollTop = 0; await sleep(120);
acc = smartMerge(readVisibleText(), acc);
return acc.trimEnd();
}
// Optional post-process: if file is JSON and looks like two JSONs glued,
// trim to the first well-balanced root object.
function fixJSONIfNeeded(path, text) {
if (!/\.json$/i.test(path)) return text;
// quick thumbs rule: if it parses, keep; else try to trim to first balanced {}
try { JSON.parse(text); return text; } catch {}
let depth = 0, inStr = false, esc = false, end = -1;
for (let i = 0; i < text.length; i++) {
const ch = text[i];
if (inStr) {
if (esc) esc = false;
else if (ch === '\\') esc = true;
else if (ch === '"') inStr = false;
} else {
if (ch === '"') inStr = true;
else if (ch === '{') depth++;
else if (ch === '}') { depth--; if (depth === 0) { end = i + 1; break; } }
}
}
if (end > 0) {
const trimmed = text.slice(0, end);
try { JSON.parse(trimmed); return trimmed; } catch {}
}
return text; // give up
}
// ----------------- File tree crawler (don’t skip folders) -----------------
const panel = document.querySelector('[id*="content-files"]') || document.body;
const rowSel = '[role="button"].group';
const nameFromRow = r => r.querySelector('span.flex-1')?.textContent?.trim() || r.innerText.trim();
const isDir = r => !!r.querySelector('[title="Reference directory in chat"]') ||
/rotate/.test(r.querySelector('svg')?.getAttribute('class') || '');
const levelOf = r => {
const ind = r.querySelector('[style*="margin-left"]');
const px = ind ? parseInt((ind.style.marginLeft || '0'), 10) : 0;
return Math.round(px / 16);
};
async function expandEverything() {
let seenCount = -1, tries = 0;
while (tries++ < 20) {
// open all closed dirs currently visible
for (const r of panel.querySelectorAll(rowSel)) {
if (!isDir(r)) continue;
const svg = r.querySelector('svg');
const open = /rotate-90/.test(svg?.getAttribute('class') || '');
if (!open) { r.click(); await sleep(120); }
}
// scroll down a "page"
const before = panel.scrollTop;
panel.scrollTop += Math.max(100, panel.clientHeight - 60);
await sleep(180);
const count = panel.querySelectorAll(rowSel).length;
if (count === seenCount && panel.scrollTop === before) break; // stable
seenCount = count;
}
// sweep back to top once more
panel.scrollTop = 0; await sleep(200);
for (const r of panel.querySelectorAll(rowSel)) {
if (isDir(r)) {
const svg = r.querySelector('svg'); const open = /rotate-90/.test(svg?.getAttribute('class') || '');
if (!open) { r.click(); await sleep(120); }
}
}
}
function listFiles() {
const rows = [...panel.querySelectorAll(rowSel)];
const files = []; const stack = [];
for (const row of rows) {
const lvl = levelOf(row);
while (stack.length && stack[stack.length - 1].level >= lvl) stack.pop();
const name = nameFromRow(row);
if (isDir(row)) { stack.push({ name, level: lvl }); continue; }
const path = [...stack.map(s => s.name), name].join('/');
files.push({ row, name, path });
}
return files;
}
// ----------------------------- RUN ---------------------------------------
await expandEverything();
const files = listFiles();
console.log('Files discovered:', files.length, files.map(f => f.path));
const outputs = {};
for (const f of files) {
f.row.click(); await sleep(250);
let txt = await readWholeFile();
txt = fixJSONIfNeeded(f.path, txt);
outputs[f.path] = txt || '';
console.log('✔', f.path, outputs[f.path].length, 'chars');
}
// ----------------------- Download as ZIP ----------------------------------
const JSZip = (await import('https://cdn.jsdelivr.net/npm/[email protected]/+esm')).default;
const zip = new JSZip();
Object.entries(outputs).forEach(([p, c]) => zip.file(p, c ?? ''));
const blob = await zip.generateAsync({ type: 'blob' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = (document.title.replace(/\s+/g, '_') || 'project') + '.zip';
document.body.append(a); a.click(); a.remove();
const suspicious = Object.entries(outputs).filter(([,c]) => !c || c.length < 50).map(([p]) => p);
if (suspicious.length) console.warn('Potentially incomplete:', suspicious);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment