// ==UserScript== // @name AI Chat Turbo Engine // @namespace https://local/ai-turbo-engine-v9 // @version 0.13.3 // @description 跨平台 AI 对话加速:常驻统计 + 完整消息提取 + YAML 交接 + 多平台适配 + 长代码折叠 + 自适应脱水 // @author Yoko // @match https://chatgpt.com/* // @match https://chat.openai.com/* // @match https://gemini.google.com/* // @match https://grok.com/* // @match https://x.com/i/grok* // @match https://claude.ai/* // @match https://chat.deepseek.com/* // @match https://copilot.microsoft.com/* // @match https://perplexity.ai/* // @match https://www.perplexity.ai/* // @match https://poe.com/* // @match https://doubao.com/* // @match https://www.doubao.com/* // @match https://yuanbao.tencent.com/* // @match https://chat.qwen.ai/* // @match https://qianwen.com/* // @match https://www.qianwen.com/* // @match https://chatglm.cn/* // @match https://chat.z.ai/* // @run-at document-start // @grant none // @noframes // @icon data:image/svg+xml;utf8, // @license MIT // ==/UserScript== (function () { "use strict"; const VERSION = "0.13.3"; const HUD_ID = "ai-turbo-extreme-hud"; const STYLE_ID = "ai-turbo-engine-styles"; const CODE_COLLAPSE_HEIGHT = 300; const CODE_PREVIEW_HEIGHT = 200; const LOAD_INDICATOR_FULL_TURNS = 60; const LOAD_RING_RADIUS = 16.2; const LOAD_RING_CIRCUMFERENCE = 2 * Math.PI * LOAD_RING_RADIUS; const TURN_MARKER_ATTRIBUTES = new Set([ "data-testid", "data-turn", "data-turn-key", "data-message-author-role", "data-message-role", "data-conversation-role", "data-role", "data-content-search-unit-key", "data-content-search-turn-key", "data-thread-find-target", "data-conversation-transcript", "data-user-message-bubble", "data-chatgpt-agent-turn-start", "data-app-shell-main-surface", ]); const TURN_SELECTOR_ATTRIBUTES = new Set([ ...TURN_MARKER_ATTRIBUTES, "class", "id", ]); const CHATGPT_SHELL_ROOT_SELECTOR = '[data-app-shell-main-surface] [data-thread-find-target="conversation"]'; const CHATGPT_SHELL_TURN_SELECTOR = '[data-app-shell-main-surface] [data-thread-find-target="conversation"] [data-turn-key]'; const CHATGPT_TRANSCRIPT_ROOT_SELECTOR = "ol[data-conversation-transcript]"; const CHATGPT_TRANSCRIPT_MESSAGE_SELECTOR = 'ol[data-conversation-transcript] > li[data-message-role]'; const CHATGPT_CODE_CARD_BOUNDARY_SELECTOR = [ "[data-turn-key]", '[data-testid^="conversation-turn-"]', "article[data-turn]", "section[data-turn]", 'li[data-message-role="assistant"]', '[data-message-author-role="assistant"]', ].join(", "); const CHATGPT_CODE_HEADER_MARKER_SELECTOR = [ ".code-block-decoration", '[data-testid*="code-block-header" i]', '[data-testid*="code-block-toolbar" i]', '[class*="code-block-header" i]', '[class*="code-header" i]', '[class*="code-toolbar" i]', 'button[aria-label*="copy code" i]', 'button[title*="copy code" i]', '[data-testid*="copy-code" i]', 'button[aria-label*="复制代码"]', 'button[title*="复制代码"]', ].join(", "); const CHATGPT_CODE_CARD_MAX_ANCESTORS = 10; const SEARCH_PREVIEW_LIMIT = 180; const SEARCH_LIMIT = 10; const STORAGE_KEY_POS = "ai_turbo_pill_pos"; const STORAGE_KEY_CONFIG = "ai_turbo_engine_config"; const VIRTUAL_FILE_THRESHOLD = 1500; const STREAMING_IDLE_DELAY = 1500; const THROTTLE_INTERVAL = 80; const CODE_FENCE = "\x60\x60\x60"; const VF_TOKEN_START = "[[TURBO_VFILE_START:"; const VF_TOKEN_END = ":TURBO_VFILE_END]]"; const VF_REGEX = /\[\[TURBO_VFILE_START:([^|\r\n]+)\|(\d+)\|(\d+):TURBO_VFILE_END\]\]/; const HANDOFF_STORAGE_KEY = "ai_turbo_handoff_restore"; const REASONING_SELECTOR = [ "model-thoughts", ".thoughts-container", ".thoughts-content", "[data-test-id*='thought' i]", "[data-testid*='thought' i]", "[data-testid*='reasoning' i]", "[data-testid*='thinking' i]", "[class*='reasoning' i]", "[class*='thinking' i]", "[class*='thought-process' i]", "[aria-label*='reasoning' i]", "[aria-label*='thinking' i]", "[aria-label*='思考']", "[aria-label*='推理']", "[data-type='reasoning']", ].join(", "); const BLOCK_TEXT_TAGS = new Set([ "ADDRESS", "ARTICLE", "ASIDE", "BLOCKQUOTE", "DD", "DIV", "DL", "DT", "FIELDSET", "FIGCAPTION", "FIGURE", "FOOTER", "H1", "H2", "H3", "H4", "H5", "H6", "HEADER", "HR", "LI", "MAIN", "NAV", "OL", "P", "SECTION", "TABLE", "TBODY", "TD", "TH", "THEAD", "TR", "UL", ]); const PLATFORMS = { chatgpt: { name: "ChatGPT", turnSelectorGroups: [ [CHATGPT_SHELL_TURN_SELECTOR], [CHATGPT_TRANSCRIPT_MESSAGE_SELECTOR], [ '[data-testid^="conversation-turn-"]', "article[data-turn]", "section[data-turn]", ], [ '[data-message-author-role="user"]', '[data-message-author-role="assistant"]', '[data-message-role="user"]', '[data-message-role="assistant"]', '[data-role="user"]', '[data-role="assistant"]', ], [ '[data-content-search-unit-key$=":user"]', '[data-content-search-unit-key$=":assistant"]', ], ["[data-turn-key]"], ], turnSelectors: [ CHATGPT_SHELL_TURN_SELECTOR, CHATGPT_TRANSCRIPT_MESSAGE_SELECTOR, '[data-testid^="conversation-turn-"]', "article[data-turn]", "section[data-turn]", '[data-message-author-role="user"]', '[data-message-author-role="assistant"]', '[data-message-role="user"]', '[data-message-role="assistant"]', '[data-role="user"]', '[data-role="assistant"]', '[data-content-search-unit-key$=":user"]', '[data-content-search-unit-key$=":assistant"]', "[data-turn-key]", ], userTurnSelectors: [ '[data-message-author-role="user"]', '[data-role="user"]', '[data-content-search-unit-key$=":user"]', '[data-user-message-bubble="true"]', '[data-testid="collapsible-user-message-root"]', '[data-testid*="user-message"]', '[class*="user-message"]', ], userContentSelectors: [ '[data-testid="collapsible-user-message-root"]', ".whitespace-pre-wrap", '[data-user-message-bubble="true"]', '[data-content-search-unit-key$=":user"]', '[data-message-author-role="user"]', ], codeSelectors: ["pre"], contentSelectors: [ ".markdown", ".prose", '[class*="markdown"]', ".whitespace-pre-wrap", '[data-content-search-unit-key$=":assistant"]', '[data-message-author-role="assistant"]', '[data-testid="message-content"]', ], inputSelectors: [ 'form[data-chatgpt-composer] [contenteditable="true"][role="textbox"]', 'form[data-chatgpt-composer] [contenteditable="true"]', '[data-chatgpt-composer] [contenteditable="true"]', 'div.ProseMirror[contenteditable="true"][role="textbox"]', "#prompt-textarea", 'textarea[name="prompt-textarea"]', 'div[contenteditable="true"][role="textbox"]', 'div[contenteditable="true"]', "textarea", ], sendButtonSelectors: [ 'form[data-chatgpt-composer] button[type="submit"]', 'form[data-chatgpt-composer] button[data-testid*="send"]', 'form[data-chatgpt-composer] button[aria-label*="Send"]', 'form[data-chatgpt-composer] button[aria-label*="发送"]', 'button[data-testid="send-button"]', 'button[data-testid="fruitjuice-send-button"]', 'button[aria-label*="Send"]', 'button[aria-label*="发送"]', 'button[type="submit"]', ], }, gemini: { name: "Gemini", turnSelectors: [ "user-query", "model-response", '[data-test-id="user-query"]', '[data-test-id="model-response"]', ".user-query", ".model-response", ".user-query-container", ".model-response-container", ], codeSelectors: ["code-block", "pre"], contentSelectors: [ ".query-text", ".user-query-text", "[class*='query-content']", ".model-response-text", "message-content", ".message-content", ".markdown", ".md-content", ".response-content", ], inputSelectors: [ "rich-textarea .ql-editor", "rich-textarea div[contenteditable='true']", ".rich-textarea div[contenteditable='true']", "div[contenteditable='true'][role='textbox']", "div[contenteditable='true']", "textarea", ], sendButtonSelectors: [ "button.send-button", 'button[aria-label*="Send message"]', 'button[aria-label*="发送消息"]', 'button[aria-label*="Send"]', 'button[aria-label*="发送"]', 'button[aria-label*="提交"]', ".send-button-container button", "rich-textarea + * button", ], }, grok: { name: "Grok", turnSelectors: [ 'div[data-testid*="message-container"]', 'div[data-testid*="conversation-turn"]', 'div[class*="message-bubble"]', ], codeSelectors: ["pre"], contentSelectors: ['div[class*="message-content"]', 'div[dir="auto"]'], inputSelectors: ["textarea", 'div[contenteditable="true"]'], sendButtonSelectors: [ 'button[aria-label*="Grok"]', 'button[type="submit"]', 'button[aria-label*="Send"]', 'button[aria-label*="发送"]', ], }, claude: { name: "Claude", turnSelectors: [ "div.font-user-message", ".font-claude-response", ".font-claude-message", "div[data-test-render-count]", ], codeSelectors: ["pre"], contentSelectors: [ ".standard-markdown", ".progressive-markdown", ".markdown", '[class*="font-claude"]', ], inputSelectors: [ "div.ProseMirror[contenteditable='true']", 'div[contenteditable="true"]', ], sendButtonSelectors: [ 'button[aria-label*="Send Message"]', 'button[aria-label*="发送消息"]', 'button[aria-label*="Send"]', 'button[aria-label*="发送"]', "fieldset button:last-child", ], }, deepseek: { name: "DeepSeek", turnSelectors: [ ".ds-message", "div[data-um-id]", "div.dad65929", "div._4f9bf79", "div.d29f3d7d", '[data-message-author-role="user"]', '[data-message-author-role="assistant"]', '[data-role="user"]', '[data-role="assistant"]', ], userTurnSelectors: [ '[data-message-author-role="user"]', '[data-role="user"]', "div[data-um-id]", "div.d29f3d7d", '[data-testid*="user-message"]', '[class*="user-message"]', ], userContentSelectors: [ "._9663006 .fbb737a4", ".ds-markdown", "div[data-um-id]", '[data-testid*="user-message"]', '[class*="user-message"]', ], codeSelectors: ["pre", "code-block"], contentSelectors: [ ".ds-markdown", ".markdown", ".prose", '[class*="markdown"]', '[class*="answer"]', ], inputSelectors: [ "#chat-input", 'textarea[data-testid="chat-input"]', 'textarea[placeholder*="Send a message" i]', 'textarea[placeholder]', 'div[contenteditable="true"][role="textbox"]', "textarea", '[contenteditable="true"]', ], sendButtonSelectors: [ 'button[data-testid="send-button"]', 'button[aria-label="Send message"]', 'button[data-testid*="send"]', 'button[type="submit"]', 'button[aria-label*="Send"]', 'button[aria-label*="发送"]', ], }, copilot: { name: "Microsoft Copilot", turnSelectors: [ '[class*="user-message"]', '[class*="assistant-message"]', '[data-testid*="message"]', ], userTurnSelectors: [ '[class*="user-message"]', '[data-testid*="user-message"]', '[data-message-author-role="user"]', ], userContentSelectors: [ '[class*="user-message"]', '[data-testid*="user-message"]', ], codeSelectors: ["pre"], contentSelectors: [ ".ac-textBlock", ".markdown", ".prose", '[class*="message-content"]', ], inputSelectors: [ 'textarea[placeholder]', 'div[contenteditable="true"][role="textbox"]', 'div[contenteditable="true"]', "textarea", ], sendButtonSelectors: [ 'button[data-testid*="send"]', 'button[aria-label*="Send"]', 'button[aria-label*="发送"]', 'button[type="submit"]', ], }, perplexity: { name: "Perplexity", turnSelectors: [ '[data-testid="chat-message"]', '[data-testid*="chat-message"]', '[data-message-author-role="user"]', '[data-message-author-role="assistant"]', ], userTurnSelectors: [ '[data-testid*="user-message"]', '[data-message-author-role="user"]', '[class*="user-message"]', '[class*="human"]', ], userContentSelectors: [ '[data-testid*="user-message"]', '[class*="user-message"]', ".whitespace-pre-wrap", ], codeSelectors: ["pre"], contentSelectors: [ ".prose", ".markdown", '[class*="prose"]', '[class*="message-content"]', ], inputSelectors: [ 'textarea[placeholder]', 'div[contenteditable="true"][role="textbox"]', 'div[contenteditable="true"]', "textarea", ], sendButtonSelectors: [ 'button[data-testid*="send"]', 'button[aria-label*="Send"]', 'button[aria-label*="发送"]', 'button[type="submit"]', ], }, poe: { name: "Poe", turnSelectors: [ ".Message", ".ChatMessage", '[data-message-author-role="user"]', '[data-message-author-role="assistant"]', ], userTurnSelectors: [ '[data-message-author-role="user"]', '[data-testid*="user-message"]', '[class*="UserMessage"]', '[class*="humanMessage"]', ], userContentSelectors: [ '[data-testid*="user-message"]', '[class*="UserMessage"]', ".Message", ], codeSelectors: ["pre"], contentSelectors: [ ".markdown", ".MessageContent", '[class*="messageContent"]', '[class*="message-content"]', ], inputSelectors: [ 'textarea[placeholder]', 'div[contenteditable="true"][role="textbox"]', 'div[contenteditable="true"]', "textarea", ], sendButtonSelectors: [ 'button[data-testid*="send"]', 'button[aria-label*="Send"]', 'button[aria-label*="发送"]', 'button[type="submit"]', ], }, doubao: { name: "Doubao", turnSelectors: [ '[data-testid="send_message"]', '[data-testid="receive_message"]', '[data-testid="message_text_content"]', '[data-testid="message_content"]', '[data-testid*="message_content"]', '[data-message-author-role="user"]', '[data-message-author-role="assistant"]', ], userTurnSelectors: [ '[data-testid="send_message"]', '[data-testid*="user-message"]', '[data-message-author-role="user"]', '[data-message-role="user"]', '[data-role="user"]', '[class*="user-message"]', ], userContentSelectors: [ '[data-testid="message_text_content"]', '[data-testid="message_content"]', '[data-testid*="user-message"]', '[class*="user-message"]', ], codeSelectors: ["pre"], contentSelectors: [ '[data-testid="message_text_content"]', '[data-testid="message_content"]', '[data-testid*="message_content"]', ".markdown", '[class*="message-content"]', ], inputSelectors: [ '[data-testid="chat_input_input"]', "#chat-input", 'textarea[placeholder]', 'div[contenteditable="true"][role="textbox"]', 'div[contenteditable="true"]', "textarea", ], sendButtonSelectors: [ '[data-testid="chat_input_send_button"]', 'button[data-testid*="send"]', 'button[aria-label*="Send"]', 'button[aria-label*="发送"]', 'button[type="submit"]', ], }, yuanbao: { name: "Tencent Yuanbao / 腾讯元宝", turnSelectors: [ ".agent-chat__list__item--human", ".agent-chat__list__item--ai", '[data-message-author-role="user"]', '[data-message-author-role="assistant"]', ], userTurnSelectors: [ ".agent-chat__list__item--human", ".agent-chat__bubble--human", '[data-message-author-role="user"]', '[data-role="user"]', ], userContentSelectors: [ ".agent-chat__bubble--human .hyc-content-text", ".agent-chat__bubble--human .agent-chat__bubble__content", ".agent-chat__bubble__content", '[data-message-author-role="user"]', ], codeSelectors: ["pre"], contentSelectors: [ ".hyc-common-markdown-style", ".hyc-content-md-done", ".hyc-content-md", ".agent-chat__speech-text", ".agent-chat__bubble__content", ".hyc-content-text", '[data-message-author-role="assistant"]', ], inputSelectors: [ '.agent-dialogue__content--common__input .ql-editor[contenteditable="true"]', '#search-bar .ql-editor[contenteditable="true"]', '.ql-editor[contenteditable="true"]', 'div[contenteditable="true"][role="textbox"]', "textarea", ], sendButtonSelectors: [ "#yuanbao-send-btn", '.agent-dialogue__content--common__input button[type="submit"]', 'button[aria-label*="发送"]', 'button[aria-label*="Send"]', 'button[type="submit"]', ], }, qwen: { name: "Qwen Chat", turnSelectors: [ ".chat-user-message-container", ".chat-user-message", '[data-message-author-role="user"]', '[data-message-author-role="assistant"]', '[data-message-author="user"]', '[data-message-author="assistant"]', '[data-role="user"]', '[data-role="assistant"]', '[data-testid="chat-message"]', '[class*="questionItem"]', '[class*="answerItem"]', ], userTurnSelectors: [ ".chat-user-message-container", ".chat-user-message", '[data-message-author-role="user"]', '[data-message-author="user"]', '[data-role="user"]', '[data-testid="user-message"]', '[class*="questionItem"]', '[class*="user-message"]', ], userContentSelectors: [ ".chat-user-message", ".chat-user-message-container", '[class*="questionItem"]', '[data-message-author-role="user"]', '[data-message-author="user"]', '[data-role="user"]', ".whitespace-pre-wrap", ], codeSelectors: ["pre"], contentSelectors: [ ".markdown-root", ".markdown-body", ".tongyi-markdown", ".markdown", ".prose", '[class*="answerItem"]', '[data-message-author-role="assistant"]', '[data-message-author="assistant"]', '[data-role="assistant"]', ], inputSelectors: [ "textarea#chat-input", '#chat-input[contenteditable="true"]', "textarea.message-input-textarea", 'textarea[placeholder*="Ask Qwen" i]', 'textarea[placeholder*="message" i]', '[contenteditable="true"][role="textbox"]', "textarea", '[contenteditable="true"]', ], sendButtonSelectors: [ ".message-input-right-button-send", 'button[data-testid="send-button"]', 'button[aria-label*="Send"]', 'button[aria-label*="发送"]', 'button[type="submit"]', ], }, qianwen: { name: "Qianwen / 千问", turnSelectors: [ '[class*="questionItem"]', '[class*="answerItem"]', '[data-message-author-role="user"]', '[data-message-author-role="assistant"]', '[data-message-author="user"]', '[data-message-author="assistant"]', '[data-role="user"]', '[data-role="assistant"]', ], userTurnSelectors: [ '[class*="questionItem"]', '[data-message-author-role="user"]', '[data-message-author="user"]', '[data-role="user"]', '[class*="user-message"]', ], userContentSelectors: [ '[class*="questionItem"]', ".bubble-uo23is", '[data-message-author-role="user"]', '[data-message-author="user"]', '[data-role="user"]', ], codeSelectors: ["pre"], contentSelectors: [ ".tongyi-markdown", ".markdown-body", ".markdown", ".prose", '[class*="answerItem"]', '[data-message-author-role="assistant"]', '[data-message-author="assistant"]', '[data-role="assistant"]', ], inputSelectors: [ "textarea#chat-input", "textarea.message-input-textarea", '#chat-input[contenteditable="true"]', 'textarea[placeholder*="问" i]', 'textarea[placeholder*="消息" i]', '[contenteditable="true"][role="textbox"]', "textarea", '[contenteditable="true"]', ], sendButtonSelectors: [ ".message-input-right-button-send", 'button[data-testid="send-button"]', 'button[aria-label*="发送"]', 'button[aria-label*="Send"]', 'button[type="submit"]', ], }, glm: { name: "ChatGLM / 智谱清言", turnSelectors: [ ".question-txt.dots", '[data-message-author="user"]', '[data-message-author="assistant"]', '[data-message-author-role="user"]', '[data-message-author-role="assistant"]', '[data-role="user"]', '[data-role="assistant"]', ".role-user", ".role-assistant", ".message-user", ".message-assistant", ], userTurnSelectors: [ ".question-txt.dots", '[data-message-author="user"]', '[data-message-author-role="user"]', '[data-role="user"]', ".role-user", ".message-user", '[data-testid*="user-message"]', ], userContentSelectors: [ ".question-txt.dots", ".message-user .markdown-body", ".message-user .markdown", ".role-user", ".message-user", '[data-message-author="user"]', '[data-role="user"]', ], codeSelectors: ["pre"], contentSelectors: [ ".role-assistant .markdown-body", ".message-assistant .markdown-body", ".markdown-body", ".markdown", ".prose", '[class*="markdown"]', '[class*="answer"]', '[data-message-author="assistant"]', '[data-role="assistant"]', ], inputSelectors: [ "#chat-input", 'textarea[placeholder*="请输入"]', 'textarea[placeholder*="消息"]', 'div[contenteditable="true"][role="textbox"]', 'div[contenteditable="true"]', "textarea", ], sendButtonSelectors: [ 'button[data-testid="send-button"]', 'button[aria-label*="发送"]', 'button[aria-label*="Send"]', 'button[type="submit"]', ], }, zai: { name: "Z.ai (GLM)", turnSelectors: [ ".chat-user", ".chat-assistant", '[data-message-author="user"]', '[data-message-author="assistant"]', '[data-role="user"]', '[data-role="assistant"]', ".message-user", ".message-assistant", ".role-user", ".role-assistant", ], userTurnSelectors: [ ".chat-user", '[data-message-author="user"]', '[data-role="user"]', ".message-user", ".role-user", '[data-testid="user-message"]', ], userContentSelectors: [ ".chat-user .flex.justify-end.pb-1", '[data-message-author="user"]', '[data-role="user"]', ".chat-user", ".message-user", ".role-user", ], codeSelectors: ["pre"], contentSelectors: [ ".chat-assistant .markdown-body", ".chat-assistant .markdown-root", ".chat-assistant", ".markdown-body", ".markdown-root", ".markdown", ".prose", '[class*="markdown"]', '[data-message-author="assistant"]', '[data-role="assistant"]', ".message-assistant", ".role-assistant", ], inputSelectors: [ 'div[contenteditable="true"]#chat-input', 'div[contenteditable="true"][data-testid*="input"]', "textarea#chat-input", 'textarea[placeholder="Send a Message"]', 'textarea[placeholder*="message" i]', 'div[contenteditable="true"][role="textbox"]', "textarea", '[contenteditable="true"]', ], sendButtonSelectors: [ 'button[type="submit"]', 'button[aria-label="Send"]', 'button[data-testid="send-button"]', 'button[aria-label*="send" i]', 'button[aria-label*="发送"]', ], }, }; function hostnameMatches(hostname, domains) { return domains.some( (domain) => hostname === domain || hostname.endsWith("." + domain), ); } function detectPlatform() { const hostname = window.location.hostname.toLowerCase(); const pathname = window.location.pathname; if (hostnameMatches(hostname, ["chatgpt.com", "chat.openai.com"])) { return PLATFORMS.chatgpt; } if (hostnameMatches(hostname, ["gemini.google.com"])) { return PLATFORMS.gemini; } if ( hostnameMatches(hostname, ["grok.com"]) || (hostname === "x.com" && pathname.startsWith("/i/grok")) ) { return PLATFORMS.grok; } if (hostnameMatches(hostname, ["claude.ai"])) { return PLATFORMS.claude; } if (hostnameMatches(hostname, ["deepseek.com"])) { return PLATFORMS.deepseek; } if (hostnameMatches(hostname, ["copilot.microsoft.com"])) { return PLATFORMS.copilot; } if (hostnameMatches(hostname, ["perplexity.ai"])) { return PLATFORMS.perplexity; } if (hostnameMatches(hostname, ["poe.com"])) { return PLATFORMS.poe; } if (hostnameMatches(hostname, ["doubao.com"])) { return PLATFORMS.doubao; } if (hostnameMatches(hostname, ["yuanbao.tencent.com"])) { return PLATFORMS.yuanbao; } if (hostnameMatches(hostname, ["chat.qwen.ai"])) { return PLATFORMS.qwen; } if (hostnameMatches(hostname, ["qianwen.com"])) { return PLATFORMS.qianwen; } if (hostnameMatches(hostname, ["chatglm.cn"])) { return PLATFORMS.glm; } if (hostnameMatches(hostname, ["chat.z.ai"])) { return PLATFORMS.zai; } return null; } const currentPlatform = detectPlatform(); if (!currentPlatform) return; const state = { pruneEnabled: true, keepTurns: 8, collapseCode: true, virtualFileEnabled: true, hudOpen: false, totalTurns: 0, hiddenCount: 0, foldedCodeCount: 0, indexedTurns: [], }; function loadSavedConfig() { try { const raw = localStorage.getItem(STORAGE_KEY_CONFIG); if (raw) { const parsed = JSON.parse(raw); if (typeof parsed.keepTurns === "number" && !isNaN(parsed.keepTurns)) { state.keepTurns = Math.max(2, Math.min(50, parsed.keepTurns)); } if (typeof parsed.pruneEnabled === "boolean") { state.pruneEnabled = parsed.pruneEnabled; } if (typeof parsed.collapseCode === "boolean") { state.collapseCode = parsed.collapseCode; } if (typeof parsed.virtualFileEnabled === "boolean") { state.virtualFileEnabled = parsed.virtualFileEnabled; } } } catch (_) {} } function saveCurrentConfig() { try { const config = { keepTurns: state.keepTurns, pruneEnabled: state.pruneEnabled, collapseCode: state.collapseCode, virtualFileEnabled: state.virtualFileEnabled, }; localStorage.setItem(STORAGE_KEY_CONFIG, JSON.stringify(config)); } catch (_) {} } const turnIndex = new Map(); const codeState = new Map(); const processedUserTurns = new WeakSet(); const virtualFileChatRows = new WeakMap(); let activeVirtualFiles = []; let trayIsDirty = false; let isSubmitting = false; let hudRootElement = null; let hudShadowRoot = null; let hudStats = null; let handoffStatusNode = null; let searchInput = null; let searchResultsBox = null; let virtualFileTrayAnchor = null; let virtualFileTrayResizeObserver = null; let virtualFileTrayPositionFrame = 0; let searchDebounceTimer = null; let observer = null; let codeResizeObserver = null; let scheduleTimer = null; let lastUpdateTime = 0; let fullTurnSyncRequested = true; let codeScanRequested = true; let dirtyTurns = new Set(); let dirtyCodeBlocks = new Set(); let previousTurns = []; let activeTurnSelectorGroup = null; let selfMutationDepth = 0; function el(tag, attrs = {}, children = []) { const elem = document.createElement(tag); for (const [key, value] of Object.entries(attrs)) { if (key === "className") { elem.className = value; } else if (key === "style" && typeof value === "string") { elem.style.cssText = value; } else if (key.startsWith("on") && typeof value === "function") { elem.addEventListener(key.slice(2).toLowerCase(), value); } else { elem.setAttribute(key, value); } } for (const child of children) { if (typeof child === "string") { elem.appendChild(document.createTextNode(child)); } else if (child instanceof Node) { elem.appendChild(child); } } return elem; } function withSelfMutation(task) { selfMutationDepth++; try { return task(); } finally { selfMutationDepth--; } } function fastCountLines(text) { if (!text) return 0; let count = 1; for (let i = 0; i < text.length; i++) { if (text.charCodeAt(i) === 10) count++; } return count; } function escapeHtml(text) { return String(text || "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function normalizeText(sourceText) { return String(sourceText || "") .replace(/\u00a0/g, " ") .replace(/\r\n?/g, "\n") .split("\n") .map((line) => line.replace(/[ \t]+$/g, "")) .join("\n") .replace(/\n{3,}/g, "\n\n") .trim(); } function matchesReasoningElement(element) { if (!(element instanceof Element)) return false; try { return element.matches(REASONING_SELECTOR); } catch (_) { return false; } } function isReasoningElement(element) { if (!(element instanceof Element)) return false; try { return ( matchesReasoningElement(element) || Boolean(element.closest(REASONING_SELECTOR)) ); } catch (_) { return false; } } function elementToPlainText(rootElement) { if (!(rootElement instanceof Element) || matchesReasoningElement(rootElement)) { return ""; } // Walk the live DOM without cloning large messages on each streaming mutation. // CSS dehydration/folding does not remove text nodes, so full text remains readable. const ignoredSelectors = [ REASONING_SELECTOR, ".ai-turbo-chat-file-row", ".ai-turbo-chat-capsule", ".ai-turbo-code-bar", "#ai-turbo-vf-tray", "#ai-turbo-vf-preview-modal", "button", "[role='toolbar']", "script", "style", "svg", "[hidden]", ].join(", "); const preservedCodeBlocks = []; function walk(node) { if (node.nodeType === Node.TEXT_NODE) { return node.nodeValue || ""; } if (!(node instanceof Element)) { return Array.from(node.childNodes || []).map(walk).join(""); } try { if (node !== rootElement && node.matches(ignoredSelectors)) return ""; } catch (_) {} const tag = node.tagName.toUpperCase(); if (tag === "BR") return "\n"; if (tag === "PRE") { const codeText = node.textContent || ""; const codeClasses = [ node.className || "", node.querySelector("code")?.className || "", node.getAttribute("data-language") || "", node.querySelector("code")?.getAttribute("data-language") || "", node.querySelector("code")?.getAttribute("lang") || "", ].join(" "); const languageMatch = codeClasses.match(/language-([a-z0-9+#.-]+)/i); const language = languageMatch ? languageMatch[1].toLowerCase() : "text"; const token = `\uE000TURBO_CODE_${preservedCodeBlocks.length}\uE001`; preservedCodeBlocks.push({ token, codeText, language }); return `\n${CODE_FENCE}${language}\n${token}\n${CODE_FENCE}\n`; } const content = Array.from(node.childNodes).map(walk).join(""); return BLOCK_TEXT_TAGS.has(tag) ? "\n" + content + "\n" : content; } let text = normalizeText(walk(rootElement)); for (const block of preservedCodeBlocks) { const placeholder = `${CODE_FENCE}${block.language}\n${block.token}\n${CODE_FENCE}`; const preserved = `${CODE_FENCE}${block.language}\n${block.codeText}\n${CODE_FENCE}`; text = text.replace(placeholder, preserved); } return text; } function isGeminiThoughtElement(element) { if (currentPlatform !== PLATFORMS.gemini || !(element instanceof Element)) return false; return Boolean( element.closest( "model-thoughts, .thoughts-container, .thoughts-content, [data-test-id*='thought']", ), ); } function isUserTurn(turnElement) { if (!(turnElement instanceof Element)) return false; const contentSearchKey = turnElement.getAttribute( "data-content-search-unit-key", ); if (contentSearchKey && /:(user|human)$/i.test(contentSearchKey)) { return true; } if (contentSearchKey && /:(assistant|model|system)$/i.test(contentSearchKey)) { return false; } const roleValues = [ turnElement.getAttribute("data-message-author-role"), turnElement.getAttribute("data-message-author"), turnElement.getAttribute("data-role"), turnElement.getAttribute("data-message-role"), turnElement.getAttribute("data-conversation-role"), turnElement.getAttribute("data-turn"), ].filter(Boolean); if (roleValues.some((role) => /^(user|human)$/i.test(role))) return true; if (roleValues.some((role) => /^(assistant|model|system)$/i.test(role))) { return false; } const platformUserSelectors = currentPlatform.userTurnSelectors || []; for (const selector of platformUserSelectors) { if ( turnElement.matches(selector) || turnElement.querySelector(selector) ) { return true; } } // Some platforms mark the wrapper's parent rather than the message-content node. let ancestor = turnElement.parentElement; let ancestorDepth = 0; while (ancestor && ancestorDepth < 3) { if (platformUserSelectors.some((selector) => ancestor.matches(selector))) { return true; } ancestor = ancestor.parentElement; ancestorDepth++; } if ( turnElement.matches( "user-query, [data-test-id='user-query'], .user-query, .user-query-container", ) || turnElement.querySelector( ".user-query-text, .query-text, [class*='query-content']", ) ) { return true; } if ( turnElement.matches("div.font-user-message") || turnElement.querySelector(".font-user-message") ) { return true; } if ( turnElement.matches('[data-testid*="user"], [class*="user"]') || turnElement.querySelector('[data-testid*="user"]') ) { return true; } return false; } function getTurnRole(turnElement) { if (!(turnElement instanceof Element)) return "assistant"; const contentSearchKey = turnElement.getAttribute( "data-content-search-unit-key", ); if (contentSearchKey && /:(user|human)$/i.test(contentSearchKey)) { return "user"; } if (contentSearchKey && /:(assistant|model)$/i.test(contentSearchKey)) { return "assistant"; } const roleValues = [ turnElement.getAttribute("data-message-author-role"), turnElement.getAttribute("data-message-author"), turnElement.getAttribute("data-role"), turnElement.getAttribute("data-message-role"), turnElement.getAttribute("data-conversation-role"), turnElement.getAttribute("data-turn"), ].filter(Boolean); if (roleValues.some((role) => /^(user|human)$/i.test(role))) return "user"; if (roleValues.some((role) => /^(assistant|model)$/i.test(role))) { return "assistant"; } for (const selector of currentPlatform.userTurnSelectors || []) { try { if (turnElement.matches(selector)) return "user"; } catch (_) {} } return isUserTurn(turnElement) ? "user" : "assistant"; } function uniqueDomOrdered(elements) { const seen = new Set(); const unique = []; for (const element of elements) { if (!(element instanceof Element) || seen.has(element)) continue; seen.add(element); unique.push(element); } unique.sort((first, second) => { if (first === second) return 0; const relation = first.compareDocumentPosition(second); if (relation & Node.DOCUMENT_POSITION_FOLLOWING) return -1; if (relation & Node.DOCUMENT_POSITION_PRECEDING) return 1; return 0; }); return unique; } function filterOutNested(elements) { const ordered = uniqueDomOrdered(elements); const candidateSet = new Set(ordered); return ordered.filter((element) => { let parent = element.parentElement; while (parent) { if (candidateSet.has(parent)) return false; parent = parent.parentElement; } return true; }); } function getActiveTurnSelectors() { const selectorGroups = currentPlatform.turnSelectorGroups; if (!Array.isArray(selectorGroups)) { return currentPlatform.turnSelectors; } if (activeTurnSelectorGroup) { return activeTurnSelectorGroup; } for (const selectorGroup of selectorGroups) { if (selectorGroup.some((selector) => document.querySelector(selector))) { activeTurnSelectorGroup = selectorGroup; return activeTurnSelectorGroup; } } return currentPlatform.turnSelectors; } function invalidateActiveTurnSelectorGroup() { activeTurnSelectorGroup = null; } function isRenderedChatGPTElement(element) { if ( !(element instanceof HTMLElement) || !element.isConnected || element.closest('[hidden], [aria-hidden="true"], [inert]') ) { return false; } const computedStyle = window.getComputedStyle(element); return ( computedStyle.display !== "none" && computedStyle.visibility !== "hidden" && computedStyle.visibility !== "collapse" && element.getClientRects().length > 0 ); } function selectChatGPTRootWithMostTurns(roots, turnSelector) { let selectedRoot = null; let largestTurnCount = -1; for (const candidateRoot of roots) { const turnCount = Array.from( candidateRoot.querySelectorAll(turnSelector), ).filter( (turnElement) => turnElement.closest("[data-turn-key]") === turnElement && isRenderedChatGPTElement(turnElement), ).length; if (turnCount >= largestTurnCount) { selectedRoot = candidateRoot; largestTurnCount = turnCount; } } return selectedRoot; } function collectChatGPTShellMessages(conversationRoot) { const messageNodes = []; const seenTurnRoles = new Set(); const turnContainers = Array.from( conversationRoot.querySelectorAll("[data-turn-key]"), ).filter( (turnContainer) => turnContainer.closest("[data-turn-key]") === turnContainer && isRenderedChatGPTElement(turnContainer), ); const roleSelectors = [ { role: "user", selectors: [ '[data-content-search-unit-key$=":user"]', '[data-user-message-bubble="true"]', '[data-message-role="user"]', '[data-conversation-role="user"]', ], }, { role: "assistant", selectors: [ '[data-content-search-unit-key$=":assistant"]', '[data-message-role="assistant"]', '[data-conversation-role="assistant"]', "[data-chatgpt-agent-turn-start]", ], }, ]; for (const turnContainer of turnContainers) { const turnKey = ( turnContainer.getAttribute("data-turn-key") || turnContainer.getAttribute("data-turn-id") || turnContainer.id || "" ).trim(); if (!turnKey) continue; for (const roleConfig of roleSelectors) { const roleIdentity = `${turnKey}:${roleConfig.role}`; if (seenTurnRoles.has(roleIdentity)) continue; let roleElement = null; for (const selector of roleConfig.selectors) { roleElement = Array.from( turnContainer.querySelectorAll(selector), ).find( (candidateElement) => candidateElement.closest("[data-turn-key]") === turnContainer && isRenderedChatGPTElement(candidateElement), ); if (roleElement) break; } if (!roleElement) continue; seenTurnRoles.add(roleIdentity); messageNodes.push(roleElement); } } return filterOutNested(uniqueDomOrdered(messageNodes)).filter( (element) => !isGeminiThoughtElement(element) && !matchesReasoningElement(element), ); } function getCurrentChatGPTTurns() { if (currentPlatform.name !== "ChatGPT") return null; const shellRoots = Array.from( document.querySelectorAll(CHATGPT_SHELL_ROOT_SELECTOR), ).filter(isRenderedChatGPTElement); if (shellRoots.length) { const activeShellRoot = selectChatGPTRootWithMostTurns( shellRoots, "[data-turn-key]", ); // A recognized but empty shell is authoritative; do not count a kept or hidden chat. return activeShellRoot ? collectChatGPTShellMessages(activeShellRoot) : []; } const transcriptRoots = Array.from( document.querySelectorAll(CHATGPT_TRANSCRIPT_ROOT_SELECTOR), ).filter(isRenderedChatGPTElement); if (transcriptRoots.length) { let activeTranscriptRoot = transcriptRoots[0]; let largestMessageCount = -1; for (const candidateRoot of transcriptRoots) { const messageCount = Array.from( candidateRoot.querySelectorAll(":scope > li[data-message-role]"), ).filter(isRenderedChatGPTElement).length; if (messageCount >= largestMessageCount) { activeTranscriptRoot = candidateRoot; largestMessageCount = messageCount; } } return Array.from( activeTranscriptRoot.querySelectorAll(":scope > li[data-message-role]"), ).filter(isRenderedChatGPTElement); } return null; } function getTurns() { const currentChatGPTTurns = getCurrentChatGPTTurns(); if (currentChatGPTTurns !== null) return currentChatGPTTurns; const collected = []; for (const selector of getActiveTurnSelectors()) { document .querySelectorAll(selector) .forEach((elem) => collected.push(elem)); } const unnested = filterOutNested(collected); return unnested.filter( (element) => (currentPlatform.name !== "ChatGPT" || isRenderedChatGPTElement(element)) && !isGeminiThoughtElement(element) && !matchesReasoningElement(element), ); } function findContentElement(turnElement) { const selectorGroups = []; if ( isUserTurn(turnElement) && Array.isArray(currentPlatform.userContentSelectors) ) { selectorGroups.push(currentPlatform.userContentSelectors); } selectorGroups.push(currentPlatform.contentSelectors || []); for (const selectorGroup of selectorGroups) { for (const selector of selectorGroup) { const match = turnElement.matches(selector) ? turnElement : turnElement.querySelector(selector); if (match && !isGeminiThoughtElement(match)) return match; } } return turnElement; } // 从完整消息容器中提取文本;不再只取第一个 Markdown 子节点或截断到 800 字符。 function extractTurnText(turnElement) { let raw = elementToPlainText(turnElement); const vfIdx = raw.indexOf(VF_TOKEN_START); if (vfIdx !== -1) { let userPart = raw .slice(0, vfIdx) .replace(/```[a-z]*\s*$/i, "") .trim(); if ( userPart === "请查阅并分析以下文本内容:" || userPart === "请查阅以下文本:" ) { userPart = ""; } const match = raw.match(VF_REGEX); const fileName = match ? ` [📎 ${match[1]}]` : " [📎 附件文件]"; raw = (userPart || "附件文本") + fileName; } return normalizeText(raw); } function mountVirtualFileCapsuleOutsideTurn(turnElement, pill) { if (!(turnElement instanceof Element) || !pill) { return null; } const parent = turnElement.parentElement; if (!parent) { return null; } const previousRow = virtualFileChatRows.get(turnElement); if (previousRow?.isConnected) { previousRow.remove(); } const row = el( "div", { className: "ai-turbo-chat-file-row", role: "group", "aria-label": "虚拟文件", }, [pill], ); /* * 关键: * 不再把 pill 插入 bubble 内部。 * 而是把它作为 turnElement 的同级节点, * 并放在消息气泡之前。 */ parent.insertBefore(row, turnElement); virtualFileChatRows.set(turnElement, row); return row; } // 修复问题(2):保留并醒目呈现用户原本的提问文本,将大文件精准收纳为小胶囊 function collapseInChatFileBlocks(turnElement) { if (!state.virtualFileEnabled || !turnElement || !isUserTurn(turnElement)) { return; } const existingRow = virtualFileChatRows.get(turnElement); if (processedUserTurns.has(turnElement) && existingRow?.isConnected) { return; } /* * 宿主网站 SPA 重绘后,原来的外置节点可能已经被移除。 * 这种情况下允许重新挂载。 */ if (!existingRow?.isConnected) { processedUserTurns.delete(turnElement); virtualFileChatRows.delete(turnElement); } const walker = document.createTreeWalker( turnElement, NodeFilter.SHOW_TEXT, null, false, ); let targetTextNode = null; while (walker.nextNode()) { if ( walker.currentNode.nodeValue && walker.currentNode.nodeValue.includes(VF_TOKEN_START) ) { targetTextNode = walker.currentNode; break; } } if (!targetTextNode) return; const codeBlockElem = targetTextNode.parentElement?.closest("pre, code-block"); // 智能定位待折叠宿主元素 let containerBlock = codeBlockElem; if (!containerBlock) { let cur = targetTextNode.parentElement; while (cur && cur !== turnElement) { const tag = cur.tagName.toLowerCase(); if ( ["p", "div", "pre", "code-block"].includes(tag) || cur.classList.contains("query-text") || cur.classList.contains("user-query-text") ) { containerBlock = cur; break; } cur = cur.parentElement; } } if (!containerBlock) containerBlock = targetTextNode.parentElement; const fullRawText = containerBlock.textContent || ""; const startIndex = fullRawText.indexOf(VF_TOKEN_START); if (startIndex === -1) return; const match = fullRawText.match(VF_REGEX); if (!match) return; const fileName = match[1].trim(); const lineCount = parseInt(match[2], 10) || 0; const sizeBytes = parseInt(match[3], 10) || 0; const kbSize = (sizeBytes / 1024).toFixed(1); const metaInfo = `${lineCount.toLocaleString()} 行 · ${kbSize} KB`; // 提取用户真实提问 Prompt let userPrompt = fullRawText.slice(0, startIndex); userPrompt = userPrompt.replace(/```[a-z]*\s*$/i, "").trim(); if ( userPrompt === "请查阅并分析以下文本内容:" || userPrompt === "请查阅以下文本:" ) { userPrompt = ""; } // 检查是否有前序兄弟节点已经独立呈现了该提问 let promptAlreadyVisible = false; let prev = containerBlock.previousElementSibling; while (prev) { if (prev.textContent && prev.textContent.trim().length > 0) { promptAlreadyVisible = true; break; } prev = prev.previousElementSibling; } withSelfMutation(() => { let isExpanded = false; const toggleBtn = el( "button", { type: "button", style: "background:rgba(255,255,255,0.08);border:1px solid rgba(148,163,184,0.25);color:#cbd5e1;padding:3px 12px;border-radius:999px;font:11px sans-serif;cursor:pointer;margin-left:auto;transition:all 0.15s ease;", }, ["展开查看文本"], ); const pill = el( "div", { className: "ai-turbo-chat-capsule", style: "display:inline-flex;align-items:center;gap:8px;padding:6px 14px;background:rgba(18,24,38,0.95);border:1px solid rgba(52,211,153,0.45);border-radius:9999px;box-shadow:0 2px 10px rgba(0,0,0,0.3);user-select:none;max-width:100%;box-sizing:border-box;margin:6px 0 10px;", }, [ el("span", { style: "font-size:14px;line-height:1;" }, ["📄"]), el( "span", { style: "font:600 12px ui-monospace,monospace;color:#f1f5f9;max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;", }, [fileName], ), el( "span", { style: "font:10px ui-monospace;background:rgba(52,211,153,0.15);color:#34d399;padding:2px 6px;border-radius:4px;", }, [metaInfo], ), toggleBtn, ], ); // 将原大文本容器隔离脱水 containerBlock.classList.add("ai-turbo-chat-folded-target"); containerBlock.style.setProperty("display", "none", "important"); containerBlock.style.setProperty("contain", "strict", "important"); toggleBtn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); isExpanded = !isExpanded; toggleBtn.textContent = isExpanded ? "收起文本" : "展开查看文本"; if (isExpanded) { containerBlock.style.setProperty("display", "block", "important"); containerBlock.style.setProperty("max-height", "320px", "important"); containerBlock.style.setProperty("overflow", "auto", "important"); } else { containerBlock.style.setProperty("display", "none", "important"); } }); // 若原容器内包含用户提问且无前序元素展示,独立保留用户真实提问 if (!promptAlreadyVisible && userPrompt) { const promptElem = el( "div", { className: "ai-turbo-chat-user-prompt", style: "margin-bottom:8px!important;white-space:pre-wrap!important;word-break:break-word!important;font:inherit!important;color:inherit!important;line-height:1.5!important;", }, [userPrompt], ); containerBlock.insertAdjacentElement("beforebegin", promptElem); } }); const mountedRow = mountVirtualFileCapsuleOutsideTurn(turnElement, pill); if (mountedRow) { processedUserTurns.add(turnElement); } } // 读写分离脱水,彻底杜绝 Layout Thrashing function syncTurnIndex(forceFull) { const turns = getTurns(); const structuralChange = forceFull || turns.length !== previousTurns.length || turns.some((turnElement, index) => turnElement !== previousTurns[index]); if (structuralChange) { turnIndex.clear(); turns.forEach((turnElement, index) => { collapseInChatFileBlocks(turnElement); turnIndex.set(turnElement, { id: index, element: turnElement, text: extractTurnText(turnElement), }); }); } else { for (const turnElement of dirtyTurns) { collapseInChatFileBlocks(turnElement); const record = turnIndex.get(turnElement); if (record) { record.text = extractTurnText(turnElement); } else { fullTurnSyncRequested = true; } } } state.totalTurns = turns.length; state.indexedTurns = turns.map((turnElement, index) => { const record = turnIndex.get(turnElement); if (record) { record.id = index; return record; } const fallbackRecord = { id: index, element: turnElement, text: extractTurnText(turnElement), }; turnIndex.set(turnElement, fallbackRecord); return fallbackRecord; }); const cutoff = state.pruneEnabled ? Math.max(0, turns.length - state.keepTurns) : 0; const toDehydrate = []; const toHydrate = []; turns.forEach((turnElement, index) => { const shouldDehydrate = state.pruneEnabled && index < cutoff; const isDehydrated = turnElement.classList.contains( "ai-turbo-dehydrated", ); if (shouldDehydrate && !isDehydrated) { toDehydrate.push({ elem: turnElement, height: turnElement.offsetHeight || 120, }); } else if (!shouldDehydrate && isDehydrated) { toHydrate.push(turnElement); } }); withSelfMutation(() => { for (let i = 0; i < toDehydrate.length; i++) { const item = toDehydrate[i]; item.elem.style.setProperty("--turbo-cached-h", `${item.height}px`); item.elem.classList.add("ai-turbo-dehydrated"); } for (let i = 0; i < toHydrate.length; i++) { toHydrate[i].classList.remove("ai-turbo-dehydrated"); toHydrate[i].style.removeProperty("--turbo-cached-h"); } }); state.hiddenCount = turns.reduce( (count, turnElement) => count + Number(turnElement.classList.contains("ai-turbo-dehydrated")), 0, ); previousTurns = turns; dirtyTurns.clear(); fullTurnSyncRequested = false; } function normalizeCodeElement(candidate) { if (!(candidate instanceof HTMLElement)) return null; const geminiBlock = candidate.closest("code-block"); if (geminiBlock instanceof HTMLElement) return geminiBlock; if (candidate.matches("code")) { return candidate.parentElement?.matches("pre") ? candidate.parentElement : candidate; } return candidate; } function getCodeBlocks(rootNode = document) { const collected = []; const selectors = currentPlatform.codeSelectors || ["pre"]; for (const selector of selectors) { rootNode.querySelectorAll(selector).forEach((candidate) => { const belongsToUserTurn = ( currentPlatform.userTurnSelectors || [] ).some((selector) => { try { return candidate.matches(selector) || Boolean(candidate.closest(selector)); } catch (_) { return false; } }); if ( candidate.classList.contains("ai-turbo-chat-folded-target") || belongsToUserTurn || candidate.closest( ".ai-turbo-chat-folded-target, .ai-turbo-chat-capsule, user-query, [data-message-author-role='user'], [data-content-search-unit-key$=':user'], [data-user-message-bubble='true'], [data-testid='collapsible-user-message-root'], .font-user-message", ) ) { return; } const codeElement = normalizeCodeElement(candidate); if (!codeElement) return; if (isGeminiThoughtElement(codeElement)) return; collected.push(codeElement); }); } return filterOutNested(collected); } function getCodeText(codeElement) { const sourceElement = codeElement.querySelector("pre code") || codeElement.querySelector("pre") || codeElement.querySelector("code") || codeElement; return typeof sourceElement.textContent === "string" ? sourceElement.textContent : ""; } function getCodeLanguage(codeElement) { const langElem = codeElement.querySelector( 'code[class*="language-"], [class*="language-"]', ); if (langElem) { const match = String(langElem.className || "").match( /language-([a-z0-9+#.-]+)/i, ); if (match) return match[1].toUpperCase(); } const decElem = codeElement.querySelector( ".code-block-decoration, [data-testid*='language']", ); const decText = decElem?.textContent?.trim(); if (decText && decText.length < 24) return decText.toUpperCase(); const attrLang = codeElement.getAttribute("data-language") || codeElement.getAttribute("lang"); if (attrLang) return attrLang.toUpperCase(); return "代码"; } function getCodeElementHeight(codeElement) { const pre = codeElement.querySelector("pre"); return Math.max( codeElement.scrollHeight || 0, codeElement.offsetHeight || 0, pre?.scrollHeight || 0, pre?.offsetHeight || 0, ); } function applyFoldStyles(entry, codeElement) { withSelfMutation(() => { if (entry.expanded) { codeElement.classList.remove("ai-turbo-folded"); codeElement.classList.add("ai-turbo-expanded"); codeElement.style.removeProperty("max-height"); codeElement.style.removeProperty("overflow"); } else { codeElement.classList.remove("ai-turbo-expanded"); codeElement.classList.add("ai-turbo-folded"); codeElement.style.setProperty("display", "block", "important"); codeElement.style.setProperty( "max-height", `${CODE_PREVIEW_HEIGHT}px`, "important", ); codeElement.style.setProperty("overflow", "hidden", "important"); } }); } function updateCodeMeta(entry, codeElement) { if (!entry || !codeElement?.isConnected) return; const currentText = getCodeText(codeElement); const currentLen = currentText.length; const now = Date.now(); if (currentLen !== entry.lastLength) { entry.lastLength = currentLen; entry.lastMutationTime = now; entry.renderedStaticOnce = false; } const isActivelyStreaming = entry.lastMutationTime > 0 && now - entry.lastMutationTime < STREAMING_IDLE_DELAY; if (isActivelyStreaming) { if (!entry.isDisplayingStreaming) { entry.isDisplayingStreaming = true; entry.langLabel.textContent = getCodeLanguage(codeElement); entry.linesLabel.textContent = ""; entry.linesLabel.appendChild( el("span", { className: "ai-turbo-streaming-badge" }, [ "● 正在输出...", ]), ); entry.toggleButton.textContent = entry.expanded ? "收起代码" : "展开代码"; } if (entry.streamingTimer) clearTimeout(entry.streamingTimer); entry.streamingTimer = setTimeout(() => { updateCodeMeta(entry, codeElement); }, STREAMING_IDLE_DELAY + 50); return; } if (!entry.isDisplayingStreaming && entry.renderedStaticOnce) { return; } entry.isDisplayingStreaming = false; entry.renderedStaticOnce = true; const lineCount = fastCountLines(currentText); entry.langLabel.textContent = getCodeLanguage(codeElement); entry.linesLabel.textContent = lineCount ? `${lineCount.toLocaleString()} 行` : ""; entry.toggleButton.textContent = entry.expanded ? "收起代码" : `展开 ${lineCount ? `${lineCount.toLocaleString()} 行` : "代码"}`; entry.toggleButton.setAttribute("aria-expanded", String(entry.expanded)); } function unmountCodeControls(codeElement) { const entry = codeState.get(codeElement); if (!entry) return; if (entry.streamingTimer) clearTimeout(entry.streamingTimer); withSelfMutation(() => { entry.bar?.remove(); codeElement.classList.remove( "ai-turbo-code-target", "ai-turbo-folded", "ai-turbo-expanded", ); codeElement.style.removeProperty("max-height"); codeElement.style.removeProperty("overflow"); codeElement.style.removeProperty("display"); }); codeState.delete(codeElement); } function isNativeCodeHeaderNode(node) { if (!(node instanceof HTMLElement)) return false; try { if ( node.matches(CHATGPT_CODE_HEADER_MARKER_SELECTOR) || node.querySelector(CHATGPT_CODE_HEADER_MARKER_SELECTOR) ) { return true; } } catch (_) { return false; } const className = typeof node.className === "string" ? node.className : ""; const markerText = [ className, node.getAttribute("data-testid") || "", node.getAttribute("aria-label") || "", node.getAttribute("title") || "", ].join(" "); if ( /(?:code|language).*(?:header|toolbar|decoration)|(?:header|toolbar).*(?:code|language)/i.test( markerText, ) ) { return true; } // Some native headers expose the language name and an icon-only copy // button instead of an accessible "Copy code" label. if (node.querySelector("button") && !node.querySelector("pre, code")) { const headerText = (node.textContent || "").trim(); if ( headerText.length > 0 && headerText.length <= 64 && (/^(?:[a-z0-9_+#.-]+)$/i.test(headerText) || /copy|复制|运行代码|code block/i.test(headerText)) ) { return true; } } return false; } function getChatGPTCodeCardAnchor(codeElement) { let branch = codeElement; let parent = codeElement.parentElement; let classBasedFallback = null; let depth = 0; while ( parent instanceof HTMLElement && depth < CHATGPT_CODE_CARD_MAX_ANCESTORS ) { // Never treat an entire assistant turn or transcript slot as a code // card. Only inspect ancestors below that boundary. if (parent.matches(CHATGPT_CODE_CARD_BOUNDARY_SELECTOR)) break; let precedingSibling = branch.previousElementSibling; while (precedingSibling) { if (isNativeCodeHeaderNode(precedingSibling)) { return parent.contains(codeElement) ? parent : null; } precedingSibling = precedingSibling.previousElementSibling; } const className = typeof parent.className === "string" ? parent.className : ""; const cardMarker = [ className, parent.getAttribute("data-testid") || "", parent.getAttribute("data-role") || "", ].join(" "); if ( /(?:code[-_ ]?(?:block|card|container)|(?:block|card)[-_ ]?code|group\/code)/i.test( cardMarker, ) ) { classBasedFallback = parent; } branch = parent; parent = parent.parentElement; depth++; } // If header controls cannot be recognized, only use an explicitly named // code-card wrapper. Otherwise leave this native code block untouched. return classBasedFallback instanceof HTMLElement && classBasedFallback.isConnected && classBasedFallback.contains(codeElement) ? classBasedFallback : null; } function getCodeControlAnchor(codeElement) { if (currentPlatform !== PLATFORMS.chatgpt) return codeElement; return getChatGPTCodeCardAnchor(codeElement); } function positionCodeControl(entry, codeElement) { const anchor = getCodeControlAnchor(codeElement); if (!(anchor instanceof HTMLElement) || anchor === codeElement) { // Fail closed: remove our fold state and reveal the original code if the // current ChatGPT DOM no longer exposes a safe outer-card anchor. unmountCodeControls(codeElement); return false; } if ( entry.anchor === anchor && entry.bar.isConnected && entry.bar.nextElementSibling === anchor ) { return true; } withSelfMutation(() => { entry.bar.remove(); anchor.insertAdjacentElement("beforebegin", entry.bar); entry.anchor = anchor; }); return true; } function mountCodeControls(codeElement) { if ( !state.collapseCode || !codeElement.isConnected || codeState.has(codeElement) ) return; if (codeElement.parentElement?.closest(".ai-turbo-code-target")) return; const measuredH = getCodeElementHeight(codeElement); if (measuredH <= CODE_COLLAPSE_HEIGHT) return; const anchor = getCodeControlAnchor(codeElement); if ( currentPlatform === PLATFORMS.chatgpt && (!(anchor instanceof HTMLElement) || anchor === codeElement) ) { return; } let sibling = codeElement.previousElementSibling; while (sibling && sibling.classList.contains("ai-turbo-code-bar")) { const toRemove = sibling; sibling = sibling.previousElementSibling; toRemove.remove(); } const initialText = getCodeText(codeElement); const langLabel = el("span", { className: "ai-turbo-code-lang" }); const linesLabel = el("span", { className: "ai-turbo-code-lines" }); const metaWrap = el("div", { className: "ai-turbo-code-bar-meta" }, [ langLabel, linesLabel, ]); const toggleButton = el("button", { type: "button", className: "ai-turbo-code-toggle", "aria-expanded": "false", }); const bar = el( "div", { className: currentPlatform === PLATFORMS.chatgpt ? "ai-turbo-code-bar ai-turbo-code-bar-chatgpt" : "ai-turbo-code-bar", }, [metaWrap, toggleButton], ); const entry = { bar, anchor, langLabel, linesLabel, toggleButton, expanded: false, lastLength: initialText.length, lastMutationTime: 0, streamingTimer: null, isDisplayingStreaming: false, renderedStaticOnce: false, }; codeState.set(codeElement, entry); withSelfMutation(() => { codeElement.classList.add("ai-turbo-code-target"); anchor.insertAdjacentElement("beforebegin", bar); }); toggleButton.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); entry.expanded = !entry.expanded; applyFoldStyles(entry, codeElement); entry.renderedStaticOnce = false; updateCodeMeta(entry, codeElement); if (!entry.expanded) { bar.scrollIntoView({ block: "nearest", behavior: "smooth" }); } }); applyFoldStyles(entry, codeElement); updateCodeMeta(entry, codeElement); } function sweepOrphanBars() { const activeBars = new Set( Array.from(codeState.values(), (entry) => entry.bar), ); document.querySelectorAll(".ai-turbo-code-bar").forEach((bar) => { if (!activeBars.has(bar)) { bar.remove(); return; } if (bar.classList.contains("ai-turbo-code-bar-chatgpt")) return; const next = bar.nextElementSibling; if (!next || !codeState.has(next) || codeState.get(next).bar !== bar) { bar.remove(); } }); } function syncCodeBlocks(forceScan, dirtyCandidates) { if (!state.collapseCode) { for (const [codeElement] of codeState) { unmountCodeControls(codeElement); } state.foldedCodeCount = 0; codeScanRequested = false; dirtyCodeBlocks.clear(); sweepOrphanBars(); return; } for (const [codeElement] of codeState) { if (!codeElement.isConnected) { unmountCodeControls(codeElement); } } const rawCandidates = forceScan ? getCodeBlocks() : Array.from(dirtyCandidates) .map(normalizeCodeElement) .filter((elem) => elem instanceof HTMLElement && elem.isConnected); const candidates = filterOutNested(rawCandidates); for (const codeElement of candidates) { const entry = codeState.get(codeElement); if (entry) { if ( currentPlatform === PLATFORMS.chatgpt && !positionCodeControl(entry, codeElement) ) { continue; } updateCodeMeta(entry, codeElement); } else { mountCodeControls(codeElement); } if (codeResizeObserver) { codeResizeObserver.observe(codeElement); } } sweepOrphanBars(); state.foldedCodeCount = Array.from(codeState.values()).filter( (entry) => !entry.expanded, ).length; codeScanRequested = false; dirtyCodeBlocks.clear(); } function getChatInput() { const selectors = [ ...(currentPlatform.inputSelectors || []), 'form[data-chatgpt-composer] [contenteditable="true"][role="textbox"]', 'div.ProseMirror[contenteditable="true"][role="textbox"]', "rich-textarea div[contenteditable='true']", 'div[contenteditable="true"][role="textbox"]', "textarea", '[contenteditable="true"]', ]; for (const selector of [...new Set(selectors)]) { const candidates = document.querySelectorAll(selector); for (const candidate of candidates) { if ( !(candidate instanceof HTMLElement) || !candidate.isConnected || candidate.closest("#" + HUD_ID) || (candidate.offsetParent === null && candidate.getClientRects().length === 0) ) { continue; } if ( candidate.matches('input[type="hidden"], textarea[aria-hidden="true"]') || candidate.getAttribute("aria-hidden") === "true" ) { continue; } return candidate; } } return null; } function isSendControlForInput(sendControl, inputElement) { if ( !(sendControl instanceof HTMLElement) || !(inputElement instanceof HTMLElement) ) { return false; } const inputForm = inputElement.closest("form"); const sendForm = sendControl.closest("form"); if (inputForm && sendForm) { return inputForm === sendForm; } const composerSelectors = [ "[data-chatgpt-composer]", ".input-area", ".input-area-container", "rich-textarea", "[class*='composer-parent']", "[class*='input-area']", "[class*='composer']", "[class*='input-container']", "fieldset", ]; const sharesComposerContainer = composerSelectors.some((selector) => { const inputContainer = inputElement.closest(selector); return Boolean( inputContainer && inputContainer === sendControl.closest(selector), ); }); if (sharesComposerContainer) return true; // Some platforms render the input and send control as siblings without a // shared form or a consistently named composer wrapper. let ancestor = inputElement.parentElement; let depth = 0; while (ancestor && depth < 5 && ancestor !== document.body) { if (ancestor.contains(sendControl)) return true; ancestor = ancestor.parentElement; depth++; } return false; } function getInputContainer(input) { if (!input) return null; const candidates = [ ".input-area", ".input-area-container", "rich-textarea", "[class*='composer-parent']", "[class*='input-area']", "form", "[class*='composer']", "[class*='input-container']", "fieldset", ]; for (const selector of candidates) { const matched = input.closest(selector); if (matched && matched.getBoundingClientRect().height > 20) { return matched; } } return input.parentElement; } function createUiIcon(name, size = 14) { const paths = { file: ["M6 3.5h7.7L18.5 8v12H6z", "M13.5 3.5V8h5"], eye: [ "M2.8 12s3.3-5 9.2-5 9.2 5 9.2 5-3.3 5-9.2 5-9.2-5-9.2-5z", "M12 14.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z", ], close: ["M5 5l14 14", "M19 5L5 19"], bolt: ["M13.2 2.5L5 13h6.1l-.8 8.5L19 10.2h-6.2z"], }; const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("viewBox", "0 0 24 24"); svg.setAttribute("width", String(size)); svg.setAttribute("height", String(size)); svg.setAttribute("fill", "none"); svg.setAttribute("stroke", "currentColor"); svg.setAttribute("stroke-width", "1.8"); svg.setAttribute("stroke-linecap", "round"); svg.setAttribute("stroke-linejoin", "round"); svg.setAttribute("aria-hidden", "true"); for (const pathData of paths[name] || []) { const path = document.createElementNS( "http://www.w3.org/2000/svg", "path", ); path.setAttribute("d", pathData); svg.appendChild(path); } return svg; } function stopVirtualFileTrayTracking() { if (virtualFileTrayResizeObserver) { virtualFileTrayResizeObserver.disconnect(); virtualFileTrayResizeObserver = null; } virtualFileTrayAnchor = null; if (virtualFileTrayPositionFrame) { window.cancelAnimationFrame(virtualFileTrayPositionFrame); virtualFileTrayPositionFrame = 0; } } function scheduleVirtualFileTrayPosition() { if (!activeVirtualFiles.length) return; if (virtualFileTrayPositionFrame) return; virtualFileTrayPositionFrame = window.requestAnimationFrame(() => { virtualFileTrayPositionFrame = 0; positionVirtualFileTray(); }); } function positionVirtualFileTray() { const tray = document.getElementById("ai-turbo-vf-tray"); if (!tray || !tray.isConnected || !activeVirtualFiles.length) { return; } const input = getChatInput(); const anchor = getInputContainer(input); if (!input || !anchor || !input.isConnected || !anchor.isConnected) { tray.classList.remove("is-positioned"); return; } if (anchor !== virtualFileTrayAnchor) { if (virtualFileTrayResizeObserver) { virtualFileTrayResizeObserver.disconnect(); } virtualFileTrayAnchor = anchor; if ("ResizeObserver" in window) { virtualFileTrayResizeObserver = new ResizeObserver(() => { scheduleVirtualFileTrayPosition(); }); virtualFileTrayResizeObserver.observe(anchor); if (input !== anchor) { virtualFileTrayResizeObserver.observe(input); } } } const anchorRect = anchor.getBoundingClientRect(); const inputRect = input.getBoundingClientRect(); if ( !anchorRect.width || !anchorRect.height || !inputRect.width || !inputRect.height ) { tray.classList.remove("is-positioned"); return; } const viewportPadding = 18; const verticalGap = 16; const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const availableWidth = Math.max(0, viewportWidth - viewportPadding * 2); const trayWidth = Math.min(Math.max(anchorRect.width, 280), availableWidth); const left = Math.min( Math.max(viewportPadding, anchorRect.left), Math.max(viewportPadding, viewportWidth - trayWidth - viewportPadding), ); /* * 核心修改: * 不再使用 top / bottom 二选一。 * 直接使用 bottom 锚定输入框顶部。 * * 这样无论输入框高度怎么变化, * 托盘都始终位于输入框上方。 */ const bottom = viewportHeight - inputRect.top + verticalGap; tray.style.width = `${trayWidth}px`; tray.style.left = `${left}px`; tray.style.top = "auto"; tray.style.bottom = `${Math.max(viewportPadding, bottom)}px`; /* * 输入框非常靠近顶部时,仍然保持托盘向上布局, * 不再翻转到输入框下面。 */ const maxTrayHeight = Math.max( 48, inputRect.top - viewportPadding - verticalGap, ); tray.style.maxHeight = `${maxTrayHeight}px`; tray.style.overflowY = "auto"; tray.dataset.placement = "top"; tray.classList.add("is-positioned"); } function removeVirtualFile(id) { activeVirtualFiles = activeVirtualFiles.filter((item) => item.id !== id); trayIsDirty = true; renderVirtualFileTray(); } function showTextPreviewModal(fileItem) { document.getElementById("ai-turbo-vf-preview-modal")?.remove(); const previewBackdrop = el("div", { id: "ai-turbo-vf-preview-modal", className: "ai-turbo-modal-backdrop", onclick: (e) => { if (e.target === previewBackdrop) previewBackdrop.remove(); }, }); const titleSpan = el( "span", { style: "font:600 13px -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;color:#f8fafc;max-width:70%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;", }, [ `📄 ${fileItem.name} (${fileItem.lineCount.toLocaleString()} 行 · ${(fileItem.size / 1024).toFixed(1)} KB)`, ], ); const closeBtn = el( "button", { type: "button", style: "background:transparent;border:0;color:#94a3b8;cursor:pointer;font-size:16px;line-height:1;padding:4px;border-radius:4px;", onclick: () => previewBackdrop.remove(), }, ["✕"], ); const header = el( "div", { style: "flex-shrink:0;display:flex;justify-content:space-between;align-items:center;padding:12px 18px;border-bottom:1px solid rgba(148,163,184,0.15);background:rgba(18,24,38,0.98);", }, [titleSpan, closeBtn], ); const bodyText = el( "pre", { style: "flex:1 1 auto;min-height:0;margin:0;padding:16px;overflow-y:auto;overflow-x:auto;background:rgba(5,8,15,0.7);font:12px/1.6 ui-monospace,monospace;color:#cbd5e1;white-space:pre-wrap;word-break:break-all;", }, [ fileItem.content.slice(0, 8000) + (fileItem.content.length > 8000 ? "\n\n... (预览仅截取前 8,000 字符,发送时将提交完整全部文本)" : ""), ], ); const footerText = el( "span", { style: "font:11px sans-serif;color:#64748b;" }, ["按 Esc 键或点击外部任意区域即可快速关闭"], ); const footerCloseBtn = el( "button", { type: "button", style: "background:rgba(255,255,255,0.08);border:1px solid rgba(148,163,184,0.25);color:#e2e8f0;padding:4px 14px;border-radius:6px;font:11px sans-serif;cursor:pointer;", onclick: () => previewBackdrop.remove(), }, ["关闭"], ); const footer = el( "div", { style: "flex-shrink:0;display:flex;justify-content:space-between;align-items:center;padding:10px 18px;border-top:1px solid rgba(148,163,184,0.15);background:rgba(18,24,38,0.98);", }, [footerText, footerCloseBtn], ); const modalBox = el( "div", { style: "background:#0d121c;border:1px solid rgba(52,211,153,0.35);box-shadow:0 24px 60px rgba(0,0,0,0.85);border-radius:14px;width:720px;max-width:94vw;max-height:85vh;display:flex;flex-direction:column;overflow:hidden;box-sizing:border-box;", }, [header, bodyText, footer], ); previewBackdrop.appendChild(modalBox); document.body.appendChild(previewBackdrop); } function renderVirtualFileTray() { let tray = document.getElementById("ai-turbo-vf-tray"); if (!activeVirtualFiles.length) { if (tray) { withSelfMutation(() => tray.remove()); } stopVirtualFileTrayTracking(); trayIsDirty = false; updateHudDashboard(); return; } const input = getChatInput(); const container = getInputContainer(input); if (!container || !container.isConnected) { return; } withSelfMutation(() => { if (!tray) { tray = el("div", { id: "ai-turbo-vf-tray", className: "ai-turbo-vf-tray", role: "region", "aria-label": "虚拟文件", }); document.body.appendChild(tray); } tray.textContent = ""; const header = el( "div", { className: "ai-turbo-vf-header", }, [ el( "div", { className: "ai-turbo-vf-heading", }, [ el( "span", { className: "ai-turbo-vf-heading-icon", }, [createUiIcon("bolt", 13)], ), el( "span", { className: "ai-turbo-vf-heading-title", }, ["虚拟文件"], ), el( "span", { className: "ai-turbo-vf-heading-count", }, [`${activeVirtualFiles.length}`], ), ], ), el( "span", { className: "ai-turbo-vf-heading-hint", }, ["发送时作为上下文附加"], ), ], ); const fileList = el("div", { className: "ai-turbo-vf-list", }); tray.appendChild(header); tray.appendChild(fileList); for (const item of activeVirtualFiles) { const sizeText = `${(item.size / 1024).toFixed(1)} KB`; const nameLabel = el( "span", { className: "ai-turbo-vf-name", title: item.name, }, [item.name], ); const metaLabel = el( "span", { className: "ai-turbo-vf-meta", }, [`${item.lineCount.toLocaleString()} 行`, " · ", sizeText], ); const btnPreview = el( "button", { type: "button", className: "ai-turbo-vf-action ai-turbo-vf-action-preview", title: "预览文本内容", "aria-label": `预览 ${item.name}`, }, [createUiIcon("eye", 13)], ); btnPreview.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); showTextPreviewModal(item); }); const btnDelete = el( "button", { type: "button", className: "ai-turbo-vf-action ai-turbo-vf-action-delete", title: "移除该文件", "aria-label": `移除 ${item.name}`, }, [createUiIcon("close", 13)], ); btnDelete.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); removeVirtualFile(item.id); }); const fileItem = el( "div", { className: "ai-turbo-vf-pill", }, [ el( "span", { className: "ai-turbo-vf-file-icon", }, [createUiIcon("file", 14)], ), el( "div", { className: "ai-turbo-vf-file-copy", }, [nameLabel, metaLabel], ), el( "span", { className: "ai-turbo-vf-badge", }, ["TEXT"], ), el( "div", { className: "ai-turbo-vf-actions", }, [btnPreview, btnDelete], ), ], ); fileList.appendChild(fileItem); } trayIsDirty = false; }); positionVirtualFileTray(); window.requestAnimationFrame(positionVirtualFileTray); updateHudDashboard(); } function handlePasteIntercept(event) { if (!state.virtualFileEnabled) return; const target = event.target; if ( !(target instanceof Element) || !target.closest( 'rich-textarea, textarea, [contenteditable="true"], [role="textbox"]', ) ) { return; } const clipData = event.clipboardData || window["clipboardData"]; if (!clipData) return; const pastedText = clipData.getData("text/plain"); if (!pastedText || pastedText.length < VIRTUAL_FILE_THRESHOLD) { return; } event.preventDefault(); event.stopImmediatePropagation(); const byteSize = new Blob([pastedText]).size; const kbSize = (byteSize / 1024).toFixed(1); const lineCount = fastCountLines(pastedText); const vfId = `vf_${Date.now()}`; const fileName = `pasted_text_${kbSize}kb.txt`; activeVirtualFiles.push({ id: vfId, name: fileName, size: byteSize, charCount: pastedText.length, lineCount, content: pastedText, timestamp: Date.now(), }); updateHudDashboard(); trayIsDirty = true; renderVirtualFileTray(); } function insertTextAtomic(inputElement, text) { if ( inputElement instanceof HTMLTextAreaElement || inputElement instanceof HTMLInputElement ) { inputElement.value = text; inputElement.dispatchEvent(new Event("input", { bubbles: true })); inputElement.dispatchEvent(new Event("change", { bubbles: true })); return; } inputElement.focus(); const singleContainer = `