Codebase Uploader

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.

目前為 2026-06-27 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

Advertisement:

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

Advertisement:

// ==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);

})();