An elegant, zero-dependency userscript that packages directories and codebases for AI chats. Features smart markdown chunking, customizable ignore patterns, binary file uploads, and a premium Liquid Glass interface.
// ==UserScript==
// @name Codebase Uploader
// @namespace http://tampermonkey.net/
// @version 1.1.0
// @author quantavil
// @description An elegant, zero-dependency userscript that packages directories and codebases for AI chats. Features smart markdown chunking, customizable ignore patterns, binary file uploads, and a premium Liquid Glass interface.
// @license MIT
// @homepage https://github.com/quantavil/userscript
// @homepageURL https://github.com/quantavil/userscript
// @match *://*.kimi.com/*
// @match *://*.qwen.ai/*
// @match *://arena.lmsys.org/*
// @match *://*.z.ai/*
// @match *://chatgpt.com/*
// @match *://claude.ai/*
// @match *://gemini.google.com/*
// @match *://aistudio.google.com/*
// @match *://*.deepseek.com/*
// @match *://*.perplexity.ai/*
// @match *://*.grok.com/*
// @match *://chat.mistral.ai/*
// @match *://copilot.microsoft.com/*
// @match *://huggingface.co/chat/*
// @match *://*.groq.com/*
// @match *://openrouter.ai/*
// @match *://*.meta.ai/*
// @match *://*.arena.ai/*
// @match *://aistudio.xiaomimimo.com/*
// @match *://agent.minimax.io/*
// @grant GM_registerMenuCommand
// @run-at document-start
// @noframes
// ==/UserScript==
(function () {
'use strict';
const TOAST_DURATION = 2500;
const TOAST_FADE_MS = 300;
const TREE_INDENT_PX = 20;
const LIMIT_WARNING_THRESHOLD = 0.7;
const CHUNK_OVERHEAD_CHARS = 100;
const REVOCATION_DELAY_MS = 1e4;
const DEFAULT_SETTINGS = {
maxChunks: 10,
maxFileBytes: 2e6,
maxChunkChars: 48e4,
ignoreFolders: "node_modules,__pycache__,dist,build,venv,.next,.nuxt,.idea,.vscode,coverage,.git,out,tmp,temp,.cache,.parcel-cache,vendor,Pods,target,bin,obj,.angular,.svelte-kit",
ignoreExts: ".pyc,.pyo,.log,.lock,.map,.DS_Store,.min.js,.min.css,.exe,.dll,.so,.dylib,.bin,.o,.obj,.class",
skipHidden: true,
includeBinary: false,
customPrompt: "",
shortcutKey: "u"
};
const TEXT_EXTS = /* @__PURE__ */ new Set([
".js",
".mjs",
".cjs",
".ts",
".tsx",
".jsx",
".py",
".rb",
".go",
".rs",
".java",
".kt",
".kts",
".swift",
".c",
".h",
".cpp",
".cc",
".cxx",
".hpp",
".hxx",
".cs",
".php",
".html",
".htm",
".css",
".scss",
".sass",
".less",
".styl",
".json",
".jsonc",
".json5",
".yaml",
".yml",
".toml",
".xml",
".md",
".mdx",
".markdown",
".txt",
".csv",
".tsv",
".sh",
".bash",
".zsh",
".fish",
".ps1",
".bat",
".cmd",
".sql",
".graphql",
".gql",
".vue",
".svelte",
".astro",
".env",
".ini",
".cfg",
".conf",
".config",
".properties",
".r",
".lua",
".pl",
".pm",
".scala",
".clj",
".cljs",
".edn",
".ex",
".exs",
".elm",
".hs",
".lhs",
".ml",
".mli",
".fs",
".fsx",
".fsi",
".dart",
".gradle",
".proto",
".thrift",
".prisma",
".tf",
".tfvars",
".hcl",
".nim",
".cr",
".d",
".zig",
".v",
".sv",
".svh",
".gitignore",
".dockerignore",
".npmignore",
".editorconfig",
".gitattributes",
".gitmodules",
".babelrc",
".stylelintrc",
".rspec",
".nvmrc"
]);
const BINARY_EXTS = /* @__PURE__ */ new Set([
".png",
".jpg",
".jpeg",
".gif",
".webp",
".svg",
".ico",
".bmp",
".tiff",
".tif",
".heic",
".heif",
".avif",
".mp4",
".mp3",
".wav",
".avi",
".mov",
".mkv",
".flv",
".webm",
".ogg",
".oga",
".m4a",
".aac",
".flac",
".zip",
".gz",
".tar",
".tgz",
".rar",
".7z",
".bz2",
".xz",
".lz",
".zst",
".pdf",
".doc",
".docx",
".xls",
".xlsx",
".ppt",
".pptx",
".odt",
".ods",
".odp",
".exe",
".dll",
".so",
".dylib",
".a",
".lib",
".wasm",
".node",
".jar",
".war",
".woff",
".woff2",
".ttf",
".eot",
".otf",
".fon",
".sqlite",
".db",
".sqlite3",
".mdb",
".dbf",
".pickle",
".pkl"
]);
const TEXT_FILENAMES = /* @__PURE__ */ new Set([
"dockerfile",
"makefile",
"justfile",
"rakefile",
"gemfile",
"brewfile",
"procfile",
"vagrantfile",
"license",
"licence",
"readme",
"changelog",
"contributing",
"authors",
"thanks",
"todo",
"notice",
".env",
".eslintrc",
".prettierrc",
".node-version",
".python-version",
".ruby-version"
]);
const SITE_SELECTORS = [
'input[data-testid="file-upload-input"]',
'input[data-testid="upload-file-input"]',
"input.chat-upload__input",
'input[type="file"][accept*="text"]',
'input[type="file"][multiple]',
'input[type="file"]'
];
const STYLESHEET = `
/* ─── Design Tokens ─── */
:host {
all: initial;
position: fixed !important;
top: 0; left: 0; width: 0; height: 0;
z-index: 2147483647 !important;
pointer-events: none;
--glass-bg: rgba(22, 22, 28, 0.78);
--glass-bg-hover: rgba(32, 32, 42, 0.88);
--glass-border: rgba(255, 255, 255, 0.09);
--glass-border-highlight: rgba(255, 255, 255, 0.16);
--glass-blur: 30px;
--glass-saturate: 190%;
--surface-0: rgba(12, 12, 16, 0.88);
--surface-1: rgba(24, 24, 30, 0.8);
--surface-2: rgba(36, 36, 44, 0.65);
--text-primary: #f5f5f9;
--text-secondary: #a8a8b8;
--text-tertiary: #6c6c7c;
--accent: #8FA0FF;
--accent-glow: rgba(143, 160, 255, 0.22);
--accent-strong: #a3b2ff;
--danger: #FF6B6B;
--danger-glow: rgba(255, 107, 107, 0.2);
--success: #63FFB4;
/* Apple colored folder & file accents */
--folder-color: #FFAE19;
--folder-open-color: #FFC107;
--file-color: #5BA2FF;
--bin-color: #3CD070;
--radius-sm: 8px;
--radius-md: 14px;
--radius-lg: 22px;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", system-ui, sans-serif;
--font-mono: "SF Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-family: var(--font-sans);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
svg { pointer-events: none; display: block; flex-shrink: 0; }
/* ─── Overlay ─── */
#cu-overlay {
pointer-events: none;
position: fixed; inset: 0;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(5px) saturate(130%);
-webkit-backdrop-filter: blur(5px) saturate(130%);
z-index: 2147483647;
display: flex; align-items: center; justify-content: center;
font-family: var(--font-sans);
opacity: 0;
transition: opacity 0.3s var(--ease-out);
}
#cu-overlay.open { opacity: 1; pointer-events: auto; }
/* ─── Panel (Liquid Glass) ─── */
#cu-panel {
background: var(--glass-bg);
backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
color: var(--text-primary);
border: 1px solid var(--glass-border);
border-top-color: var(--glass-border-highlight);
border-left-color: var(--glass-border-highlight);
border-radius: var(--radius-lg);
width: min(860px, 94vw);
height: min(82vh, 820px);
display: flex; flex-direction: column;
position: relative; overflow: hidden;
box-shadow:
0 0 0 0.5px rgba(255, 255, 255, 0.05),
0 2px 4px rgba(0, 0, 0, 0.25),
0 16px 44px rgba(0, 0, 0, 0.55),
0 44px 88px rgba(0, 0, 0, 0.35),
inset 0 1px 0 rgba(255, 255, 255, 0.08),
inset 0 0 60px rgba(143, 160, 255, 0.02);
transform: translateY(12px) scale(0.98);
opacity: 0;
transition: transform 0.35s var(--ease-out), opacity 0.3s ease;
}
#cu-overlay.open #cu-panel { transform: translateY(0) scale(1); opacity: 1; }
/* ─── Header ─── */
#cu-header {
padding: 15px 22px;
border-bottom: 1px solid var(--glass-border);
display: flex; align-items: center; gap: 12px;
background: linear-gradient(180deg, rgba(255,255,255,0.04) 0%, transparent 100%);
flex-shrink: 0;
}
#cu-header h3 {
margin: 0; font-size: 16px; font-weight: 600;
color: var(--text-primary);
flex: 1; letter-spacing: 0.2px;
}
.cu-kbd {
font-size: 12px; color: var(--accent-strong);
background: rgba(143, 160, 255, 0.1);
border: 1px solid rgba(143, 160, 255, 0.2);
border-radius: 6px; padding: 4px 10px;
font-family: var(--font-mono);
font-weight: 600;
letter-spacing: 0.8px;
box-shadow: 0 2px 6px rgba(143, 160, 255, 0.08);
}
/* ─── Colored Header Controls ─── */
#cu-close {
color: var(--danger);
background: rgba(255, 107, 107, 0.06);
border: 1px solid rgba(255, 107, 107, 0.12);
}
#cu-close:hover {
background: rgba(255, 107, 107, 0.16);
border-color: rgba(255, 107, 107, 0.3);
color: #FFAAAB;
box-shadow: 0 0 8px var(--danger-glow);
}
#cu-settings-toggle {
color: var(--accent);
background: rgba(143, 160, 255, 0.06);
border: 1px solid rgba(143, 160, 255, 0.12);
}
#cu-settings-toggle:hover {
background: rgba(143, 160, 255, 0.16);
border-color: rgba(143, 160, 255, 0.3);
color: var(--accent-strong);
box-shadow: 0 0 8px var(--accent-glow);
}
.cu-icon-btn {
background: none; border: none;
cursor: pointer; padding: 7px 9px;
border-radius: var(--radius-sm);
transition: all 0.2s var(--ease-out);
display: flex; align-items: center; justify-content: center;
}
.cu-icon-btn:active { transform: scale(0.92); }
/* ─── Toolbar ─── */
#cu-toolbar {
padding: 12px 20px;
display: flex; gap: 8px; align-items: center;
border-bottom: 1px solid var(--glass-border);
flex-shrink: 0;
}
#cu-search {
flex: 1; padding: 9px 13px; border-radius: var(--radius-sm);
border: 1px solid var(--glass-border);
background: var(--surface-0);
color: var(--text-primary);
font-size: 13.5px; font-family: var(--font-sans);
outline: none;
transition: all 0.2s var(--ease-out);
}
#cu-search:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-glow);
background: rgba(6, 6, 10, 0.92);
}
#cu-search::placeholder { color: var(--text-tertiary); }
/* ─── Buttons ─── */
.cu-btn {
padding: 7px 14px; border-radius: var(--radius-sm);
border: 1px solid var(--glass-border);
cursor: pointer; font-weight: 500; font-size: 13px;
background: var(--surface-2); color: var(--text-secondary);
white-space: nowrap; font-family: var(--font-sans);
transition: all 0.2s var(--ease-out);
display: inline-flex; align-items: center; gap: 6px;
}
.cu-btn:hover {
background: var(--glass-bg-hover);
color: var(--text-primary);
border-color: var(--glass-border-highlight);
}
.cu-btn:active { transform: scale(0.96); }
.cu-btn-primary {
background: linear-gradient(135deg, rgba(143,160,255,0.22), rgba(143,160,255,0.12));
color: var(--accent-strong); border-color: rgba(143,160,255,0.24);
box-shadow: 0 2px 8px var(--accent-glow);
}
.cu-btn-primary:hover {
background: linear-gradient(135deg, rgba(143,160,255,0.32), rgba(143,160,255,0.18));
box-shadow: 0 4px 16px var(--accent-glow);
border-color: rgba(143, 160, 255, 0.4);
}
.cu-btn-danger {
color: var(--danger); border-color: rgba(255,107,107,0.18);
background: rgba(255,107,107,0.08);
}
.cu-btn-danger:hover {
background: rgba(255,107,107,0.14);
border-color: rgba(255,107,107,0.35);
}
/* ─── Action Bar ─── */
#cu-actions {
padding: 8px 20px; display: flex; gap: 8px;
border-bottom: 1px solid var(--glass-border);
flex-shrink: 0;
background: rgba(0,0,0,0.12);
}
.cu-action-group {
display: flex; gap: 1px;
background: var(--glass-border);
border-radius: var(--radius-sm);
overflow: hidden;
}
.cu-action-group .cu-btn {
border: none; border-radius: 0;
background: var(--surface-1);
font-size: 12.5px; padding: 6px 12px;
}
.cu-action-group .cu-btn:hover { background: var(--glass-bg-hover); }
.cu-action-group .cu-btn:first-child { border-radius: var(--radius-sm) 0 0 var(--radius-sm); }
.cu-action-group .cu-btn:last-child { border-radius: 0 var(--radius-sm) var(--radius-sm) 0; }
/* ─── Tree Pane ─── */
#cu-tree-pane {
flex: 1; overflow-y: auto; padding: 14px 20px;
font-family: var(--font-mono);
font-size: 13.5px; position: relative;
background: var(--surface-0);
}
#cu-tree-pane.drag-over {
background: rgba(143, 160, 255, 0.03);
outline: 2px dashed rgba(143, 160, 255, 0.35);
outline-offset: -6px;
}
#cu-tree-pane::-webkit-scrollbar { width: 5px; }
#cu-tree-pane::-webkit-scrollbar-track { background: transparent; }
#cu-tree-pane::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.08); border-radius: 3px; }
#cu-tree-pane::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.15); }
/* ─── Dropzone ─── */
#cu-dropzone {
display: none; flex-direction: column; align-items: center; justify-content: center;
gap: 18px; height: 100%; color: var(--text-tertiary);
font-size: 14.5px; text-align: center; padding: 40px 20px;
font-family: var(--font-sans);
}
#cu-tree-pane.cu-empty #cu-dropzone { display: flex; }
#cu-dropzone .cu-drop-icon {
color: var(--accent-strong);
filter: drop-shadow(0 8px 18px var(--accent-glow));
animation: cu-float 3s ease-in-out infinite;
}
@keyframes cu-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-6px); } }
#cu-dropzone strong { color: var(--text-secondary); font-size: 16px; font-weight: 500; }
#cu-dropzone .hint { max-width: 380px; line-height: 1.6; color: var(--text-tertiary); font-size: 13.5px; }
/* ─── Tree Rows ─── */
#cu-tree-list { display: flex; flex-direction: column; gap: 2px; }
.tr {
display: flex; align-items: center; gap: 8px;
padding: 5px 8px; border-radius: var(--radius-sm);
cursor: default;
transition: background 0.15s ease;
}
.tr:hover { background: rgba(255, 255, 255, 0.035); }
.tr input[type=checkbox] {
accent-color: var(--accent);
cursor: pointer; flex-shrink: 0;
width: 15px; height: 15px; outline: none;
}
.tr .caret {
width: 16px; text-align: center; color: var(--text-secondary);
cursor: pointer; flex-shrink: 0;
display: inline-flex; align-items: center; justify-content: center;
transition: color 0.15s, transform 0.1s;
}
.tr .caret:hover { color: var(--text-primary); }
.tr .caret.spacer { visibility: hidden; }
.tr .t-icon {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.tr .t-icon.folder { color: var(--folder-color); }
.tr .t-icon.folderOpen { color: var(--folder-open-color); }
.tr .t-icon.file { color: var(--file-color); }
.tr .t-icon.bin { color: var(--bin-color); }
.tr .t-label {
flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
color: var(--text-secondary); cursor: pointer; font-size: 13.5px;
transition: color 0.12s;
margin-left: 2px;
}
.tr:hover .t-label { color: var(--text-primary); }
.tr .t-label mark {
background: var(--accent-glow); color: var(--accent-strong);
border-radius: 3px; padding: 0 2px;
}
.tr .t-size {
color: var(--text-tertiary); font-size: 11.5px; flex-shrink: 0;
font-family: var(--font-mono);
}
.tr .t-badge {
font-size: 9px; padding: 2px 6px; border-radius: 4px;
background: rgba(255, 255, 255, 0.05); color: var(--text-tertiary);
flex-shrink: 0; text-transform: uppercase; letter-spacing: .5px; font-weight: 600;
}
.tr .t-badge.bin { background: rgba(99, 255, 180, 0.08); color: var(--success); }
.tr .t-remove {
opacity: 0; color: var(--text-secondary); cursor: pointer;
display: flex; align-items: center;
padding: 3px; border-radius: 4px;
transition: all 0.15s;
}
.tr:hover .t-remove { opacity: 0.7; }
.tr .t-remove:hover { opacity: 1; background: rgba(255,107,107,0.12); color: var(--danger); }
.tr-children {
margin-left: ${TREE_INDENT_PX}px;
border-left: 1px solid rgba(255, 255, 255, 0.04);
padding-left: 10px;
display: flex; flex-direction: column; gap: 2px;
}
/* ─── Settings Pane ─── */
#cu-settings-pane {
display: none; flex-direction: column; gap: 14px; padding: 22px;
overflow-y: auto; background: var(--surface-0);
font-family: var(--font-sans);
}
#cu-settings-pane.open { display: flex; flex: 1; }
.cu-setting-section {
font-size: 11px; font-weight: 600; color: var(--accent-strong);
text-transform: uppercase; letter-spacing: 1.5px;
margin-top: 14px; margin-bottom: 4px; padding-bottom: 6px;
border-bottom: 1px solid rgba(255,255,255,0.06);
}
.cu-setting-section:first-child { margin-top: 0; }
/* Grid Layout for inline inputs */
.cu-setting-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.cu-setting-row { display: flex; flex-direction: column; gap: 6px; }
.cu-setting-row.row-cb { flex-direction: row; align-items: center; gap: 10px; }
.cu-setting-row label {
font-size: 13.5px; font-weight: 500; color: var(--text-secondary);
}
.cu-setting-row input[type="text"],
.cu-setting-row input[type="number"],
.cu-setting-row textarea {
padding: 9px 13px; border-radius: var(--radius-sm);
border: 1px solid var(--glass-border);
background: var(--surface-1); color: var(--text-primary);
font-size: 13.5px; font-family: var(--font-mono);
outline: none; transition: all 0.2s;
}
.cu-setting-row textarea {
font-family: var(--font-sans);
resize: vertical; min-height: 80px; line-height: 1.5;
}
/* Hide number input spinners (up/down arrows) */
.cu-setting-row input[type="number"]::-webkit-inner-spin-button,
.cu-setting-row input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
.cu-setting-row input[type="number"] {
-moz-appearance: textfield;
}
.cu-setting-row input:focus,
.cu-setting-row textarea:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-glow);
background: rgba(6, 6, 10, 0.95);
}
.cu-setting-row input[type="checkbox"] {
accent-color: var(--accent);
width: 16px; height: 16px; cursor: pointer;
}
/* ─── Tag Chips ─── */
.cu-tag-editor {
display: flex; flex-direction: column; gap: 8px;
}
.cu-chips {
display: flex; flex-wrap: wrap; gap: 6px;
}
.cu-chip {
display: inline-flex; align-items: center; gap: 6px;
padding: 5px 9px 5px 12px; border-radius: 6px;
background: rgba(143, 160, 255, 0.08);
border: 1px solid rgba(143, 160, 255, 0.18);
color: var(--accent-strong);
font-size: 12.5px; font-family: var(--font-mono);
transition: all 0.15s;
}
.cu-chip:hover { border-color: rgba(143, 160, 255, 0.35); background: rgba(143, 160, 255, 0.12); }
.cu-chip-x {
cursor: pointer; color: var(--text-secondary);
display: flex; align-items: center;
border-radius: 3px; padding: 2px;
transition: all 0.15s;
}
.cu-chip-x:hover { color: var(--danger); background: rgba(255,107,107,0.12); }
.cu-chip-input {
padding: 8px 12px; border-radius: var(--radius-sm);
border: 1px dashed var(--glass-border);
background: transparent;
color: var(--text-primary);
font-size: 13px; font-family: var(--font-mono);
outline: none; width: 100%;
transition: all 0.2s;
}
.cu-chip-input::placeholder { color: var(--text-tertiary); }
.cu-chip-input:focus {
border-style: solid;
border-color: var(--accent);
background: var(--surface-0);
box-shadow: 0 0 0 2px var(--accent-glow);
}
/* ─── Settings Footer ─── */
.cu-settings-footer {
display: flex; justify-content: flex-end;
margin-top: 14px; padding-top: 14px;
border-top: 1px solid rgba(255,255,255,0.06);
}
.cu-reset-btn {
background: none; border: none; color: var(--text-tertiary);
font-size: 12.5px; cursor: pointer; font-family: var(--font-sans);
padding: 5px 10px; border-radius: 6px;
transition: all 0.15s;
}
.cu-reset-btn:hover { color: var(--danger); background: rgba(255,107,107,0.08); }
/* ─── Footer ─── */
#cu-footer {
padding: 14px 22px;
border-top: 1px solid var(--glass-border);
display: flex;
align-items: center;
gap: 16px;
background: rgba(0, 0, 0, 0.15);
flex-shrink: 0;
}
#cu-stats {
flex: 1;
font-size: 13px;
color: var(--text-secondary);
font-family: var(--font-sans);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#cu-chunk-estimate {
font-size: 12.5px;
font-family: var(--font-mono);
color: var(--text-tertiary);
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--glass-border);
border-radius: 6px;
padding: 3px 8px;
font-weight: 500;
}
#cu-chunk-estimate.warn {
color: #FFC107;
background: rgba(255, 193, 7, 0.08);
border-color: rgba(255, 193, 7, 0.18);
}
#cu-chunk-estimate.danger {
color: var(--danger);
background: rgba(255, 107, 107, 0.08);
border-color: rgba(255, 107, 107, 0.18);
}
/* ─── Toast ─── */
#cu-toast {
position: fixed; top: 16px; left: 50%;
transform: translateX(-50%) translateY(-16px);
background: var(--glass-bg);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
padding: 8px 18px;
border-radius: 999px;
border: 1px solid var(--glass-border);
font-size: 13px; font-weight: 500;
box-shadow: 0 8px 24px rgba(0,0,0,0.45);
pointer-events: none; opacity: 0;
transition: all 0.3s var(--ease-out);
z-index: 2147483647;
font-family: var(--font-sans);
color: var(--text-primary);
}
#cu-toast.success {
color: var(--success);
border-color: rgba(99, 255, 180, 0.25);
box-shadow: 0 8px 24px rgba(99, 255, 180, 0.1);
}
#cu-toast.error {
color: var(--danger);
border-color: rgba(255, 107, 107, 0.25);
box-shadow: 0 8px 24px rgba(255, 107, 107, 0.1);
}
#cu-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
/* ─── Reduced Motion ─── */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
`;
const state = {
allFiles: [],
openFolders: /* @__PURE__ */ new Set(),
searchQ: "",
shadowRoot: null
};
function $(id) {
if (!state.shadowRoot) return null;
return state.shadowRoot.getElementById(id);
}
function el(tag, props = {}, children = []) {
const e = document.createElement(tag);
for (const [k, v] of Object.entries(props)) {
if (k === "cls") e.className = v;
else if (k === "txt") e.textContent = v;
else if (k === "id" || k === "title") e[k] = v;
else if (k === "type" || k === "placeholder" || k === "autocomplete" || k === "rows") e[k] = v;
else if (k === "spellcheck") e.spellcheck = v;
else e.setAttribute(k, v);
}
for (const c of children) if (c) e.appendChild(c);
return e;
}
function formatSize(bytes) {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / 1048576).toFixed(2)} MB`;
}
function showToast(msg, type = "success") {
if (!state.shadowRoot) return;
const existing = state.shadowRoot.getElementById("cu-toast");
if (existing) existing.remove();
const toast = el("div", { id: "cu-toast", txt: msg });
toast.classList.add(type);
state.shadowRoot.appendChild(toast);
setTimeout(() => toast.classList.add("show"), 10);
setTimeout(() => {
toast.classList.remove("show");
setTimeout(() => toast.remove(), TOAST_FADE_MS);
}, TOAST_DURATION);
}
let settings = { ...DEFAULT_SETTINGS };
let ignoreFoldersSet = /* @__PURE__ */ new Set();
let ignoreExtsSet = /* @__PURE__ */ new Set();
function updateCachedSettings() {
ignoreFoldersSet = new Set(settings.ignoreFolders.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean));
ignoreExtsSet = new Set(settings.ignoreExts.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean));
}
function loadSettings() {
try {
const raw = localStorage.getItem("cu-settings");
if (raw) {
settings = { ...DEFAULT_SETTINGS, ...JSON.parse(raw) };
} else {
settings = { ...DEFAULT_SETTINGS };
}
} catch (e) {
console.warn("[Codebase Uploader] Failed to load settings:", e);
settings = { ...DEFAULT_SETTINGS };
}
updateCachedSettings();
return settings;
}
function saveSettings() {
try {
localStorage.setItem("cu-settings", JSON.stringify(settings));
} catch (e) {
console.warn("[Codebase Uploader] Failed to save settings:", e);
}
updateCachedSettings();
}
function resetSettings() {
settings = { ...DEFAULT_SETTINGS };
saveSettings();
}
loadSettings();
function isBinaryFile(name) {
const filename = (name || "").split("/").pop().toLowerCase();
const dotIdx = filename.lastIndexOf(".");
const ext = dotIdx > 0 ? filename.slice(dotIdx) : "";
return BINARY_EXTS.has(ext);
}
function shouldSkip(path, size) {
const segs = path.split("/");
if (settings.skipHidden) {
if (segs.slice(0, -1).some((s) => s.startsWith("."))) return true;
const filename = segs[segs.length - 1].toLowerCase();
if (filename.startsWith(".")) {
const dotIdx = filename.lastIndexOf(".");
const ext = dotIdx >= 0 ? filename.slice(dotIdx) : "";
if (!TEXT_FILENAMES.has(filename) && !TEXT_EXTS.has(ext) && !BINARY_EXTS.has(ext)) return true;
}
}
if (segs.some((s) => ignoreFoldersSet.has(s.toLowerCase()))) return true;
const name = segs[segs.length - 1].toLowerCase();
if (ignoreExtsSet.has(name)) return true;
for (const ignoreExt of ignoreExtsSet) {
if (ignoreExt.startsWith(".") && name.endsWith(ignoreExt)) return true;
}
if (size > settings.maxFileBytes) return true;
return false;
}
function ingestFiles(fileObjs) {
const existingPaths = new Set(state.allFiles.map((f) => f.path));
let added = 0, skipped = 0;
const newFiles = [...state.allFiles];
for (const { file, path } of fileObjs) {
if (shouldSkip(path, file.size)) {
skipped++;
continue;
}
if (existingPaths.has(path)) continue;
existingPaths.add(path);
const isBin = isBinaryFile(path);
if (isBin && !settings.includeBinary) {
skipped++;
continue;
}
newFiles.push({ file, path, selected: true, isBinary: isBin });
added++;
}
if (added > 0 || newFiles.length !== state.allFiles.length) {
state.allFiles = newFiles;
}
const statsEl = $("cu-stats");
if (statsEl) {
if (added > 0 && skipped > 0) statsEl.textContent = `Added ${added} file(s), skipped ${skipped}.`;
else if (added > 0) statsEl.textContent = `Added ${added} file(s).`;
else if (skipped > 0) statsEl.textContent = `Skipped ${skipped} file(s).`;
}
}
async function buildChunks(textFiles, binaryFiles = []) {
const chunks = [];
let chunkNum = 1;
let parts = [];
let currentChars = 0;
const flush = () => {
if (!parts.length) return;
chunks.push(new File([`# Codebase Context — Part ${chunkNum}
` + parts.join("")], `codebase_part_${chunkNum}.md`, { type: "text/markdown" }));
chunkNum++;
parts = [];
currentChars = 0;
};
for (const { file, path } of textFiles) {
let content;
try {
content = await file.text();
} catch (e) {
console.warn(`[Codebase Uploader] Failed to read text file ${path}:`, e);
content = `[binary or unreadable — skipped]`;
}
const ext = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
const maxContentSize = Math.max(1e3, settings.maxChunkChars - 300);
let offset = 0;
let partNum = 1;
const isLarge = content.length > maxContentSize;
while (offset < content.length || offset === 0 && content.length === 0) {
const chunkContent = content.slice(offset, offset + maxContentSize);
const displayPath = isLarge ? `${path} (Part ${partNum})` : path;
const block = `## File: \`${displayPath}\`
\`\`\`${ext}
${chunkContent}
\`\`\`
`;
if (parts.length > 0 && currentChars + block.length > settings.maxChunkChars) {
flush();
}
parts.push(block);
currentChars += block.length;
offset += chunkContent.length;
partNum++;
if (chunkContent.length === 0) break;
}
}
flush();
const fileLines = [
...textFiles.map((f) => `- \`${f.path}\``),
...binaryFiles.map((f) => `- \`${f.path}\` (binary)`)
];
const customPromptSection = settings.customPrompt ? `${settings.customPrompt.trim()}
` : "";
const manifest = `# Codebase Manifest
${customPromptSection}- **Total files:** ${textFiles.length + binaryFiles.length}
- **Chunks:** ${chunks.length}
## File list
${fileLines.join("\n")}`;
chunks.unshift(new File([manifest], "codebase_manifest.md", { type: "text/markdown" }));
return chunks;
}
function findInShadows(selectors, root = document) {
for (const sel of selectors) {
const elElement = root.querySelector(sel);
if (elElement instanceof HTMLElement) return elElement;
}
const elements = root.querySelectorAll("*");
for (const elNode of elements) {
if (elNode.shadowRoot) {
const found = findInShadows(selectors, elNode.shadowRoot);
if (found) return found;
}
}
return null;
}
function findFileInput() {
return findInShadows(SITE_SELECTORS);
}
function findChatInput() {
const chatSelectors = [
"#prompt-textarea",
"textarea",
'[contenteditable="true"]',
'input[type="text"]'
];
return findInShadows(chatSelectors);
}
function injectToChat(files) {
const dt = new DataTransfer();
files.forEach((f) => dt.items.add(f));
const fileInput = findFileInput();
if (fileInput) {
try {
fileInput.files = dt.files;
fileInput.dispatchEvent(new Event("change", { bubbles: true }));
fileInput.dispatchEvent(new Event("input", { bubbles: true }));
return true;
} catch (e) {
console.error("[Codebase Uploader] File input injection failed:", e);
}
}
const chatInput = findChatInput();
if (chatInput) {
try {
chatInput.dispatchEvent(new DragEvent("dragenter", { bubbles: true, cancelable: true, dataTransfer: dt }));
chatInput.dispatchEvent(new DragEvent("dragover", { bubbles: true, cancelable: true, dataTransfer: dt }));
const dropEvent = new DragEvent("drop", {
bubbles: true,
cancelable: true,
dataTransfer: dt
});
chatInput.dispatchEvent(dropEvent);
return true;
} catch (e) {
console.error("[Codebase Uploader] Drop event injection failed:", e);
}
}
try {
document.body.dispatchEvent(new DragEvent("dragenter", { bubbles: true, cancelable: true, dataTransfer: dt }));
document.body.dispatchEvent(new DragEvent("dragover", { bubbles: true, cancelable: true, dataTransfer: dt }));
document.body.dispatchEvent(new DragEvent("drop", {
bubbles: true,
cancelable: true,
dataTransfer: dt
}));
return true;
} catch (e) {
console.error("[Codebase Uploader] Body drop event failed:", e);
}
return false;
}
function downloadFiles(files) {
files.forEach((f) => {
const url = URL.createObjectURL(f);
const a = Object.assign(document.createElement("a"), { href: url, download: f.name });
a.click();
setTimeout(() => URL.revokeObjectURL(url), REVOCATION_DELAY_MS);
});
}
function updateStats() {
const statsEl = $("cu-stats");
const chunkEstimate = $("cu-chunk-estimate");
if (!statsEl || !chunkEstimate) return;
const visible = state.allFiles.filter((f) => !shouldSkip(f.path, f.file.size));
const active = visible.filter((f) => f.selected);
const textActive = active.filter((f) => !f.isBinary);
const binActive = active.filter((f) => f.isBinary);
const totalBytes = active.reduce((a, f) => a + f.file.size, 0);
statsEl.textContent = `${active.length}/${visible.length} files · ${textActive.length} text, ${binActive.length} bin · ${formatSize(totalBytes)}`;
if (!active.length) {
chunkEstimate.textContent = "—";
chunkEstimate.className = "";
return;
}
const estChunks = Math.max(1, Math.ceil((textActive.reduce((a, f) => a + f.file.size, 0) + textActive.length * CHUNK_OVERHEAD_CHARS) / settings.maxChunkChars));
const estTotal = estChunks + binActive.length;
chunkEstimate.textContent = `~${estTotal} upload${estTotal !== 1 ? "s" : ""}`;
chunkEstimate.className = estTotal > settings.maxChunks ? "danger" : estTotal > settings.maxChunks * LIMIT_WARNING_THRESHOLD ? "warn" : "";
}
async function run(downloadMode = false) {
const statsEl = $("cu-stats");
const overlay = $("cu-overlay");
const visible = state.allFiles.filter((f) => !shouldSkip(f.path, f.file.size));
const files = visible.filter((f) => f.selected);
if (!files.length) {
showToast("Select at least one file first.", "error");
return;
}
const textFiles = files.filter((f) => !f.isBinary);
const binaryFiles = files.filter((f) => f.isBinary);
if (statsEl) statsEl.textContent = `Building ${textFiles.length} chunks…`;
const chunks = textFiles.length || binaryFiles.length ? await buildChunks(textFiles, binaryFiles) : [];
const rawFiles = binaryFiles.map((f) => f.file);
const allUploads = [...chunks, ...rawFiles];
if (!allUploads.length) {
if (statsEl) statsEl.textContent = "Nothing to upload.";
return;
}
const doDownload = () => {
const mdFiles = allUploads.filter((f) => f.name.endsWith(".md"));
const others = allUploads.filter((f) => !f.name.endsWith(".md"));
const downloads = mdFiles.length ? [new File(mdFiles.flatMap((f, i) => i ? ["\n\n---\n\n", f] : [f]), "codebase_combined.md", { type: "text/markdown" }), ...others] : others;
downloadFiles(downloads);
showToast(`Downloaded ${downloads.length} file(s).`);
if (statsEl) statsEl.textContent = `Downloaded ${downloads.length} file(s).`;
};
if (allUploads.length > settings.maxChunks) {
if (confirm(`${allUploads.length} uploads exceeds limit of ${settings.maxChunks}.
Download combined files instead?`)) {
doDownload();
} else if (statsEl) {
statsEl.textContent = `Too many uploads (${allUploads.length}). Deselect some or raise the limit.`;
}
return;
}
if (downloadMode) {
doDownload();
return;
}
const injected = injectToChat(allUploads);
if (injected) {
if (overlay) overlay.classList.remove("open");
showToast(`Uploaded ${allUploads.length} item(s)!`);
if (statsEl) statsEl.textContent = `Uploaded ${allUploads.length} item(s).`;
} else {
showToast("No chat input found — downloading instead.", "error");
if (statsEl) statsEl.textContent = "No chat input — downloading.";
doDownload();
}
}
const NS = "http://www.w3.org/2000/svg";
function svg(size) {
const s = document.createElementNS(NS, "svg");
s.setAttribute("width", String(size));
s.setAttribute("height", String(size));
s.setAttribute("viewBox", "0 0 24 24");
s.setAttribute("fill", "none");
s.setAttribute("stroke", "currentColor");
s.setAttribute("stroke-width", "1.75");
s.setAttribute("stroke-linecap", "round");
s.setAttribute("stroke-linejoin", "round");
return s;
}
function p(parent, d) {
const el2 = document.createElementNS(NS, "path");
el2.setAttribute("d", d);
parent.appendChild(el2);
}
const PATHS = {
folder: [
"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2z"
],
folderOpen: [
"M6 14l1.45-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2v1"
],
file: [
"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z",
"M14 2v6h6"
],
paperclip: [
"m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"
],
x: [
"M18 6 6 18",
"M6 6l12 12"
],
chevronRight: [
"m9 18 6-6-6-6"
],
chevronDown: [
"m6 9 6 6 6-6"
],
arrowLeft: [
"M19 12H5",
"m12 19-7-7 7-7"
],
plus: [
"M12 5v14",
"M5 12h14"
],
download: [
"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",
"m7 10 5 5 5-5",
"M12 15V3"
],
zap: [
"M13 2 3 14h9l-1 8 10-12h-9l1-8z"
],
search: [
"M11 3a8 8 0 1 0 0 16 8 8 0 0 0 0-16z",
"m21 21-4.35-4.35"
],
settings: [
"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z",
"M9 12a3 3 0 1 0 6 0 3 3 0 1 0-6 0z"
],
upload: [
"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",
"m17 8-5-5-5 5",
"M12 3v12"
]
};
function icon(name, size = 16) {
const s = svg(size);
for (const d of PATHS[name] || []) p(s, d);
return s;
}
let searchMatches = /* @__PURE__ */ new Map();
function buildTree(files) {
const root = { isFolder: true, children: /* @__PURE__ */ new Map(), path: "", name: "" };
for (const item of files) {
const parts = item.path.split("/").filter(Boolean);
let node = root;
let curPath = "";
for (let i = 0; i < parts.length; i++) {
const seg = parts[i];
curPath = curPath ? `${curPath}/${seg}` : seg;
if (i === parts.length - 1) {
node.children.set(seg, { isFolder: false, name: seg, path: curPath, item });
} else {
if (!node.children.has(seg)) {
node.children.set(seg, { isFolder: true, name: seg, path: curPath, children: /* @__PURE__ */ new Map() });
}
node = node.children.get(seg);
}
}
}
return root;
}
function nodeCheckState(node) {
if (!node.isFolder) return node.item.selected ? 1 : 0;
const vals = [...node.children.values()].map(nodeCheckState);
if (!vals.length) return 0;
const sum = vals.reduce((a, b) => a + b, 0);
return sum === vals.length ? 1 : sum === 0 ? 0 : 0.5;
}
function setNodeChecked(node, val) {
if (!node.isFolder) {
node.item.selected = val;
return;
}
for (const c of node.children.values()) setNodeChecked(c, val);
}
function highlightLabel(text) {
if (!state.searchQ) return document.createTextNode(text);
const idx = text.toLowerCase().indexOf(state.searchQ);
if (idx < 0) return document.createTextNode(text);
const span = document.createElement("span");
span.append(text.slice(0, idx));
const mark = document.createElement("mark");
mark.textContent = text.slice(idx, idx + state.searchQ.length);
span.append(mark, text.slice(idx + state.searchQ.length));
return span;
}
function precomputeMatches(node, query) {
if (!node.isFolder) {
const match = node.path.toLowerCase().includes(query);
searchMatches.set(node.path, match);
return match;
}
let anyMatch = node.path.toLowerCase().includes(query);
for (const child of node.children.values()) {
if (precomputeMatches(child, query)) {
anyMatch = true;
}
}
searchMatches.set(node.path, anyMatch);
return anyMatch;
}
function renderTree() {
const treePane = $("cu-tree-pane");
const treeList = $("cu-tree-list");
if (!treePane || !treeList) return;
const scrollTop = treePane.scrollTop;
const visibleFiles = state.allFiles.filter((f) => !shouldSkip(f.path, f.file.size));
if (!visibleFiles.length) {
treePane.classList.add("cu-empty");
treeList.textContent = "";
updateStats();
return;
}
treePane.classList.remove("cu-empty");
const tree = buildTree(visibleFiles);
searchMatches.clear();
if (state.searchQ) {
precomputeMatches(tree, state.searchQ);
}
const frag = document.createDocumentFragment();
renderChildren(tree, frag);
treeList.textContent = "";
treeList.appendChild(frag);
updateStats();
treePane.scrollTop = scrollTop;
}
function renderChildren(node, container) {
const sorted = [...node.children.values()].sort((a, b) => {
if (a.isFolder !== b.isFolder) return a.isFolder ? -1 : 1;
return a.name.localeCompare(b.name);
});
for (const child of sorted) {
if (state.searchQ && searchMatches.get(child.path) === false) continue;
container.appendChild(child.isFolder ? renderFolder(child) : renderFile(child));
}
}
function updateDescendantCheckboxes(cb, checked) {
const row = cb.closest(".tr");
if (!row) return;
const wrap = row.parentElement;
if (!wrap) return;
const childrenWrap = wrap.querySelector(".tr-children");
if (childrenWrap) {
const childCbs = childrenWrap.querySelectorAll('input[type="checkbox"]');
for (const childCb of childCbs) {
childCb.checked = checked;
childCb.indeterminate = false;
}
}
}
function updateAncestorCheckboxes(cb) {
let cur = cb.closest(".tr-children");
while (cur) {
const parentWrap = cur.parentElement;
if (!parentWrap) break;
const parentCb = parentWrap.querySelector('.tr > input[type="checkbox"]');
if (!parentCb) break;
const siblingCbs = Array.from(cur.children).map((child) => child.classList.contains("tr") ? child.querySelector("input") : child.querySelector(".tr > input")).filter(Boolean);
let checkedCount = 0;
let indeterminateCount = 0;
for (const scb of siblingCbs) {
if (scb.checked) checkedCount++;
if (scb.indeterminate) indeterminateCount++;
}
if (checkedCount === siblingCbs.length) {
parentCb.checked = true;
parentCb.indeterminate = false;
} else if (checkedCount === 0 && indeterminateCount === 0) {
parentCb.checked = false;
parentCb.indeterminate = false;
} else {
parentCb.checked = false;
parentCb.indeterminate = true;
}
cur = parentWrap.closest(".tr-children");
}
}
function renderFolder(node) {
const wrap = document.createElement("div");
const hasMatch = state.searchQ && searchMatches.get(node.path) === true;
const isOpen2 = state.openFolders.has(node.path) || !!hasMatch;
const row = document.createElement("div");
row.className = "tr";
const cb = document.createElement("input");
cb.type = "checkbox";
const stateVal = nodeCheckState(node);
cb.checked = stateVal === 1;
cb.indeterminate = stateVal === 0.5;
cb.addEventListener("change", (e) => {
const checked = e.target.checked;
setNodeChecked(node, checked);
updateDescendantCheckboxes(cb, checked);
updateAncestorCheckboxes(cb);
updateStats();
});
const caret = document.createElement("span");
caret.className = "caret";
caret.appendChild(icon(isOpen2 ? "chevronDown" : "chevronRight", 14));
const iconEl = document.createElement("span");
iconEl.className = `t-icon ${isOpen2 ? "folderOpen" : "folder"}`;
iconEl.appendChild(icon(isOpen2 ? "folderOpen" : "folder", 17));
const label = document.createElement("span");
label.className = "t-label";
label.appendChild(highlightLabel(node.name));
const toggle = () => {
if (state.openFolders.has(node.path)) {
state.openFolders.delete(node.path);
} else {
state.openFolders.add(node.path);
}
renderTree();
};
caret.addEventListener("click", toggle);
label.addEventListener("click", toggle);
row.append(cb, caret, iconEl, label);
wrap.appendChild(row);
if (isOpen2) {
const childrenWrap = document.createElement("div");
childrenWrap.className = "tr-children";
renderChildren(node, childrenWrap);
wrap.appendChild(childrenWrap);
}
return wrap;
}
function renderFile(node) {
const row = document.createElement("div");
row.className = "tr";
const cb = document.createElement("input");
cb.type = "checkbox";
cb.checked = node.item.selected;
cb.addEventListener("change", (e) => {
const checked = e.target.checked;
node.item.selected = checked;
updateAncestorCheckboxes(cb);
updateStats();
});
const spacer = document.createElement("span");
spacer.className = "caret spacer";
const iconEl = document.createElement("span");
iconEl.className = `t-icon ${node.item.isBinary ? "bin" : "file"}`;
iconEl.appendChild(icon(node.item.isBinary ? "paperclip" : "file", 16));
const label = document.createElement("span");
label.className = "t-label";
label.appendChild(highlightLabel(node.name));
label.title = node.path;
const size = document.createElement("span");
size.className = "t-size";
size.textContent = formatSize(node.item.file.size);
row.append(cb, spacer, iconEl, label, size);
if (node.item.isBinary) {
const badge = document.createElement("span");
badge.className = "t-badge bin";
badge.textContent = "raw";
row.appendChild(badge);
}
const removeBtn = document.createElement("span");
removeBtn.className = "t-remove";
removeBtn.appendChild(icon("x", 13));
removeBtn.addEventListener("click", (e) => {
e.stopPropagation();
state.allFiles = state.allFiles.filter((f) => f.path !== node.item.path);
renderTree();
});
row.appendChild(removeBtn);
return row;
}
let isOpen = false;
let isSettingsOpen = false;
const isMac = typeof navigator !== "undefined" && (/Mac|iPhone|iPad|iPod/i.test(navigator.platform) || /Mac|iPhone|iPad|iPod/i.test(navigator.userAgent));
const MAX_DRAG_FILES = 5e3;
function debounce(fn, delay) {
let timer = null;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
function openPanel() {
isOpen = true;
const overlay = $("cu-overlay");
if (overlay) {
overlay.classList.add("open");
renderTree();
}
}
function closePanel() {
isOpen = false;
const overlay = $("cu-overlay");
if (overlay) overlay.classList.remove("open");
}
function togglePanel() {
isOpen ? closePanel() : openPanel();
}
function pickFolder() {
const input = document.createElement("input");
input.type = "file";
input.webkitdirectory = true;
input.multiple = true;
input.addEventListener("change", (e) => {
const target = e.target;
if (target.files) {
if (target.files.length > MAX_DRAG_FILES) {
if (!confirm(`You are selecting ${target.files.length} files. This may freeze the browser.
Are you sure you want to proceed?`)) {
return;
}
}
ingestFiles(Array.from(target.files).map((f) => ({ file: f, path: f.webkitRelativePath || f.name })));
renderTree();
}
});
input.click();
}
async function handleDrop(e, treePane) {
e.preventDefault();
treePane.classList.remove("drag-over");
if (!e.dataTransfer) return;
const droppedFiles = [];
let fileCount = 0;
let aborted = false;
async function traverse(entry, prefix = "") {
if (aborted) return;
if (entry.isFile) {
fileCount++;
if (fileCount > MAX_DRAG_FILES) {
aborted = true;
if (confirm(`You are uploading more than ${MAX_DRAG_FILES} files. This might be a mistake (e.g., dropping a root directory or node_modules).
Do you want to cancel the upload?`)) {
droppedFiles.length = 0;
return;
} else {
aborted = false;
}
}
const file = await new Promise((r) => entry.file(r));
if (aborted) return;
droppedFiles.push({ file, path: prefix + file.name });
} else if (entry.isDirectory) {
const reader = entry.createReader();
let entries = [], batch;
do {
batch = await new Promise((r) => reader.readEntries(r));
entries = entries.concat(batch);
} while (batch.length > 0 && !aborted);
for (const child of entries) {
if (aborted) break;
await traverse(child, prefix + entry.name + "/");
}
}
}
await Promise.all(
[...e.dataTransfer.items].filter((i) => i.kind === "file").map((i) => i.webkitGetAsEntry?.()).filter(Boolean).map((entry) => traverse(entry))
);
if (droppedFiles.length > 0) {
ingestFiles(droppedFiles);
renderTree();
}
}
function buildTagEditor(initialValue, onUpdate) {
const container = el("div", { cls: "cu-tag-editor" });
const chips = el("div", { cls: "cu-chips" });
const input = el("input", { cls: "cu-chip-input", type: "text", placeholder: "Add tag + Enter..." });
let tags = initialValue.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
const renderChips = () => {
chips.textContent = "";
tags.forEach((tag) => {
const chip = el("div", { cls: "cu-chip", txt: tag });
const remove = el("span", { cls: "cu-chip-x" });
remove.appendChild(icon("x", 10));
remove.addEventListener("click", () => {
tags = tags.filter((t) => t !== tag);
onUpdate(tags.join(","));
renderChips();
});
chip.appendChild(remove);
chips.appendChild(chip);
});
};
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
const val = input.value.trim().toLowerCase();
if (val && !tags.includes(val)) {
tags.push(val);
onUpdate(tags.join(","));
renderChips();
}
input.value = "";
}
});
renderChips();
container.append(chips, input);
return container;
}
function updateShortcutHint() {
const hint = $("cu-kbd-hint");
if (hint) {
const key = (settings.shortcutKey || "u").toUpperCase();
hint.textContent = `${isMac ? "⌥⇧" : "Alt+Shift+"}${key}`;
}
}
function buildSettingsPane() {
const pane = el("div", { id: "cu-settings-pane" });
pane.appendChild(el("div", { cls: "cu-setting-section", txt: "Limits & Shortcut" }));
const limitRow = el("div", { cls: "cu-setting-row" }, [
el("label", { txt: "Max uploads / chunks" }),
el("input", { id: "cu-set-maxChunks", type: "number", value: String(settings.maxChunks) })
]);
limitRow.querySelector("input")?.addEventListener("change", (e) => {
settings.maxChunks = Number(e.target.value) || settings.maxChunks;
saveSettings();
});
const sizeLabel = el("label", { txt: "Max file size (bytes)" });
const sizeHelper = el("span", { id: "cu-size-helper", txt: ` (${formatSize(settings.maxFileBytes)})`, style: "color: var(--accent-strong); font-size: 11.5px; margin-left: 6px;" });
sizeLabel.appendChild(sizeHelper);
const sizeRow = el("div", { cls: "cu-setting-row" }, [
sizeLabel,
el("input", { id: "cu-set-maxFileBytes", type: "number", value: String(settings.maxFileBytes) })
]);
const sizeInput = sizeRow.querySelector("input");
sizeInput?.addEventListener("input", (e) => {
const bytes = Number(e.target.value) || 0;
sizeHelper.textContent = ` (${formatSize(bytes)})`;
});
sizeInput?.addEventListener("change", (e) => {
settings.maxFileBytes = Number(e.target.value) || settings.maxFileBytes;
saveSettings();
});
const charRow = el("div", { cls: "cu-setting-row" }, [
el("label", { txt: "Max characters per chunk" }),
el("input", { id: "cu-set-maxChunkChars", type: "number", value: String(settings.maxChunkChars) })
]);
charRow.querySelector("input")?.addEventListener("change", (e) => {
settings.maxChunkChars = Number(e.target.value) || settings.maxChunkChars;
saveSettings();
});
const shortcutRow = el("div", { cls: "cu-setting-row" }, [
el("label", { txt: "Hotkey Letter (Alt+Shift+Key)" }),
el("input", { id: "cu-set-shortcutKey", type: "text", value: settings.shortcutKey || "u", maxLength: 1 })
]);
shortcutRow.querySelector("input")?.addEventListener("input", (e) => {
const val = e.target.value.trim().toLowerCase();
settings.shortcutKey = val || "u";
saveSettings();
updateShortcutHint();
});
const limitsGrid1 = el("div", { cls: "cu-setting-grid" }, [limitRow, sizeRow]);
const limitsGrid2 = el("div", { cls: "cu-setting-grid" }, [charRow, shortcutRow]);
pane.append(limitsGrid1, limitsGrid2);
pane.appendChild(el("div", { cls: "cu-setting-section", txt: "Ignored Folders & Extensions" }));
const folderLabel = el("label", { txt: "Ignored folders" });
const folderEditor = buildTagEditor(settings.ignoreFolders, (val) => {
settings.ignoreFolders = val;
saveSettings();
});
const extLabel = el("label", { txt: "Ignored extensions" });
const extEditor = buildTagEditor(settings.ignoreExts, (val) => {
settings.ignoreExts = val;
saveSettings();
});
pane.append(
el("div", { cls: "cu-setting-row" }, [folderLabel, folderEditor]),
el("div", { cls: "cu-setting-row" }, [extLabel, extEditor])
);
pane.appendChild(el("div", { cls: "cu-setting-section", txt: "Inclusion Options" }));
const skipHiddenRow = el("div", { cls: "cu-setting-row row-cb" }, [
el("input", { id: "cu-set-skipHidden", type: "checkbox" }),
el("label", { txt: "Skip hidden files & folders" })
]);
const skipHiddenCb = skipHiddenRow.querySelector("input");
skipHiddenCb.checked = settings.skipHidden;
skipHiddenCb.addEventListener("change", () => {
settings.skipHidden = skipHiddenCb.checked;
saveSettings();
});
const includeBinRow = el("div", { cls: "cu-setting-row row-cb" }, [
el("input", { id: "cu-set-includeBinary", type: "checkbox" }),
el("label", { txt: "Include binary files (images, zip, etc.)" })
]);
const includeBinCb = includeBinRow.querySelector("input");
includeBinCb.checked = settings.includeBinary;
includeBinCb.addEventListener("change", () => {
settings.includeBinary = includeBinCb.checked;
saveSettings();
});
const optionsGrid = el("div", { cls: "cu-setting-grid" }, [skipHiddenRow, includeBinRow]);
pane.appendChild(optionsGrid);
pane.appendChild(el("div", { cls: "cu-setting-section", txt: "Custom Manifest Prompt" }));
const promptRow = el("div", { cls: "cu-setting-row" }, [
el("label", { txt: "Instructions prepended to manifest" }),
el("textarea", { id: "cu-set-customPrompt", placeholder: "e.g. Please analyze this codebase for memory leaks...", rows: 3 })
]);
const promptTextarea = promptRow.querySelector("textarea");
promptTextarea.value = settings.customPrompt || "";
promptTextarea.addEventListener("change", () => {
settings.customPrompt = promptTextarea.value;
saveSettings();
});
pane.appendChild(promptRow);
const resetBtn = el("button", { cls: "cu-reset-btn", txt: "Reset to Defaults" });
resetBtn.addEventListener("click", () => {
if (confirm("Are you sure you want to reset all settings to defaults?")) {
resetSettings();
updateShortcutHint();
const parent = pane.parentElement;
if (parent) {
pane.remove();
const newPane = buildSettingsPane();
newPane.classList.add("open");
parent.insertBefore(newPane, $("cu-footer"));
}
showToast("Settings reset to defaults.");
}
});
pane.appendChild(el("div", { cls: "cu-settings-footer" }, [resetBtn]));
return pane;
}
function toggleSettings() {
const treePane = $("cu-tree-pane");
const toolbar = $("cu-toolbar");
const actions = $("cu-actions");
const settingsToggle = $("cu-settings-toggle");
if (!treePane || !settingsToggle || !toolbar || !actions) return;
const shadow = state.shadowRoot;
if (!shadow) return;
let settingsPane = $("cu-settings-pane");
isSettingsOpen = !isSettingsOpen;
if (isSettingsOpen) {
if (!settingsPane) {
settingsPane = buildSettingsPane();
const footer = $("cu-footer");
if (footer) footer.parentElement?.insertBefore(settingsPane, footer);
}
treePane.style.display = "none";
toolbar.style.display = "none";
actions.style.display = "none";
settingsPane.classList.add("open");
settingsToggle.textContent = "";
settingsToggle.appendChild(icon("arrowLeft", 16));
settingsToggle.title = "Back to Files";
} else {
treePane.style.display = "block";
toolbar.style.display = "flex";
actions.style.display = "flex";
if (settingsPane) settingsPane.classList.remove("open");
settingsToggle.textContent = "";
settingsToggle.appendChild(icon("settings", 16));
settingsToggle.title = "Settings";
renderTree();
}
}
function buildUI() {
if (document.getElementById("codebase-uploader-root")) return;
const $host = document.createElement("div");
$host.id = "codebase-uploader-root";
$host.style.cssText = "all:initial;position:fixed!important;top:0;left:0;width:0;height:0;z-index:2147483647!important;pointer-events:none;";
const shadow = $host.attachShadow({ mode: "open" });
state.shadowRoot = shadow;
const style = document.createElement("style");
style.textContent = STYLESHEET;
shadow.appendChild(style);
const closeBtn = el("button", { cls: "cu-icon-btn", id: "cu-close", title: "Close (Esc)" });
closeBtn.appendChild(icon("x", 16));
const settingsBtn = el("button", { cls: "cu-icon-btn", id: "cu-settings-toggle", title: "Settings" });
settingsBtn.appendChild(icon("settings", 16));
const kbdHint = el("span", { id: "cu-kbd-hint", cls: "cu-kbd", txt: `${isMac ? "⌥⇧" : "Alt+Shift+"}U` });
const header = el("div", { id: "cu-header" }, [
el("h3", { txt: "Codebase Uploader" }),
kbdHint,
settingsBtn,
closeBtn
]);
const searchInput = el("input", { id: "cu-search", type: "text", placeholder: "Filter files…", autocomplete: "off", spellcheck: false });
const addFolderBtn = el("button", { cls: "cu-btn", id: "cu-add-folder", txt: " Folder" });
addFolderBtn.insertBefore(icon("plus", 14), addFolderBtn.firstChild);
const toolbar = el("div", { id: "cu-toolbar" }, [searchInput, addFolderBtn]);
const selAll = el("button", { cls: "cu-btn", txt: "All" });
const selNone = el("button", { cls: "cu-btn", txt: "None" });
const selectionGroup = el("div", { cls: "cu-action-group" }, [selAll, selNone]);
const expandAll = el("button", { cls: "cu-btn", txt: "Expand" });
const collapseAll = el("button", { cls: "cu-btn", txt: "Collapse" });
const viewGroup = el("div", { cls: "cu-action-group" }, [expandAll, collapseAll]);
const clearBtn = el("button", { cls: "cu-btn cu-btn-danger", id: "cu-clear", txt: "Clear" });
const actions = el("div", { id: "cu-actions" }, [selectionGroup, viewGroup, clearBtn]);
const dropzoneBtn = el("button", { cls: "cu-btn cu-btn-primary", txt: "Choose Folder" });
const dropIcon = icon("folderOpen", 48);
dropIcon.setAttribute("class", "cu-drop-icon");
const dropzone = el("div", { id: "cu-dropzone" }, [
dropIcon,
el("strong", { txt: "Drop a folder or click below" }),
el("div", { cls: "hint", txt: "Text → markdown chunks · Binary → raw attachments" }),
dropzoneBtn
]);
const treeList = el("div", { id: "cu-tree-list" });
const treePane = el("div", { id: "cu-tree-pane", cls: "cu-empty" }, [dropzone, treeList]);
const stats = el("div", { id: "cu-stats", txt: "No files loaded." });
const chunkEstimate = el("div", { id: "cu-chunk-estimate", txt: "—" });
const downloadBtn = el("button", { cls: "cu-btn", id: "cu-download-btn", txt: " Download" });
downloadBtn.insertBefore(icon("download", 14), downloadBtn.firstChild);
const uploadBtn = el("button", { cls: "cu-btn cu-btn-primary", id: "cu-upload-btn", txt: " Upload" });
uploadBtn.insertBefore(icon("zap", 14), uploadBtn.firstChild);
const footer = el("div", { id: "cu-footer" }, [stats, chunkEstimate, downloadBtn, uploadBtn]);
const panel = el("div", { id: "cu-panel" }, [header, toolbar, actions, treePane, footer]);
const overlay = el("div", { id: "cu-overlay", role: "dialog", "aria-modal": "true" }, [panel]);
shadow.appendChild(overlay);
document.documentElement.appendChild($host);
closeBtn.addEventListener("click", closePanel);
overlay.addEventListener("click", (e) => {
if (e.target === overlay) closePanel();
});
let clearTimer = null;
clearBtn.addEventListener("click", () => {
if (clearBtn.textContent === "Clear") {
clearBtn.textContent = "Confirm?";
clearTimer = setTimeout(() => {
clearBtn.textContent = "Clear";
}, 2500);
} else {
clearTimeout(clearTimer);
clearBtn.textContent = "Clear";
state.allFiles = [];
renderTree();
showToast("Cleared.");
}
});
settingsBtn.addEventListener("click", toggleSettings);
addFolderBtn.addEventListener("click", pickFolder);
dropzoneBtn.addEventListener("click", pickFolder);
const onSearchInput = debounce(() => {
state.searchQ = searchInput.value.trim().toLowerCase();
renderTree();
}, 150);
searchInput.addEventListener("input", onSearchInput);
selAll.addEventListener("click", () => {
state.allFiles.forEach((f) => f.selected = true);
renderTree();
});
selNone.addEventListener("click", () => {
state.allFiles.forEach((f) => f.selected = false);
renderTree();
});
expandAll.addEventListener("click", () => {
state.allFiles.forEach((f) => {
const parts = f.path.split("/").slice(0, -1);
let current = "";
for (const part of parts) {
current = current ? `${current}/${part}` : part;
state.openFolders.add(current);
}
});
renderTree();
});
collapseAll.addEventListener("click", () => {
state.openFolders.clear();
renderTree();
});
uploadBtn.addEventListener("click", () => run(false));
downloadBtn.addEventListener("click", () => run(true));
treePane.addEventListener("dragover", (e) => {
e.preventDefault();
treePane.classList.add("drag-over");
});
treePane.addEventListener("dragleave", () => treePane.classList.remove("drag-over"));
treePane.addEventListener("drop", (e) => handleDrop(e, treePane));
if (isOpen) openPanel();
updateShortcutHint();
new MutationObserver(() => {
if (!document.getElementById("codebase-uploader-root")) buildUI();
}).observe(document.documentElement, { childList: true });
}
buildUI();
window.addEventListener("keydown", (e) => {
if (e.key === "Escape" && isOpen) {
const active = state.shadowRoot?.activeElement || document.activeElement;
if (active && (active.tagName === "INPUT" || active.tagName === "TEXTAREA")) {
active.blur();
e.preventDefault();
return;
}
closePanel();
e.preventDefault();
}
const targetKey = (settings.shortcutKey || "u").toLowerCase();
if (e.altKey && e.shiftKey && e.key.toLowerCase() === targetKey) {
togglePanel();
e.preventDefault();
}
});
GM_registerMenuCommand("📂 Toggle Codebase Uploader", togglePanel);
})();