Schwebender Konversationsnavigator mit schneller Nachrichtennavigation, Lesezeichen, Versionskontrollen, Nachrichtenscan, Dateibadges und Datumsmarkierungen für ChatGPT, Claude, Gemini, Grok, DeepSeek, Qwen, Kimi und andere KI-Plattformen.
// ==UserScript==
// @name AI Conversation Navigator
// @namespace https://greasyfork.org
// @version 20.22
// @description Floating conversation navigator with quick message navigation, bookmarks, version controls, message scanning, file badges, and datelines for ChatGPT, Claude, Gemini, Grok, DeepSeek, Qwen, Kimi, and other AI platforms.
// @description:zh-CN 浮动式对话导航器,支持快速消息导航、书签、版本控制、消息扫描、文件徽章和日期线,适用于 ChatGPT、Claude、Gemini、Grok、DeepSeek、Qwen、Kimi 及其他 AI 平台。
// @description:es Navegador flotante de conversaciones con navegación rápida entre mensajes, marcadores, controles de versión, escaneo de mensajes, indicadores de archivos y líneas de fecha para ChatGPT, Claude, Gemini, Grok, DeepSeek, Qwen, Kimi y otras plataformas de IA.
// @description:hi ChatGPT, Claude, Gemini, Grok, DeepSeek, Qwen, Kimi और अन्य AI प्लेटफ़ॉर्म के लिए त्वरित संदेश नेविगेशन, बुकमार्क, संस्करण नियंत्रण, संदेश स्कैनिंग, फ़ाइल बैज और दिनांक रेखाओं वाला फ़्लोटिंग वार्तालाप नेविगेटर।
// @description:ar متصفح محادثات عائم مع تنقل سريع بين الرسائل، وإشارات مرجعية، وعناصر تحكم في الإصدارات، ومسح الرسائل، وشارات الملفات، وخطوط التاريخ لمنصات ChatGPT وClaude وGemini وGrok وDeepSeek وQwen وKimi وغيرها من منصات الذكاء الاصطناعي.
// @description:fr Navigateur de conversations flottant avec navigation rapide entre les messages, favoris, contrôles de version, analyse des messages, badges de fichiers et repères de date pour ChatGPT, Claude, Gemini, Grok, DeepSeek, Qwen, Kimi et d'autres plateformes d'IA.
// @description:pt-BR Navegador flutuante de conversas com navegação rápida entre mensagens, favoritos, controles de versão, verificação de mensagens, indicadores de arquivos e linhas de data para ChatGPT, Claude, Gemini, Grok, DeepSeek, Qwen, Kimi e outras plataformas de IA.
// @description:ru Плавающий навигатор по диалогам с быстрой навигацией по сообщениям, закладками, управлением версиями, сканированием сообщений, значками файлов и линиями дат для ChatGPT, Claude, Gemini, Grok, DeepSeek, Qwen, Kimi и других ИИ-платформ.
// @description:id Navigator percakapan mengambang dengan navigasi pesan cepat, bookmark, kontrol versi, pemindaian pesan, badge file, dan penanda tanggal untuk ChatGPT, Claude, Gemini, Grok, DeepSeek, Qwen, Kimi, dan platform AI lainnya.
// @description:ja ChatGPT、Claude、Gemini、Grok、DeepSeek、Qwen、Kimi などの AI プラットフォームに対応した、メッセージのクイックナビゲーション、ブックマーク、バージョン管理、メッセージスキャン、ファイルバッジ、日付ラインを備えたフローティング会話ナビゲーター。
// @description:de Schwebender Konversationsnavigator mit schneller Nachrichtennavigation, Lesezeichen, Versionskontrollen, Nachrichtenscan, Dateibadges und Datumsmarkierungen für ChatGPT, Claude, Gemini, Grok, DeepSeek, Qwen, Kimi und andere KI-Plattformen.
// @description:ko ChatGPT, Claude, Gemini, Grok, DeepSeek, Qwen, Kimi 및 기타 AI 플랫폼을 위한 빠른 메시지 탐색, 북마크, 버전 제어, 메시지 스캔, 파일 배지 및 날짜 표시 기능을 갖춘 플로팅 대화 내비게이터.
// @description:tr ChatGPT, Claude, Gemini, Grok, DeepSeek, Qwen, Kimi ve diğer yapay zekâ platformları için hızlı mesaj gezinme, yer imleri, sürüm kontrolleri, mesaj tarama, dosya rozetleri ve tarih çizgileri sunan kayan konuşma gezgini.
// @description:vi Trình điều hướng hội thoại nổi với khả năng điều hướng nhanh giữa các tin nhắn, đánh dấu, kiểm soát phiên bản, quét tin nhắn, huy hiệu tệp và dòng ngày tháng cho ChatGPT, Claude, Gemini, Grok, DeepSeek, Qwen, Kimi và các nền tảng AI khác.
// @description:it Navigatore flottante delle conversazioni con navigazione rapida tra i messaggi, segnalibri, controlli di versione, scansione dei messaggi, badge dei file e indicatori di data per ChatGPT, Claude, Gemini, Grok, DeepSeek, Qwen, Kimi e altre piattaforme di IA.
// @description:nl Zwevende gespreksnavigator met snelle berichtnavigatie, bladwijzers, versiebeheer, berichtscanning, bestandsbadges en datummarkeringen voor ChatGPT, Claude, Gemini, Grok, DeepSeek, Qwen, Kimi en andere AI-platforms.
// @description:pl Pływający nawigator konwersacji z szybkim przechodzeniem między wiadomościami, zakładkami, kontrolą wersji, skanowaniem wiadomości, oznaczeniami plików i liniami dat dla ChatGPT, Claude, Gemini, Grok, DeepSeek, Qwen, Kimi oraz innych platform AI.
// @description:uk Плаваючий навігатор розмов із швидкою навігацією між повідомленнями, закладками, керуванням версіями, скануванням повідомлень, значками файлів і позначками дат для ChatGPT, Claude, Gemini, Grok, DeepSeek, Qwen, Kimi та інших платформ ШІ.
// @description:th ตัวนำทางการสนทนาแบบลอย พร้อมการนำทางข้อความอย่างรวดเร็ว บุ๊กมาร์ก การควบคุมเวอร์ชัน การสแกนข้อความ ป้ายกำกับไฟล์ และเส้นวันที่ สำหรับ ChatGPT, Claude, Gemini, Grok, DeepSeek, Qwen, Kimi และแพลตฟอร์ม AI อื่น ๆ
// @description:ms Navigator perbualan terapung dengan navigasi mesej pantas, penanda buku, kawalan versi, pengimbasan mesej, lencana fail dan penanda tarikh untuk ChatGPT, Claude, Gemini, Grok, DeepSeek, Qwen, Kimi dan platform AI lain.
// @description:fa پیمایشگر شناور مکالمات با پیمایش سریع میان پیامها، نشانکها، کنترل نسخه، اسکن پیامها، نشانهای فایل و خطوط تاریخ برای ChatGPT، Claude، Gemini، Grok، DeepSeek، Qwen، Kimi و دیگر پلتفرمهای هوش مصنوعی.
// @description:ur ChatGPT، Claude، Gemini، Grok، DeepSeek، Qwen، Kimi اور دیگر AI پلیٹ فارمز کے لیے تیز پیغام نیویگیشن، بُک مارکس، ورژن کنٹرول، پیغام اسکیننگ، فائل بیجز اور تاریخ کی لائنوں کے ساتھ فلوٹنگ گفتگو نیویگیٹر۔
// @description:bn ChatGPT, Claude, Gemini, Grok, DeepSeek, Qwen, Kimi এবং অন্যান্য AI প্ল্যাটফর্মের জন্য দ্রুত বার্তা নেভিগেশন, বুকমার্ক, সংস্করণ নিয়ন্ত্রণ, বার্তা স্ক্যানিং, ফাইল ব্যাজ এবং তারিখ রেখাসহ ভাসমান কথোপকথন নেভিগেটর।
// @description:fil Lumulutang na navigator ng pag-uusap na may mabilis na pag-navigate sa mga mensahe, bookmark, kontrol sa bersyon, pag-scan ng mensahe, mga badge ng file, at mga linya ng petsa para sa ChatGPT, Claude, Gemini, Grok, DeepSeek, Qwen, Kimi, at iba pang AI platform.
// @description:sv Flytande konversationsnavigator med snabb meddelandenavigering, bokmärken, versionskontroller, meddelandeskanning, filmärken och datummarkeringar för ChatGPT, Claude, Gemini, Grok, DeepSeek, Qwen, Kimi och andra AI-plattformar.
// @description:cs Plovoucí navigátor konverzací s rychlou navigací mezi zprávami, záložkami, ovládáním verzí, skenováním zpráv, odznaky souborů a datovými značkami pro ChatGPT, Claude, Gemini, Grok, DeepSeek, Qwen, Kimi a další platformy AI.
// @description:ro Navigator plutitor pentru conversații, cu navigare rapidă între mesaje, marcaje, controale de versiune, scanarea mesajelor, insigne pentru fișiere și marcaje de dată pentru ChatGPT, Claude, Gemini, Grok, DeepSeek, Qwen, Kimi și alte platforme AI.
// @description:el Αιωρούμενος πλοηγός συνομιλιών με γρήγορη πλοήγηση μεταξύ μηνυμάτων, σελιδοδείκτες, έλεγχο εκδόσεων, σάρωση μηνυμάτων, σήματα αρχείων και ενδείξεις ημερομηνίας για ChatGPT, Claude, Gemini, Grok, DeepSeek, Qwen, Kimi και άλλες πλατφόρμες AI.
// @description:he נווט שיחות צף עם ניווט מהיר בין הודעות, סימניות, בקרת גרסאות, סריקת הודעות, תגי קבצים וסימוני תאריך עבור ChatGPT, Claude, Gemini, Grok, DeepSeek, Qwen, Kimi ופלטפורמות AI נוספות.
// @description:hu Lebegő beszélgetésnavigátor gyors üzenetnavigációval, könyvjelzőkkel, verzióvezérléssel, üzenetellenőrzéssel, fájljelvényekkel és dátumjelölésekkel a ChatGPT, Claude, Gemini, Grok, DeepSeek, Qwen, Kimi és más AI-platformok számára.
// @description:da Flydende samtalenavigator med hurtig navigation mellem beskeder, bogmærker, versionskontrol, beskedscanning, filmærker og datomarkeringer til ChatGPT, Claude, Gemini, Grok, DeepSeek, Qwen, Kimi og andre AI-platforme.// @author Bui Quoc Dung, Traditio
// @match https://chatgpt.com/*
// @match https://gemini.google.com/*
// @match https://aistudio.google.com/*
// @match https://notebook.google.com/*
// @match https://www.google.com/*
// @match https://grok.com/*
// @match https://claude.ai/*
// @match https://www.kimi.com/*
// @match https://chat.mistral.ai/*
// @match https://www.perplexity.ai/*
// @match https://www.meta.ai/*
// @match https://poe.com/*
// @match https://deepai.org/*
// @match https://huggingface.co/chat/*
// @match https://chat.deepseek.com/*
// @match https://chat.qwen.ai/*
// @match https://manus.im/*
// @match https://chat.z.ai/*
// @match https://longcat.chat/*
// @match https://chatglm.cn/*
// @match https://ernie.baidu.com/*
// @match https://aistudio.xiaomimimo.com/*
// @match https://web.chatboxai.app/*
// @match https://duck.ai/*
// @match https://lmarena.ai/*
// @match https://quillbot.com/*
// @match https://www.canva.com/*
// @match https://www.genspark.ai/*
// @match https://character.ai/*
// @match https://spacefrontiers.org/*
// @match https://app.scienceos.ai/*
// @match https://evidencehunt.com/*
// @match https://playground.allenai.org/*
// @match https://paperfigureqa.allen.ai/*
// @match https://app.liner.com/*
// @match https://scira.ai/*
// @match https://exa.ai/*
// @match https://consensus.app/*
// @match https://www.openevidence.com/*
// @match https://math-gpt.org/*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_listValues
// @grant unsafeWindow
// @license MIT
// ==/UserScript==
// ============================================================================
// ARCHITECTURE AND MESSAGE-DATA APPROACH
// ============================================================================
// ChatGPT: Backend-API + TOC + Fiber + DOM + Attachment Interaction.
// ---------------------------------------------------------------------------
// Uses ChatGPT's own /backend-api/conversation endpoint as the primary source for conversation structure, ordinals, timestamps,
// and generated-file data, supplemented by the native Table of Contents (TOC) for navigation UI, the React Fiber tree for
// force-rendering unmounted turns and extracting generated-file data directly from turn props, and mounted DOM data for message
// content, files, images, versions, and other interaction.
//
// Attachment download and preview (both user-uploaded and assistant-generated files, covering images, PDFs, and plain-text formats)
// resolve directly through the same backend endpoints ChatGPT's own UI uses, without navigating to or mounting the underlying
// message, and a confirmed-unavailable attachment is detected before any navigation is attempted at all.
//
// Claude: React Fiber tree + DOM.
// ---------------------------------------------------------------------------
// Uses the React Fiber tree as the primary source for conversation structure, message data, timestamps, and virtualized-message
// information, supplemented by the DOM for interaction with mounted elements.
//
// Others: DOM.
// ---------------------------------------------------------------------------
// Primarily use the DOM to discover, extract, and navigate messages.
//
// =============================================================================
(function () {
'use strict';
// ============================================================================
// 01. CONSTANTS, SYMBOLS, AND GLOBAL STATE
// ============================================================================
// 01.1 Shared constants, symbols, and global state
// ---------------------------------------------------------------------------
const NAV_WIDTH = 250;
const NAV_MINI_WIDTH = 41;
const NAV_SUPERMINI_WIDTH = 40;
const RESPONSIVE_MINI_BREAKPOINT = 900;
const RESPONSIVE_RESIZE_DEBOUNCE_MS = 150;
const DEBOUNCE_TIME = 500;
const BOOKMARK_TTL_MS = 5 * 24 * 60 * 60 * 1000;
const BOOKMARK_PREFIX = 'acn_';
const MINI_BAR_STORAGE_KEY = 'acn_mini_bar_' + window.location.hostname;
const SUPERMINI_BAR_STORAGE_KEY = 'acn_supermini_bar_' + window.location.hostname;
const NAV_SIDE_STORAGE_KEY = 'acn_nav_left_' + window.location.hostname;
const NAV_THEME_STORAGE_KEY = 'acn_nav_theme_' + window.location.hostname;
const NAV_CUSTOM_COLOR_STORAGE_KEY = 'acn_nav_custom_color_' + window.location.hostname;
const NAV_THEME_GLOBAL_KEY = 'acn_nav_theme_global';
const NAV_CUSTOM_COLOR_GLOBAL_KEY = 'acn_nav_custom_color_global';
const NAV_THEME_SCOPE_KEY = 'acn_nav_theme_scope';
const NAV_FONT_SETTINGS_GLOBAL_KEY = 'acn_nav_idxfont_settings_global';
const NAV_FONT_SETTINGS_STORAGE_KEY = 'acn_nav_idxfont_settings_' + window.location.hostname;
const NAV_FONT_AND_SIZE_SCOPE_KEY = 'acn_nav_idxfont_scope';
const NAV_DATELINE_STYLE_KEY = 'acn_nav_dateline_style_global';
const DATELINE_GROUP_COLORS = ['#EC4899', '#378ADD', '#1D9E75', '#A855F7', '#EAB308', '#D85A30', '#14B8A6'];
const DATELINE_HOUR_GAP_PX = 1;
const DATELINE_STYLE_OPTIONS = ['default', 'solid', 'dashed', 'dotted', 'double', 'groove', 'ridge', 'inset', 'outset'];
const DATELINE_STYLE_DEFAULT = 'default';
function dateLineStyleToCssValue(style) {
return style === 'default' ? 'dashed' : style;
}
const DEFAULT_SETTINGS = Object.freeze({
theme: Object.freeze({
scope: 'global',
mode: 'website',
customColor: '#2b2b2b',
}),
font: Object.freeze({
scope: 'global',
face: 'system',
customFamily: '',
mini: Object.freeze({ idxScale: 1, dateScale: 1, baseSize: 15 }),
max: Object.freeze({ idxScale: 1, dateScale: 1, baseSize: 15, counterSize: 18 }),
supermini: Object.freeze({ idxScale: 1, dateScale: 1, baseSize: 15 }),
}),
settingsPanel: Object.freeze({
fontFamily: 'sans-serif',
fontSize: 15,
}),
});
const IDXFONT_DEFAULTS = {
mini: { idxScale: DEFAULT_SETTINGS.font.mini.idxScale, dateScale: DEFAULT_SETTINGS.font.mini.dateScale },
max: { idxScale: DEFAULT_SETTINGS.font.max.idxScale, dateScale: DEFAULT_SETTINGS.font.max.dateScale },
supermini: { idxScale: DEFAULT_SETTINGS.font.supermini.idxScale, dateScale: DEFAULT_SETTINGS.font.supermini.dateScale },
face: DEFAULT_SETTINGS.font.face,
customFamily: DEFAULT_SETTINGS.font.customFamily,
};
const HISTORY_POSITION_PREFIX = 'acn_reading_pos_';
const HISTORY_LOAD_MAX_ROUNDS = 18;
const HISTORY_LOAD_STABLE_ROUNDS = 3;
const HISTORY_LOAD_WAIT_MS = 420;
let activeMessageIndex = -1;
let lastUrl = window.location.href;
let lastPromptsContent = "";
let cachedPrompts = [];
let chatGptLastKnownPromptCount = 0;
let chatGptLastNearBottomAt = 0;
let urlCheckInterval = null;
let urlCheckIntervalId = null;
let injectedStyleId = 'nav-shift-styles';
let bookmarkedMessages = new Set();
let conversationObserver = null;
let isClosedByUser = false;
let tooltipTimeout = null;
let isMiniBar = GM_getValue(MINI_BAR_STORAGE_KEY, false);
let isSuperMini = GM_getValue(SUPERMINI_BAR_STORAGE_KEY, false);
let isNavOnLeftSide = GM_getValue(NAV_SIDE_STORAGE_KEY, false);
let navThemeScope = GM_getValue(NAV_THEME_SCOPE_KEY, DEFAULT_SETTINGS.theme.scope);
if (navThemeScope !== 'global' && navThemeScope !== 'per-site') navThemeScope = DEFAULT_SETTINGS.theme.scope;
let navTheme = GM_getValue(navThemeScope === 'global' ? NAV_THEME_GLOBAL_KEY : NAV_THEME_STORAGE_KEY, DEFAULT_SETTINGS.theme.mode);
if (navTheme === 'default') navTheme = 'website';
if (navTheme === 'auto') navTheme = 'website';
if (navTheme === 'system') navTheme = 'website';
let navCustomColor = GM_getValue(navThemeScope === 'global' ? NAV_CUSTOM_COLOR_GLOBAL_KEY : NAV_CUSTOM_COLOR_STORAGE_KEY, DEFAULT_SETTINGS.theme.customColor);
let navFontAndSizeScope = GM_getValue(NAV_FONT_AND_SIZE_SCOPE_KEY, DEFAULT_SETTINGS.font.scope);
if (navFontAndSizeScope !== 'global' && navFontAndSizeScope !== 'per-site') navFontAndSizeScope = DEFAULT_SETTINGS.font.scope;
let idxFontSettings = loadIdxFontSettings();
applyIdxFontSettings();
let dateLineStyle = GM_getValue(NAV_DATELINE_STYLE_KEY, DATELINE_STYLE_DEFAULT);
if (!DATELINE_STYLE_OPTIONS.includes(dateLineStyle)) dateLineStyle = DATELINE_STYLE_DEFAULT;
document.documentElement.style.setProperty('--acn-date-line-style', dateLineStyleToCssValue(dateLineStyle));
let isResponsiveMiniActive = false;
let responsiveResizeTimer = null;
let isHistoryLoading = false;
let historyLoadedUrl = '';
let historyLastSavedAt = 0;
let scrollLockInstalled = false;
let isScanningOldMessages = false;
const chatGptFullyLoadedConversationIds = new Set();
let chatGptHighlightSuppressionDepth = 0;
let promptCacheUrl = '';
let promptCacheItems = [];
let promptCacheMap = new Map();
let superminiDragInitialized = false;
let universalScrollSyncRafId = null;
let universalScrollSyncLastIndex = -1;
let currentFileAdapter = null;
window.navigatorUpdateTimeout = null;
let cachedSiteFaviconUrl = null;
let navDateDecorRerenderCleanup = null;
const dateLineLiveScaleLastPct = {};
const SYMBOL_MAXBAR = '▢';
const SYMBOL_MINIBAR = '◧';
const SYMBOL_PREVIOUS = '◀';
const SYMBOL_NEXT = '▶';
const SYMBOL_FIRST = '|◀';
const SYMBOL_LAST = '▶|';
const SYMBOL_PREVIOUS_FAST = '◀◀';
const SYMBOL_NEXT_FAST = '▶▶';
const SYMBOL_CLOSE = '✕';
const SYMBOL_BOOKMARK = '⛉';
const SYMBOL_DRAG = '⋮⋮';
const SYMBOL_SCAN = '⇡';
const SYMBOL_HARVEST_MOUNT_POSITION = '⚓\uFE0E';
const SYMBOL_SCANNING = '■';
const SYMBOL_THEME = '🎨\uFE0E';
const SYMBOL_FONT = 'Aa';
const SYMBOL_SETTINGS = '⛭';
// 01.2 Claude-specific constants and state
// ---------------------------------------------------------------------------
const CLAUDE_NAV_THEME_PALETTE = { dark: '#1e1e1e', light: '#f5f5f5' };
const CLAUDE_IDXFONT_MODE_GROUPS = [
{ id: 'mini', label: 'Mini navbar', hasCounter: true },
{ id: 'max', label: 'Full navbar', hasCounter: true },
{ id: 'supermini', label: 'Supermini navbar', hasCounter: false },
];
const CLAUDE_IDXFONT_FACE_OPTIONS = [
{ id: 'system', label: 'System Sans (Default)' },
{ id: 'serif', label: 'Serif' },
{ id: 'sans-serif', label: 'Sans-serif' },
{ id: 'monospace', label: 'Monospace' },
{ id: 'custom', label: 'Custom' },
];
const CLAUDE_HOLD_SCROLL_INITIAL_DELAY_MS = 300;
const CLAUDE_HOLD_SCROLL_SPEED_PX_PER_SEC = 450;
const CLAUDE_CURRENT_HIGHLIGHT_THROTTLE_MS = 200;
const CLAUDE_RESTORE_POSITION_DELAY_MS = 400;
const CLAUDE_LOCATING_TOAST_DELAY_MS = 250;
const CLAUDE_MESSAGE_TEXT_NODE_SELECTOR = 'p.whitespace-pre-wrap.break-words, ol, ul';
const CLAUDE_THEME_OPTIONS = [
{ id: 'website', label: 'Website' },
{ id: 'dark', label: 'Dark' },
{ id: 'light', label: 'Light' },
{ id: 'transparent', label: 'Transparent' },
{ id: 'custom', label: 'Custom' }
];
const CLAUDE_THEME_SCOPE_OPTIONS = [
{ id: 'global', label: 'Global' },
{ id: 'per-site', label: 'Per-site' },
];
let claudeStreamingUpdateInterval = null;
let themeAutoObserver = null;
let themeAutoApplyTimer = null;
let claudeSidebarResizeObserver = null;
let claudeBookmarkedUuids = new Set();
let claudeBookmarkedLegacyIndices = new Set();
let claudeCurrentVisibleIndex = -1;
let claudeCurrentVisibleUuid = null;
let claudeLastClickedIndex = -1;
let claudeCurrentHighlightLastComputedAt = 0;
let claudeNavToken = 0;
let claudeNavModifierAlt = false;
let claudeNavModifierCtrl = false;
let claudeNavModifierShift = false;
let claudeNavModifierEscape = false;
let chatGptNavModifierAlt = false;
let chatGptNavModifierCtrl = false;
let chatGptNavModifierShift = false;
let chatGptNavModifierEscape = false;
let claudeHoldScrollStartTimer = null;
let claudeHoldScrollRafId = null;
let claudeHoldScrollWasActive = false;
let claudeHarvestingActive = false;
let claudeCssStripRestoreTimer = null;
let claudeVirtualizerOffsetHooks = null;
let lastClaudeOffsetRefresh = 0;
let claudeNativeCacheCoverage = 0;
let claudeStabilityCheckToken = 0;
let claudePreviousChatFirstUuid = null;
let claudeReadingPositionRestoredUrl = null;
let claudeRestorePendingUrl = null;
let claudeSaveGuardActive = true;
let claudeNavigationActive = false;
let claudeConfirmedOffsets = new Map();
let claudePosinsetToIndex = new Map();
let claudeUuidToIndex = new Map();
let claudeLastKnownChainLength = 0;
let claudeForceRebuildPending = false;
let claudeTaggingObserver = null;
let claudeTaggingDebounce = null;
const CLAUDE_FILE_ADAPTER = {
async activateUserFile(file, index, fileIdx, mode = 'default') {
if (file && file.fileKind) return downloadUserFileClaude(file, index, fileIdx);
if (mode === 'attachment-preview') return openClaudeAttachmentPreview(file, index, fileIdx);
return openFilePreviewClaude(index, fileIdx);
},
async activateGeneratedFile(file, index, fileIdx) {
if (file && file.kind === 'widget') return downloadWidgetFileClaude(file);
if (isClaudeSharePage()) return activateGeneratedFileShareClaude(file, index, fileIdx);
if (file && file.kind === 'artifact') return openArtifactPanelClaude(index, fileIdx);
if (file && file.kind === 'inline-image') return openInlineImagePreviewClaude(index, fileIdx, file);
return focusAndDownloadGeneratedFileClaude(index, fileIdx, file);
}
};
// 01.3 ChatGPT-specific constants and state
// ---------------------------------------------------------------------------
const CHATGPT_CONVERSATION_READY_TIMEOUT_MS = 2800;
const CHATGPT_CONVERSATION_READY_POLL_MS = 70;
let chatGptRootCoverSyncTimer = null;
let chatGptRootCoverDepth = 0;
const TOC_OPEN_REASON_GPT = Object.freeze({
FIRST_OPEN: 'firstOpen',
ADD_NEW_MESSAGE: 'addNewMessage',
EDIT_MESSAGE: 'editMessage'
});
const ADDITIONAL_DATA_REASON_GPT = Object.freeze({
FIRST_OPEN: 'firstOpen',
MESSAGE_MOUNT: 'messageMount',
ADD_NEW_MESSAGE: 'addNewMessage',
EDIT_MESSAGE: 'editMessage',
SWITCH_VERSION: 'switchVersion'
});
const chatGptTocTextCache = new Map();
const chatGptTocDetailCache = new Map();
let chatGptNavigationToken = 0;
let chatGptPendingConversationUrl = '';
let chatGptSawTransientWebUrl = false;
let chatGptPendingConversationSnapshot = null;
let chatGptQueuedPreviousSnapshot = null;
let chatGptLastCommittedConversationSnapshot = null;
let chatGptTocCacheUrl = '';
let chatGptTocObserver = null;
let chatGptTocObservedGroup = null;
let chatGptTocRefreshTimer = null;
let chatGptStickyActiveOrdinal = -1;
let chatGptTocHoverPromise = null;
let chatGptTocHoverSignature = '';
let chatGptTocHoverAttemptedSignature = '';
let chatGptTocSyntheticHoverGroup = null;
let chatGptTocInitialHarvestDone = false;
let chatGptTocPostActionSequence = 0;
let chatGptTocPostActionPending = null;
let chatGptTocPostActionTimers = [];
const CHATGPT_FILE_ADAPTER = {
async activateUserFile(file, index, fileIdx, mode = 'default') {
const debug = typeof unsafeWindow !== 'undefined' && unsafeWindow.__acnChatGPTScrollDebug;
if (debug) console.log('[ACN-GPT-USERFILE]', { stage: 'activateUserFile called', file, index, fileIdx, mode });
if (mode === 'attachment-preview') {
const isImage = file && (file.sourceType === 'chatgpt-user-image' || file.ext === 'img');
const noNavResult = isImage
? await openUserImagePreviewNoNavGPT(file)
: await openUserAttachmentPreviewNoNavGPT(file);
if (noNavResult.ok) return true;
if (debug) console.log('[ACN-GPT-USERFILE]', { stage: 'no-nav preview failed', notFound: noNavResult.notFound, isImage });
if (noNavResult.notFound) {
showStatusOverlayClaude('Attachment no longer available.', { showSpinner: false });
await new Promise(resolve => setTimeout(resolve, 2200));
hideStatusOverlayClaude();
return false;
}
const previewed = await openUserFilePreviewGPT(file, index, fileIdx);
if (previewed) return true;
if (debug) console.log('[ACN-GPT-USERFILE]', { stage: 'preview failed, falling back to navigate' });
return navigateToMessage(index);
}
const isImage = file && (file.sourceType === 'chatgpt-user-image' || file.ext === 'img');
if (file && file.attachmentId) {
const result = await downloadUserFileGPT(file);
if (debug) console.log('[ACN-GPT-USERFILE]', { stage: 'downloadUserFileGPT result', result });
if (result.downloaded) return true;
}
if (isImage) {
const downloadedViaDom = await downloadUserImageGPT(file, index, fileIdx);
if (debug) console.log('[ACN-GPT-USERFILE]', { stage: 'downloadUserImageGPT fallback result', downloadedViaDom });
if (downloadedViaDom) return true;
} else if (!file || !file.attachmentId) {
if (debug) console.log('[ACN-GPT-USERFILE]', { stage: 'no attachmentId on file, falling back to navigate', file });
}
showStatusOverlayClaude('Download failed. File may no longer be available.', { showSpinner: false });
await new Promise(resolve => setTimeout(resolve, 2200));
hideStatusOverlayClaude();
return false;
},
async activateGeneratedFile(file, index, fileIdx, mode = 'default') {
const debug = typeof unsafeWindow !== 'undefined' && unsafeWindow.__acnChatGPTScrollDebug;
if (mode === 'attachment-preview' && file && file.downloadMessageId && file.sandboxPath) {
const ext = String(file.ext || '').toLowerCase();
if (PLAIN_TEXT_PREVIEW_EXTENSIONS_GPT.has(ext)) {
const resolved = await resolveGeneratedFileBlobGPT(file);
if (debug) console.log('[ACN-GPT-GENFILE]', { stage: 'preview resolveGeneratedFileBlobGPT result', hasBlob: Boolean(resolved.blob), notFound: resolved.notFound });
if (resolved.blob) {
let text;
try {
text = await resolved.blob.text();
} catch (err) {
text = '(could not read file as text)';
}
const contentEl = buildPlainTextPreviewContentGPT(text, file);
mountAttachmentPreviewOverlayGPT(contentEl);
return true;
}
if (resolved.notFound) {
showStatusOverlayClaude('Preview failed. File may no longer be available.', { showSpinner: false });
await new Promise(resolve => setTimeout(resolve, 2200));
hideStatusOverlayClaude();
return false;
}
}
}
if (file && file.downloadMessageId && file.sandboxPath) {
const result = await downloadGeneratedFileGPT(file);
if (debug) console.log('[ACN-GPT-GENFILE]', { stage: 'downloadGeneratedFileGPT result', result });
if (result.downloaded) return true;
if (result.notFound) {
showStatusOverlayClaude('Download failed. File may no longer be available.', { showSpinner: false });
await new Promise(resolve => setTimeout(resolve, 2200));
hideStatusOverlayClaude();
return false;
}
} else if (file && file.attachmentId) {
const result = await downloadUserFileGPT(file);
if (debug) console.log('[ACN-GPT-GENFILE]', { stage: 'downloadUserFileGPT (native image) result', result });
if (result.downloaded) return true;
if (result.notFound) {
showStatusOverlayClaude('Download failed. File may no longer be available.', { showSpinner: false });
await new Promise(resolve => setTimeout(resolve, 2200));
hideStatusOverlayClaude();
return false;
}
}
return focusGeneratedFileGPT(index, fileIdx, file);
}
};
// 01.4 AI Studio-specific constants and state
// ---------------------------------------------------------------------------
const AISTUDIO_DEBOUNCE_TIME = 200;
// ============================================================================
// 02. SITE CONFIGURATION AND DETECTION
// ============================================================================
// 02.1 Shared loading selectors
// ---------------------------------------------------------------------------
const COMMON_HISTORY_LOADING_SELECTORS = [
'[data-test-id="loading-history-spinner"]',
'[data-testid="loading-history-spinner"]',
'[aria-label*="Loading"]',
'[aria-label*="loading"]',
'.animate-spin'
];
// 02.2 Site definitions
// ---------------------------------------------------------------------------
const SITE_CONFIGS = {
claude: {
domain: 'claude.ai',
dateLine: { enabled: true, timeSource: 'fiber' },
historyPreload: true,
fastUpdate: true,
debounceTime: DEBOUNCE_TIME,
elements: {
article: '[role="article"]',
userBubble: '[data-user-message-bubble="true"], [data-cds="UserMessage"]',
fileThumbnail: '[data-testid="file-thumbnail"]',
answerCard: '[data-testid="ask-user-answers-card"]',
messageActionBar: '[data-cds="MessageActions"]'
},
fiber: {
reactKeyPrefix: '__reactFiber$',
propsKeyPrefix: '__reactProps$'
},
userMessage: {
container: 'div[role="article"]:has([data-user-message-bubble="true"], [data-testid="file-thumbnail"], [data-testid="ask-user-answers-card"], [data-cds="UserMessage"])',
file: '[data-testid="file-thumbnail"]',
fileType: '.uppercase.truncate',
fileName: 'h3',
image: '[data-testid*="."]',
text: '[data-testid="user-message"]'
},
inputAndSend: {
composerInput: '[data-testid="chat-input"]',
composerSendButton: 'button[data-testid="chat-input-send"]'
},
aiMessage: {
container: '[role="article"]',
file: '.group\\/artifact-block',
fileDownload: 'button[aria-label^="Download "]',
fileOpen: 'button[aria-label^="View "]',
relationRoot: '[role="article"]',
fileTarget: '.group\\/artifact-block',
image: 'button.group\\/tile',
scrollRoot: '[data-autoscroll-container="true"]'
},
shiftTarget: 'main, div[data-testid="incognito-frame"], .fixed.draggable-none, #main-content, .animate-incognito-header-in',
versionControl: {
container: '[data-cds="MessageActions"]',
versionText: 'span.self-center.shrink-0.select-none.font-small.text-muted',
prevButton: 'button[aria-label*="Previous version"]',
nextButton: 'button[aria-label*="Next version"]'
}
},
chatgpt: {
domain: 'chatgpt.com',
customFinder: getMessageDataGPT,
historyPreload: true,
dateLine: { enabled: true, timeSource: 'api' },
virtualScroll: {
enabled: true,
strategy: 'nativePromptToc',
scrollContainer: 'infinite-scroller.chat-history, [class~="scrollbar-gutter"], [class*="scrollbar-gutter"], [class~="@container/main"] > div, [class*="@container/main"] > div, #thread, main#main, [data-radix-scroll-area-viewport]',
scrollableCandidates: [
'infinite-scroller.chat-history',
'[class~="scrollbar-gutter"]',
'[class*="scrollbar-gutter"]',
'[class~="@container/main"] > div',
'[class*="@container/main"] > div',
'#thread',
'main#main',
'[data-radix-scroll-area-viewport]',
'main'
],
allowAutoScrollableDetect: false,
loadingSelectors: COMMON_HISTORY_LOADING_SELECTORS,
responseContainer: 'infinite-scroller.chat-history, main#main, #thread',
messageContainer: '[data-testid^="conversation-turn"], [data-turn-id], [data-turn-id-container]'
},
inputAndSend: {
composerInput: '#prompt-textarea',
composerSendButton: 'button[data-testid="send-button"]',
composerContainer: 'form',
editInput: 'textarea[aria-label="Edit message"]',
editSendButton: 'button.btn-primary'
},
nativePromptToc: {
button: 'button[data-toc-item-index]',
displayNumbers: true,
preserveDuplicates: false,
enrichOnNavigate: true
},
userMessage: {
container: 'user-query, div[data-message-author-role="user"]',
turnContainer: '[data-turn="user"], section[data-turn="user"]',
file: '.group\\/file-tile[role="group"][aria-label], [data-testid="file-thumbnail"]',
fileName: '.truncate.font-semibold',
fileType: '.truncate.text-token-text-secondary',
text: 'message-content, .user-message-bubble-color, .whitespace-pre-wrap',
image: 'img'
},
aiMessage: {
container: '[data-turn="assistant"], section[data-turn="assistant"]',
userTurn: '[data-turn="user"], section[data-turn="user"]',
file: 'button[aria-label="Download file"], button[aria-label^="Download file"]',
image: '.group\\/imagegen-image img, img[alt^="Generated image:" i]',
imageTarget: '.group\\/imagegen-image',
writingBlock: '[data-writing-block="true"], [data-oai-writing-block-surface]',
writingBlockRoot: '[data-writing-block="true"]',
writingBlockTarget: '[data-writing-block="true"], [data-oai-writing-block-surface]',
writingBlockTitle: 'h1, h2, h3, [data-testid*="title" i]',
writingBlockIgnore: '#message-nav, #acn-mini-tooltip',
fileDownload: 'button[aria-label="Download file"], button[aria-label^="Download file"]',
fileName: 'button[aria-label]',
fileNameVisualHint: '.truncate, [class*="truncate"]',
relationRoot: '#thread',
turnWrapper: '[data-turn-id-container]',
fileTarget: '[role="group"], .group\\/file-tile, [class*="file"], [class*="artifact"]'
},
shiftTarget: '.flex.h-svh.w-screen.flex-col, div[data-testid="fullscreen-shell-body"]',
versionControl: {
container: '.z-0.flex.justify-end, [class*="justify-end"]:has(.tabular-nums)',
versionText: '.tabular-nums',
prevButton: 'button[aria-label*="Previous response"], button[aria-label*="Previous version"]',
nextButton: 'button[aria-label*="Next response"], button[aria-label*="Next version"]'
}
},
gemini: {
domain: 'gemini.google.com',
inputAndSend: {
composerInput: 'rich-textarea .ql-editor[contenteditable="true"][role="textbox"]',
composerSendButton: '[data-test-id="send-button-container"] button[aria-label="Send message"]',
composerContainer: 'input-area-v2',
inputNavAnchor: '.text-input-field'
},
userMessage: {
container: '.user-query-container .user-query-container .user-query-container',
file: 'button.new-file-preview-file, .new-file-preview-container button[aria-label]',
text: '.query-text p',
image: '.preview-image'
},
aiMessage: {
container: 'model-response',
file: 'generated-file',
fileName: '[data-test-id="file-name"]',
fileType: '.file-type-lr',
fileDownload: 'button[aria-label*="Download" i], button[title*="Download" i], a[download], [data-test-id*="download" i], [data-testid*="download" i]',
fileTarget: 'generated-file',
image: 'generated-image single-image[data-image-attachment-index], single-image.generated-image[data-image-attachment-index]',
imagePreview: 'img.image[data-gwr-response-id], img[alt*="AI generated" i]',
imageDownload: 'download-generated-image-button button[aria-label*="Download" i], [data-test-id="download-generated-image-button"] button, button[aria-label="Download full size image"]',
imageTarget: 'single-image[data-image-attachment-index]',
markdownBlock: 'code-block',
markdownLanguage: '.code-block-decoration.header-formatted > span:first-child',
markdownLanguageValue: 'markdown',
markdownDownload: 'button[aria-label="Download code"], gem-icon-button[arialabel="Download code"] button',
markdownTarget: 'code-block',
relationRoot: '.conversation-container',
userTurn: 'user-query'
},
shiftTarget: 'chat-app, .boqOnegoogleliteOgbOneGoogleBar, top-bar-actions',
},
aistudio: {
domain: 'aistudio.google.com',
customFinder: getDataAIStudio,
useClick: true,
shiftTarget: '.layout-wrapper',
fastUpdate: true,
debounceTime: AISTUDIO_DEBOUNCE_TIME,
},
notebook: {
domain: 'notebook.google.com',
userMessage: { container: 'chat-message .from-user-container p' },
shiftTarget: 'notebook, .boqOnegoogleliteOgbOneGoogleBar',
},
googleSearch: {
domain: 'www.google.com',
userMessage: { container: '.tbIZh.wQN2Jd.Odbbif' },
shiftTarget: '[jsname="oEQ3x"], header[jsname="kNXmHc"], .eT9Cje',
},
grok: {
domain: 'grok.com',
userMessage: {
container: '.relative.group.flex.flex-col.justify-center.items-end',
file: 'button[aria-label="Open attachment"]',
fileName: 'span.truncate',
fileType: 'span.text-xs.text-fg-secondary',
text: '.message-bubble',
image: 'img:not([src*="/file-icons/"])'
},
aiMessage: {
container: '[data-testid="assistant-message"]',
file: '[role="button"][aria-label^="Open "], button[aria-label^="Download "]',
fileDownload: 'button[aria-label^="Download "], button[aria-label*="Download " i]',
relationRoot: '[data-scroll-anchor-root="true"]',
userTurn: '[data-testid="user-message"]'
},
shiftTarget: '#grok-app-root',
versionControl: {
container: '.relative.group.flex.flex-col.justify-center.items-end .action-buttons',
versionText: '[class="px-0.5"]',
prevButton: 'button[aria-label="Previous message"]',
nextButton: 'button[aria-label="Next message"]'
}
},
mistral: {
domain: 'chat.mistral.ai',
userMessage: {
container: 'div[data-message-author-role="user"] div[dir="auto"]',
file: '.max-w-2xs',
image: 'img',
text: '.select-none'
},
shiftTarget: 'main.bg-sidebar-subtle',
versionControl: {
container: 'div[data-message-author-role="user"]',
versionText: '[class*="text-muted"][class*="dark:text-muted"]',
prevButton: 'button[aria-label*="Previous version"]',
nextButton: 'button[aria-label*="Next version"]'
}
},
meta: {
domain: 'meta.ai',
userMessage: {
container: '[data-message-type="user"]',
file: '.mb-1.self-end',
fileName: '.mb-1.self-end .text-footnote',
fileType: '.mb-1.self-end .text-text-secondary',
text: '.relative .text-response',
image: 'button [alt="Image attachment"]'
},
shiftTarget: '[data-slot="sidebar-wrapper"]',
versionControl: {
container: '[data-message-type="user"] .min-h-0.min-w-0.items-center',
versionText: '.tabular-nums',
prevButton: 'button[aria-label="Previous revision"]',
nextButton: 'button[aria-label="Next revision"]'
}
},
deepseek: {
domain: 'chat.deepseek.com',
userMessage: {
container: '.d29f3d7d',
file: '.f3a54b52',
image: 'img',
text: '._9663006 .fbb737a4'
},
shiftTarget: ' ._2be88ba, ._2bd7b35',
versionControl: {
container: '._9663006',
versionText: '.dd7e4fda',
prevButton: '.e7367035:first-of-type',
nextButton: '.e7367035:last-of-type'
}
},
kimi: {
domain: 'www.kimi.com',
userMessage: {
container: '.segment.segment-user',
file: '.attachment-list .file-card-info-name',
image: '.image-wrapper.image-detail img',
text: '.user-content'
},
aiMessage: {
container: '.segment-container',
userTurn: '.segment.segment-user',
file: 'a[href^="sandbox:"]'
},
shiftTarget: '.has-sidebar',
versionControl: {
container: '.segment-user-actions',
versionText: '.assistant-page-info',
prevButton: '.icon-button.assistant-page-item:first-of-type',
nextButton: '.icon-button.assistant-page-item:last-of-type'
}
},
glm: {
domain: 'chat.z.ai',
userMessage: {
container: '.chat-user',
file: '.mb-1.truncate',
image: 'img.object-cover',
text: '.flex.justify-end.pb-1'
},
shiftTarget: '#chat-container',
versionControl: {
container: '.chat-user .flex.justify-end.text-gray-600',
versionText: '.self-center.text-sm.font-semibold.tracking-widest',
prevButton: '.self-center.p-1.rounded-md.transition:first-of-type',
nextButton: '.self-center.p-1.rounded-md.transition:last-of-type'
}
},
qwen: {
domain: 'chat.qwen.ai',
userMessage: {
container: '.chat-user-message-container',
file: '.index-module__file-message___SeOoR',
fileType: '.fileitem-file-name-ext',
fileName: '.fileitem-file-name-text',
image: 'img',
text: '.chat-user-message'
},
shiftTarget: '.desktop-layout-content',
versionControl: {
container: '.user-message-footer.ant-flex',
versionText: '.qwen-chat-ui-packages-siblings-text',
prevButton: '.anticon.qwen-chat-ui-packages-siblings-active-icon:first-of-type',
nextButton: '.anticon.qwen-chat-ui-packages-siblings-active-icon:last-of-type'
}
},
chatglm: {
domain: 'chatglm.cn',
userMessage: { container: '.question-txt.dots' },
shiftTarget: '.detail-container',
},
ernie: {
domain: 'ernie.baidu.com',
userMessage: {
container: '.roleUser__TCPTqNDW',
file: '.chat-file-item-card',
fileType: '.metaDesc__zRkzT0lZ span:first-of-type',
fileName: '.title__gkcd9NRs',
image: '.singleImage__BG1t1bGa img',
text: '#question_text_id'
},
shiftTarget: '#root',
versionControl: {
container: '.editControls__OdmgmAiJ',
versionText: '.pageCount__SvI_mTsu',
prevButton: '.turnIcon__oN1i9Cks:first-of-type',
nextButton: '.turnIcon__oN1i9Cks:last-of-type'
},
reverse: true,
},
xiaomimimo: {
domain: 'aistudio.xiaomimimo.com',
userMessage: { container: '.relative.inline-block.whitespace-pre-wrap' },
shiftTarget: '.absolute > .flex.flex-1.overflow-hidden',
},
perplexity: {
domain: 'perplexity.ai',
userMessage: {
container: 'div.group:has(button[aria-label="Edit query"]):has(button[aria-label="Copy query"])'
},
shiftTarget: '#root',
},
poe: {
domain: 'poe.com',
userMessage: { container: '[class*="ChatMessagesView_tupleGroupContainer"] > div > div:first-child' },
shiftTarget: '[class*="CanvasSidebarLayout_chat-column"]'
},
deepai: {
domain: 'deepai.org',
userMessage: { container: '.chatbox' },
shiftTarget: '.chat-layout-container, .new-chat-button-container, .persistent-compose-area, .nav-items'
},
huggingface: {
domain: 'huggingface.co',
userMessage: { container: '.disabled.w-full.appearance-none' },
shiftTarget: '.relative.min-h-0.min-w-0'
},
manus: {
domain: 'manus.im',
userMessage: { container: '.flex.relative.flex-col.gap-2.items-end' },
shiftTarget: '.simplebar-content'
},
longcat: {
domain: 'longcat.chat',
userMessage: { container: '.user-message' },
shiftTarget: '.content',
},
chatboxai: {
domain: 'web.chatboxai.app',
userMessage: { container: '.user-msg' },
shiftTarget: '.h-full.w-full.MuiBox-root'
},
duck: {
domain: 'duck.ai',
userMessage: { container: 'div[data-testid="user-message"]' },
shiftTarget: 'main'
},
lmarena: {
domain: 'lmarena.ai',
userMessage: { container: '.justify-end.gap-2' },
shiftTarget: '#chat-area',
reverse: true
},
quillbot: {
domain: 'quillbot.com',
userMessage: { container: 'div.MuiGrid-root.MuiGrid-container > div.MuiGrid-root > p.MuiTypography-root.MuiTypography-bodyMedium.MuiTypography-paragraph' },
shiftTarget: '#root-client'
},
canva: {
domain: 'www.canva.com',
userMessage: { container: '#_r_1_ .uV9Uzw .Ka9auQ p' },
shiftTarget: '#root'
},
genspark: {
domain: 'www.genspark.ai',
userMessage: { container: '.conversation-item-desc.user' },
shiftTarget: '.n-config-provider'
},
character: {
domain: 'character.ai',
userMessage: { container: '.w-full .bg-surface-elevation-3.opacity-90' },
shiftTarget: '#__next, #chat-header-background',
reverse: true
},
spacefrontiers: {
domain: 'spacefrontiers.org',
userMessage: { container: '.inline.whitespace-pre-line' },
shiftTarget: '#app'
},
scienceos: {
domain: 'app.scienceos.ai',
userMessage: { container: 'div[data-prompt]' },
shiftTarget: 'div[data-strategy]',
versionControl: {
container: 'div:has(> button[aria-label*="thread"])',
versionText: 'div[style*="tabular-nums"]',
prevButton: 'button[aria-label="Previous thread"]',
nextButton: 'button[aria-label="Next thread"]'
},
},
evidencehunt: {
domain: 'evidencehunt.com',
userMessage: { container: '.chat__message:has(.message__user-image) .message__content p' },
shiftTarget: '.v-main, .v-app-bar, .chat-tab-bar'
},
playground: {
domain: 'playground.allenai.org',
userMessage: { container: 'div[class*="chat-message"]:nth-of-type(even)' },
shiftTarget: '.MuiPaper-outlined'
},
paperfigure: {
domain: 'paperfigureqa.allen.ai',
userMessage: { container: '#chat-scroll-container > div > div:nth-of-type(odd) .MuiPaper-root' },
shiftTarget: '#root'
},
liner: {
domain: 'app.liner.com',
userMessage: { container: 'div.gap-positive-300.flex.flex-col p' },
shiftTarget: '.flex.min-h-0.w-full.flex-1,.flex.relative.w-full.gap-1'
},
scira: {
domain: 'scira.ai',
userMessage: { container: '.max-w-full .relative' },
shiftTarget: '.sm\\:max-w-2xl'
},
exa: {
domain: 'exa.ai',
userMessage: { container: 'div[data-test-id="UserMessage"]' },
shiftTarget: 'div[data-test-id="ChatPresentation"]'
},
consensus: {
domain: 'consensus.app',
userMessage: { container: '.flex.flex-col.pt-6.w-full.max-w-page h2' },
shiftTarget: '#__next'
},
openevidence: {
domain: 'openevidence.com',
userMessage: { container: '.brandable--query-bar--container form' },
shiftTarget: '#__next, .brandable--query-bar--container.hide-on-print.follow-up'
},
mathgpt: {
domain: 'math-gpt.org',
userMessage: { container: '.w-full.flex.items-end.flex-col.pb-8.relative' },
shiftTarget: '.overflow-x-hidden, .px-2.flex.flex-col.gap-1'
},
};
// 02.3 Site prompt finders
// ---------------------------------------------------------------------------
function getDataAIStudio() {
const prompts = [];
const scrollButtons = document.querySelectorAll('ms-items-scrollbar button');
scrollButtons.forEach(btn => {
const isUser = btn.querySelector('.items-scrollbar-dot');
if (isUser) {
const text = btn.getAttribute('aria-label');
if (text) prompts.push({ element: btn, textElement: btn, text: text.trim(), files: [] });
}
});
return prompts;
}
function getMessageDataGPT() {
if (!IS_CHATGPT) return [];
const visiblePrompts = extractVisibleMessagesDOMGPT();
if (typeof unsafeWindow !== 'undefined' && unsafeWindow.__acnChatGPTActiveDebug) {
console.log('[ACN-GPT-PENDING]', { source: 'getMessageDataGPT', visiblePromptsLength: visiblePrompts.length, hasUsableTOC: hasUsableTOCGPT(), apiReady: isChatGPTApiReadyGPT() });
}
if (!hasUsableTOCGPT() && !isChatGPTApiReadyGPT()) {
return rememberVisiblePromptsForCache(visiblePrompts, 'replace');
}
seedTOCMessageCacheGPT(visiblePrompts);
return promptCacheItems.length ? promptCacheItems : buildTOCMessageItemsGPT(visiblePrompts);
}
// ============================================================================
// 02.4 Current-site resolution
// ---------------------------------------------------------------------------
function getCurrentConfig() {
const hostname = window.location.hostname;
for (const key in SITE_CONFIGS) {
if (hostname.includes(SITE_CONFIGS[key].domain)) return SITE_CONFIGS[key];
}
return null;
}
const CURRENT_SITE = getCurrentConfig();
if (!CURRENT_SITE) return;
const IS_CHATGPT = CURRENT_SITE.domain === 'chatgpt.com';
const IS_CLAUDE = CURRENT_SITE.domain === 'claude.ai';
const IS_GEMINI = CURRENT_SITE.domain === 'gemini.google.com';
const IS_KIMI = CURRENT_SITE.domain === 'www.kimi.com';
const IS_GROK = CURRENT_SITE.domain === 'grok.com';
let chatGptPaginationFetchListeners = null;
let chatGptOriginalFetchForPagination = null;
let chatGptNumTurnsRewriteActive = false;
if (IS_CHATGPT) installChatGPTPaginationFetchWatcher();
// ============================================================================
// 03. GLOBAL NAVIGATOR STYLES
// ============================================================================
function injectStyles() {
const allUserSelectors = Object.values(SITE_CONFIGS)
.map(config => config.userMessage?.container)
.filter(selector => typeof selector === 'string' && selector.length > 0)
.join(', ');
// 03.1 Shared navigator container and header
// ---------------------------------------------------------------------------
GM_addStyle(`#message-nav { position: fixed; top: 0; right: 0; bottom: 0; z-index: 9999; font-family: sans-serif; font-size: 17px; color: var(--acn-nav-text, CanvasText); text-align: left; transition: width 0.25s ease, opacity 0.25s ease; }
#nav-header { display: flex; position: sticky; top: 0; z-index: 10; background-color: var(--acn-nav-bg-solid, Canvas); border-bottom: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 15%, transparent); }
#nav-buttons-group { display: flex; }
.nav-control-btn { background: none; border: none; cursor: pointer; color: inherit; }
#nav-btn-close, #nav-btn-mini { background: none; border: none; cursor: pointer; color: inherit; display: flex; align-items: center; justify-content: center; }
#nav-btn-scan { font-size: 12px; line-height: 1; padding: 2px 4px; border-radius: 4px; opacity: 0.85; }
#nav-btn-scan:hover { opacity: 1; background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 12%, transparent); }
#nav-btn-scan.scanning { font-weight: 700; text-decoration: underline; }
#nav-message-counter { user-select: none; color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 80%, transparent); font-weight: normal; display: flex; align-items: center; justify-content: center;}
@keyframes nav-blink-animation { 0% { opacity: 1; } 50% { opacity: 0.1; } 100% { opacity: 1; } }
.nav-blink-active { animation: nav-blink-animation 0.5s ease-in-out 3; }
`);
// 03.2 Shared settings button, panels, controls, and typography
// ---------------------------------------------------------------------------
GM_addStyle(`#nav-theme-panel,
#nav-theme-subpanel,
#nav-idxfont-subpanel { font-family: sans-serif; font-size: 15px; }
#nav-theme-panel *,
#nav-theme-subpanel *,
#nav-idxfont-subpanel * { font-family: inherit; }
#nav-theme-btn { position: relative; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; cursor: pointer; border-radius: 4px; opacity: 0.65; font-size: 15px; flex-shrink: 0; background: none; border: none; color: inherit; }
#message-nav.mini-bar #nav-theme-btn { order: 2; width: ${NAV_MINI_WIDTH}px; margin-top: -2px; margin-bottom: 1px; }
#nav-theme-btn:hover { opacity: 1; background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 12%, transparent); }
#nav-theme-btn.acn-panel-open { opacity: 1; }
#nav-theme-panel { display: none; position: fixed; width: auto; box-sizing: border-box; padding: 4px; background-color: var(--acn-nav-bg-solid, Canvas); color: var(--acn-nav-text, CanvasText); border: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 20%, transparent); border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 10002; }
.acn-settings-icon-btn { display: flex; align-items: center; justify-content: center; width: 26px; height: 26px; border-radius: 4px; cursor: pointer; background: transparent; border: none; color: var(--acn-nav-text, CanvasText); font-size: 15px; flex-shrink: 0; }
.acn-settings-icon-btn:hover { background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 10%, transparent); }
#nav-theme-subpanel { display: none; position: absolute; top: 100%; left: 0; margin-top: 4px; width: 170px; max-height: calc(100vh - 20px); overflow-y: auto; box-sizing: border-box; padding: 6px; background-color: var(--acn-nav-bg-solid, Canvas); color: var(--acn-nav-text, CanvasText); border: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 20%, transparent); border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 10003; }
#nav-theme-options { display: flex; flex-direction: column; gap: 2px; }
#nav-idxfont-subpanel { display: none; position: absolute; top: 100%; left: 0; margin-top: 4px; width: 230px; max-height: calc(100vh - 20px); overflow-y: auto; box-sizing: border-box; padding: 6px; background-color: var(--acn-nav-bg-solid, Canvas); color: var(--acn-nav-text, CanvasText); border: 1px solid color-mix(in srgb, CanvasText 20%, transparent); border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 10003; }
.acn-idxfont-group-label { font-size: 15px; font-weight: 600; margin-bottom: 2px; }
.acn-idxfont-row { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
.acn-idxfont-row-label { flex: 1 1 auto; font-size: 15px; }
.acn-idxfont-size-input { width: 48px; font-size: 15px; padding: 3px 4px; border-radius: 4px; background: transparent; color: var(--acn-nav-text, CanvasText); border: 1px solid color-mix(in srgb, CanvasText 20%, transparent); text-align: right; }
.acn-idxfont-size-unit { font-size: 15px; opacity: 0.7; }
.acn-idxfont-custom-input { width: 100%; box-sizing: border-box; font-size: 15px; padding: 4px 6px; border-radius: 4px; background-color: var(--acn-nav-bg-solid, Canvas); color: var(--acn-nav-text, CanvasText); border: 1px solid color-mix(in srgb, CanvasText 20%, transparent); margin-top: 4px; }
.acn-idxfont-hint { font-size: 15px; opacity: 0.7; margin-top: 2px; }
.acn-theme-option-btn { display: block; width: 100%; text-align: left; padding: 5px 8px; font-size: 15px; border-radius: 4px; cursor: pointer; background: transparent; border: 1px solid transparent; color: var(--acn-nav-text, CanvasText); }
.acn-theme-option-btn:hover { background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 10%, transparent); }
.acn-theme-option-btn.active { background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 15%, transparent); border-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 25%, transparent); font-weight: 600; }
#nav-theme-scope { display: flex; gap: 4px; }
.acn-theme-scope-btn { flex: 1 1 0; text-align: center; padding: 4px 6px; font-size: 15px; border-radius: 4px; cursor: pointer; background: transparent; border: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 20%, transparent); color: var(--acn-nav-text, CanvasText); opacity: 0.7; }
.acn-theme-scope-btn:hover { opacity: 1; }
.acn-theme-scope-btn.active { opacity: 1; background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 15%, transparent); border-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 30%, transparent); font-weight: 600; }
.acn-theme-divider { height: 1px; margin: 6px 0; background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 15%, transparent); }
#nav-theme-custom-color { width: 100%; height: 28px; margin-top: 6px; padding: 0; border: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 20%, transparent); border-radius: 4px; cursor: pointer; }
`);
GM_addStyle(`
#nav-theme-panel.acn-tabbed-settings { display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 310px; height: min(474px, calc(80vh - 20px)); max-width: calc(100vw - 20px); max-height: calc(100vh - 20px); overflow: hidden; box-sizing: border-box; flex-direction: column; align-items: stretch; gap: 0; padding: 0; background-color: var(--acn-nav-bg-solid, Canvas); color: var(--acn-nav-text, CanvasText); border: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 22%, transparent); border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,0.22); z-index: 2147483646; isolation: isolate; }
.acn-settings-titlebar { display: flex; align-items: center; justify-content: space-between; padding: 8px 10px; font-size: 15px; font-weight: 600; border-bottom: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 15%, transparent); }
.acn-settings-close { width: 24px; height: 24px; border: none; border-radius: 4px; background: transparent; color: inherit; cursor: pointer; font-size: 15px; }
.acn-settings-close:hover { background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 12%, transparent); }
.acn-settings-tabs { display: flex; gap: 4px; padding: 5px 6px 0; }
.acn-settings-tab { flex: 1; padding: 6px 8px; border: none; border-radius: 5px 5px 0 0; background: transparent; color: inherit; cursor: pointer; font-size: 15px; }
.acn-settings-tab:hover { background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 8%, transparent); }
.acn-settings-tab.active { font-weight: 600; background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 13%, transparent); }
.acn-settings-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 10px; box-sizing: border-box; }
.acn-settings-page { display: none; }
.acn-settings-page.active { display: block; min-height: 100%; }
.acn-settings-field { margin-bottom: 10px; }
.acn-settings-two-columns { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 10px; margin-bottom: 10px; }
.acn-settings-one-column { display: grid; grid-template-columns: minmax(0, 1fr); margin-bottom: 10px; }
.acn-settings-one-column .acn-settings-field { margin-bottom: 0; min-width: 0; }
.acn-settings-one-column .acn-settings-inline { width: 100%; }
.acn-settings-one-column .acn-settings-percent { width: 100%; min-width: 0; }
.acn-settings-two-columns .acn-settings-field { margin-bottom: 0; min-width: 0; }
.acn-settings-two-columns .acn-settings-inline { width: 100%; }
.acn-settings-two-columns .acn-settings-percent { width: 100%; min-width: 0; }
.acn-settings-label { display: block; margin-bottom: 4px; font-size: 15px; font-weight: 600; }
.acn-settings-select, .acn-settings-input { width: 100%; box-sizing: border-box; min-height: 30px; padding: 4px 6px; border-radius: 5px; background-color: var(--acn-nav-bg-solid, Canvas); color: inherit; border: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 24%, transparent); font: inherit; }
.acn-settings-input[type="number"] { appearance: auto !important; -webkit-appearance: auto !important; -moz-appearance: auto !important; padding-right: 2px; }
.acn-settings-input[type="number"]::-webkit-inner-spin-button,
.acn-settings-input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: inner-spin-button !important; appearance: auto !important; display: block !important; opacity: 1 !important; margin: 0 !important; }
.acn-settings-inline { display: flex; align-items: center; gap: 6px; }
.acn-settings-inline .acn-settings-input { flex: 1; }
.acn-settings-percent { width: 70px; text-align: right; }
.acn-settings-hint { margin-top: 4px; font-size: 15px; opacity: 0.72; line-height: 1.35; }
.acn-settings-divider { height: 1px; margin: 10px 0; background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 15%, transparent); }
.acn-settings-reset { width: 100%; padding: 6px 8px; border-radius: 5px; cursor: pointer; background: transparent; color: inherit; border: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 24%, transparent); }
.acn-settings-reset:hover { background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 10%, transparent); }
.acn-font-match { width: 18px; flex: 0 0 18px; text-align: center; font-size: 15px; }
#nav-theme-panel.acn-tabbed-settings #nav-theme-custom-color { width: 100%; height: 30px; margin: 0; padding: 0; border-radius: 5px; }
#message-nav.max-bar #nav-btn-scan { position: absolute; top: 50%; left: 26px; right: auto; transform: translateY(-50%); width: 24px; height: 24px; margin: 0; padding: 0; }
#message-nav.mini-bar #nav-mini-tools { display: flex; align-items: center; justify-content: center; gap: 2px; width: 100%; height: 24px; margin: 0; order: 2; flex-shrink: 0; }
#message-nav.mini-bar #nav-mini-tools #nav-btn-scan,
#message-nav.mini-bar #nav-mini-tools #nav-theme-btn { position: static; width: 18px; height: 18px; margin: 0; padding: 0; transform: none; order: initial; display: flex; align-items: center; justify-content: center; }
#message-nav.mini-bar #nav-mini-tools #nav-btn-scan,
#message-nav.mini-bar #nav-mini-tools #nav-theme-btn { position: relative; top: -4px; }
#message-nav.mini-bar #nav-mini-tools #nav-btn-scan { font-size: 15px; }
#message-nav.mini-bar #nav-mini-tools #nav-theme-btn { font-size: 15px; opacity: 1; }
#message-nav.max-bar #nav-mini-tools,
#message-nav.supermini-bar #nav-mini-tools { display: contents; }
body:has(#message-nav) #nav-theme-panel .acn-settings-input[type="number"] { appearance: auto !important; -webkit-appearance: auto !important; }
`);
// 03.3 Minibar
// ---------------------------------------------------------------------------
GM_addStyle(`#message-nav.mini-bar { width: ${NAV_MINI_WIDTH}px; overflow: hidden; opacity: 1; background-color: var(--acn-nav-bg, Canvas); border-left: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 20%, transparent); }
#message-nav.mini-bar.nav-left { border-left: none; border-right: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 20%, transparent); }
#message-nav.mini-bar #message-nav-content,
#message-nav.mini-bar #nav-bookmarks-header { display: none !important; }
#message-nav.mini-bar #nav-header { flex-direction: column; height: 100%; padding: 4px 0 1px 0; gap: 1px; border-bottom: none; overflow: hidden; align-items: center; justify-content: flex-start; }
#message-nav.mini-bar #nav-buttons-group { flex-direction: column; gap: 0; align-items: center; }
#message-nav.mini-bar .nav-control-btn { font-size: 12px; padding: 1px 0; transform: rotate(90deg); }
#message-nav.mini-bar #nav-btn-back, #message-nav.mini-bar #nav-btn-forward { margin-left: -1px; }
#message-nav.mini-bar #nav-btn-close { display: none; }
#message-nav.mini-bar #nav-btn-mini { position: static; background: none !important; opacity: 1 !important; width: ${NAV_MINI_WIDTH}px; height: 16px; font-size: 15px; margin-top: 5px; }
#nav-message-counter { flex-direction: column; font-size: calc(var(--acn-font-size-mini, 15px) * var(--acn-idx-scale-mini, 1)); font-family: var(--acn-idxfont-family, sans-serif); padding: 1px 0; text-align: center; gap: 0; }
.counter-divider { font-size: 1px; width: 16px; height: 1px; background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 15%, transparent); margin: 0; }
#nav-mini-msg-wrapper { display: none; flex-direction: column; align-items: center; width: 100%; flex: 1; overflow: hidden; margin-top: 1px; margin-bottom: 5px; }
#message-nav.mini-bar #nav-mini-msg-wrapper { display: flex; order: 1; border-top: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 15%, transparent); border-bottom: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 15%, transparent); padding-top: 4px; margin-top: 3px; }
#nav-mini-msg-numbers { display: flex; flex-direction: column; align-items: center; gap: 3px; width: 100%; overflow-y: auto; scroll-behavior: smooth; flex: 1; padding: 8px 0 2px 0; scrollbar-width: none; }
#nav-mini-msg-numbers::-webkit-scrollbar { display: none; }
#message-nav.max-bar #nav-list { padding: 0; margin: 0; list-style: none; position: relative; }
#message-nav.mini-bar .counter-divider { display: none !important;}
.nav-mini-scroll-btn { display: none; background: none; border: none; cursor: pointer; color: inherit; font-size: 10px; width: 100%; padding: 1px 0; opacity: 0.8; flex-shrink: 0; transition: opacity 0.2s; }
.nav-mini-scroll-btn:hover { opacity: 1; background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 12%, transparent); }
.nav-mini-item-wrapper { display: flex; flex-direction: column; align-items: center; width: 100%; margin: 0; position: relative; }
.nav-mini-version-row { display: flex; align-items: center; justify-content: center; gap: 1px; width: 100%; }
.nav-mini-version-label { font-size: 12px; line-height: 1; font-weight: 600; color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 60%, transparent); user-select: none; letter-spacing: -1.2px; font-variant-numeric: tabular-nums; white-space: nowrap; }
.nav-mini-row { display: flex; flex-direction: row; align-items: center; justify-content: center; width: 100%; }
.nav-mini-vbtn { background: none; border: none; padding: 0; margin: 0 -1px; font-size: 8px; cursor: pointer; color: var(--acn-nav-text, CanvasText); opacity: 0.5; display: flex; align-items: center; justify-content: center; width: 7px; flex-shrink: 0; }
.nav-mini-vbtn:hover { opacity: 1; background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 15%, transparent); border-radius: 2px; }
.nav-mini-vbtn:disabled { opacity: 0.15; cursor: not-allowed; }
.nav-mini-version-row .nav-mini-vbtn:last-child { margin-left: 0px; }
.nav-mini-num { font-size: calc(var(--acn-font-size-mini, 15px) * var(--acn-idx-scale-mini, 1)); font-family: var(--acn-idxfont-family, sans-serif); line-height: 1; cursor: pointer; user-select: none; padding: 4px; border-radius: 3px; color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 75%, transparent); width: 100%; text-align: center; box-sizing: border-box; transition: font-size 0.2s ease, background-color 0.2s ease; }
#message-nav.mini-bar .nav-mini-num { width: auto; min-width: 14px; padding: 1px 2px; }
.nav-mini-num:hover { background-color: color-mix(in srgb, Canvas 88%, CanvasText 12%); color: var(--acn-nav-text, CanvasText); }
.nav-mini-num.active { font-size: calc(var(--acn-font-size-mini, 15px) * var(--acn-idx-scale-mini, 1)); font-weight: bold; text-decoration: underline; color: var(--acn-nav-text, CanvasText); background-color: color-mix(in srgb, var(--acn-nav-bg, Canvas) 90%, CanvasText 10%); }
.nav-mini-num.bookmarked { background-color: #fff59d; color: black; }
#nav-mini-bookmarks { display: none; position: relative; width: 30px; height: 24px; flex: 0 0 24px; align-items: center; justify-content: center; margin: 3px 0 1px; border-radius: 4px; background-color: #fff59d; color: black; cursor: default; user-select: none; font-size: 14px; font-weight: 700; }
#message-nav.mini-bar #nav-mini-bookmarks.has-bookmarks { display: flex; }
#nav-mini-bookmarks-panel { display: none; position: fixed; width: 220px; max-height: 280px; overflow-y: auto; padding: 5px; box-sizing: border-box; background-color: var(--acn-nav-bg, Canvas); color: var(--acn-nav-text, CanvasText); border: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 20%, transparent); border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 10001; scrollbar-width: none; }
#nav-mini-bookmarks-panel::-webkit-scrollbar { display: none; }
#nav-mini-bookmarks:hover #nav-mini-bookmarks-panel, #nav-mini-bookmarks-panel:hover { display: block; }
.acn-compact-bookmark-item { display: flex; align-items: center; gap: 6px; padding: 4px 5px; border-radius: 3px; cursor: pointer; font-size: 14px; font-weight: normal; min-width: 0; }
.acn-compact-bookmark-item:hover { background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 12%, transparent); }
.acn-compact-bookmark-item.active { font-weight: bold; background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 10%, transparent); }
.acn-compact-bookmark-num { flex: 0 0 auto; min-width: 20px; padding: 1px 4px; border-radius: 3px; background-color: #fff59d; color: black; text-align: center; }
.acn-compact-bookmark-text { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#acn-mini-tooltip { position: fixed; z-index: 10000; background: var(--acn-nav-bg-solid, Canvas); color: var(--acn-nav-text, CanvasText); border: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 20%, transparent); border-radius: 6px; padding: 8px 12px; max-width: 300px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); pointer-events: auto; font-family: sans-serif; font-size: 15px; line-height: 1.4; word-break: break-word; display: none; }
#acn-mini-tooltip .nav-max-number { cursor: pointer; font-weight: normal; transition: font-weight 0.1s ease; margin-right: 4px; }
#acn-mini-tooltip .nav-max-number.bookmarked { background-color: #fff59d !important; color: black !important; padding: 2px 4px; border-radius: 3px; }
#acn-mini-tooltip .nav-file-tag { display: inline-block; font-size: 12px; font-weight: 700; padding: 1px 4px; border-radius: 6px; margin: 0 3px 0 2px; vertical-align: middle; letter-spacing: 0.04em; opacity: 0.9; background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 12%, transparent); color: var(--acn-nav-text, CanvasText); border: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 20%, transparent); line-height: 1.4; }
#acn-mini-tooltip .nav-file-tag[title]:hover { opacity: 1; cursor: pointer; background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 15%, transparent); }
#acn-mini-tooltip .acn-message-inline-badge:has(+ .nav-file-tag) { margin-right: 0; }
#acn-mini-tooltip .nav-max-version-controls { display: inline-flex; align-items: center; margin-left: 5px; gap: 2px; border-radius: 10px; padding: 2px; border: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 20%, transparent); }
#acn-mini-tooltip .nav-max-version-btn { background: none; border: none; cursor: pointer; font-size: 12px; padding: 1px 4px; border-radius: 3px; color: inherit; line-height: 1; }
#acn-mini-tooltip .nav-max-version-btn:hover { background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 10%, transparent); }
#acn-mini-tooltip .nav-max-version-btn:disabled { opacity: 0.3; cursor: not-allowed; }
#acn-mini-tooltip .nav-max-version-text { font-size: 12px; color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 70%, transparent); margin: 0 2px; }
`);
// 03.4 Maxbar
// ---------------------------------------------------------------------------
GM_addStyle(`#message-nav.max-bar { width: ${NAV_WIDTH}px; overflow-y: auto; background-color: var(--acn-nav-bg, Canvas); border-left: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 15%, transparent); scrollbar-width: none; font-size: calc(var(--acn-font-size-max, 15px) * var(--acn-idx-scale-max, 1)); }
#message-nav.max-bar.nav-left { border-left: none; border-right: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 15%, transparent); }
#message-nav.max-bar::-webkit-scrollbar { display: none; }
#message-nav.max-bar #nav-header { align-items: center; justify-content: center; padding: 12px 8px; position: sticky; top: 0; z-index: 20; background-color: var(--acn-nav-bg-solid, Canvas); }
#message-nav.max-bar #nav-buttons-group { gap: 0; align-items: center; justify-content: center; flex-shrink: 0; }
#message-nav.max-bar .nav-control-btn { font-size: 16px; line-height: 1; padding: 2px 5px; }
#message-nav.max-bar #nav-btn-mini,
#message-nav.max-bar #nav-btn-close { position: absolute; top: 50%; transform: translateY(-50%); width: 24px; height: 24px; margin: 0; padding: 0; font-size: 15px; line-height: 1; }
#message-nav.max-bar #nav-btn-mini { right: 26px; }
#message-nav.max-bar #nav-btn-close { right: 2px; }
#message-nav.max-bar #nav-theme-btn { position: absolute; top: 0; bottom: 0; margin: auto 0; width: 24px; height: 24px; padding: 0; font-size: 15px; line-height: 1; left: 2px; right: auto; opacity: 1; }
#message-nav.max-bar #nav-message-counter { padding: 0; min-width: 40px; text-align: center; font-size: calc(var(--acn-counter-size-max, 18px) * var(--acn-idx-scale-max, 1)) !important; font-family: var(--acn-idxfont-family, sans-serif); flex-direction: row; gap: 2px; }
#message-nav.max-bar .counter-divider { font-size: inherit; width: auto; height: auto; background: none; margin: 0 2px; }
#message-nav.max-bar #message-nav-content { padding: 5px; position: relative; }
#message-nav.max-bar .nav-max-item { cursor: pointer; padding: 5px 5px 5px 5px; display: flex; align-items: flex-start; font-weight: normal; transition: font-weight 0.1s ease; position: relative; }
#message-nav.max-bar .nav-max-number { flex: 0 0 auto; text-align: center; margin-right: 4px; min-width: 16px; font-size: inherit !important; font-family: var(--acn-idxfont-family, sans-serif); }
#message-nav.max-bar .nav-max-content { flex: 1; min-width: 0; font-family: var(--acn-idxfont-family, sans-serif); font-size: inherit !important; }
#message-nav.max-bar .nav-max-item,
#message-nav.max-bar .nav-max-item * { font-size: inherit; }
#message-nav.max-bar .nav-max-item.active { font-weight: bold !important; background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 10%, transparent); }
#message-nav.max-bar .nav-max-number.bookmarked { background-color: #fff59d !important; color: black !important; padding: 2px 4px; border-radius: 3px; }
.nav-max-preview { white-space: pre-line; font-size: inherit !important; }
#message-nav.max-bar .nav-file-tag { display: inline-block; font-size: 12px; font-weight: 400; padding: 3px 4px 1px 4px; border-radius: 6px; margin: 0 3px 0 2px; vertical-align: 0px; letter-spacing: 0.04em; opacity: 0.9; background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 12%, transparent); color: var(--acn-nav-text, CanvasText); border: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 20%, transparent); line-height: 1.4; }
#message-nav.max-bar .nav-file-tag[title]:hover { opacity: 1; cursor: pointer; background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 15%, transparent); }
#message-nav.max-bar .nav-max-version-controls { display: inline-flex; align-items: center; margin-left: 5px; gap: 2px; border-radius: 10px; padding: 2px; border: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 20%, transparent); }
#message-nav.max-bar .nav-max-version-btn { background: none; border: none; cursor: pointer; font-size: 12px; padding: 1px 4px; border-radius: 3px; color: inherit; line-height: 1; }
#message-nav.max-bar .nav-max-version-btn:hover { background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 10%, transparent); }
#message-nav.max-bar .nav-max-version-btn:disabled { opacity: 0.3; cursor: not-allowed; }
#message-nav.max-bar .nav-max-version-text { font-size: 12px; color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 70%, transparent); margin: 0 2px; }
#message-nav.max-bar .nav-bookmarks-container { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; padding: 4px 10px; background-color: var(--acn-nav-bg-solid, Canvas); border-bottom: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 10%, transparent); min-height: 34px; box-sizing: border-box; width: 100%; flex-shrink: 0; position: sticky; z-index: 9; }
#message-nav.max-bar .nav-bookmark-item { background-color: #fff59d; color: black; padding: 3px 6px; border-radius: 3px; cursor: pointer; font-size: 15px; user-select: none; transition: background-color 0.2s; }
#message-nav.max-bar .nav-bookmark-item:hover { background-color: #fff176; }
#message-nav.max-bar .nav-bookmark-item.active { font-weight: bold; background-color: #ffeb3b; }
`);
// 03.5 Superminibar
// ---------------------------------------------------------------------------
GM_addStyle(`#message-nav.supermini-bar { width: ${NAV_SUPERMINI_WIDTH}px; min-width: ${NAV_SUPERMINI_WIDTH}px; max-width: ${NAV_SUPERMINI_WIDTH}px; height: auto; position: fixed; top: 100px; right: 10px; bottom: auto; left: auto; z-index: 9999; background-color: var(--acn-nav-bg, Canvas); border: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 20%, transparent); border-radius: 8px; opacity: 0.3; transition: opacity 0.25s ease; overflow: visible; }
#message-nav.supermini-bar { transition: opacity 0.25s ease !important; }
#message-nav.supermini-bar:hover { opacity: 1;}
#message-nav.supermini-bar.acn-panel-open { opacity: 1; }
#message-nav.supermini-bar #nav-header,
#message-nav.supermini-bar #nav-mini-msg-wrapper,
#message-nav.supermini-bar #message-nav-content,
#message-nav.supermini-bar #nav-bookmarks-header { display: none !important; }
#nav-supermini-header { display: none; flex-direction: column; align-items: center; padding: 4px 0; gap: 4px;}
#message-nav.supermini-bar #nav-supermini-header {display: flex; }
.nav-supermini-btn { background: none; border: none; cursor: pointer; color: inherit; font-size: 14px; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; border-radius: 4px; padding: 0; }
#message-nav.supermini-bar #nav-theme-btn { width: 28px; height: 28px; opacity: 1; }
.nav-supermini-btn:hover { background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 12%, transparent); }
#nav-btn-drag { cursor: grab; font-size: 11px; letter-spacing: -1px;}
#nav-btn-drag:active { cursor: grabbing; }
#acn-supermini-input-nav { position: absolute; left: 50%; top: 0; right: auto; bottom: auto; margin: 0; transform: translate(-50%, calc(-100% - 3px)); display: none; align-items: center; justify-content: center; gap: 5px; min-height: 26px; padding: 2px 6px; box-sizing: border-box; border-radius: 8px; background-color: var(--acn-nav-bg-solid, Canvas); color: var(--acn-nav-text, CanvasText); border: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 18%, transparent); box-shadow: 0 -2px 8px rgba(0,0,0,0.08); font-family: var(--acn-idxfont-family, sans-serif); font-size: 13px; line-height: 1; z-index: 20; user-select: none; white-space: nowrap; opacity: 0.3; transition: opacity 0.25s ease; }
#acn-supermini-input-nav:hover { opacity: 1; }
#acn-supermini-input-nav.acn-visible { display: flex; }
.acn-supermini-input-host { position: relative !important; }
.acn-supermini-input-btn { display: flex; align-items: center; justify-content: center; width: 22px; height: 22px; padding: 0; border: none; border-radius: 5px; background: transparent; color: inherit; cursor: pointer; font-size: 13px; line-height: 1; }
.acn-supermini-input-btn:hover:not(:disabled) { background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 12%, transparent); }
.acn-supermini-input-btn:disabled { opacity: 0.28; cursor: default; }
#acn-supermini-input-counter { min-width: 48px; text-align: center; font-variant-numeric: tabular-nums; }
#supermini-message-panel { position: absolute; right: 100%; top: 0; width: 403px; height: auto; max-height: calc(100vh - 20px); box-sizing: border-box; background-color: var(--acn-nav-bg, Canvas); color: var(--acn-nav-text, CanvasText); border: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 20%, transparent); border-radius: 6px; padding: 3px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: none; flex-direction: column; z-index: 10000; font-family: sans-serif; overflow: hidden; }
#supermini-scroll-inner { flex: 0 1 auto; min-height: 0; max-height: none; overflow-y: auto; scrollbar-width: none; -ms-overflow-style: none; }
#supermini-scroll-inner::-webkit-scrollbar { display: none; }
#message-nav.supermini-bar:hover #supermini-message-panel,
#supermini-message-panel:hover { display: flex; }
#supermini-bookmarks-row { display: none; flex: 0 0 auto; align-items: center; gap: 4px; flex-wrap: wrap; max-height: 72px; overflow-y: auto; padding: 2px 2px 6px; margin-bottom: 4px; border-bottom: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 12%, transparent); scrollbar-width: none; }
#supermini-bookmarks-row::-webkit-scrollbar { display: none; }
#supermini-bookmarks-row.has-bookmarks { display: flex; }
#supermini-bookmarks-row .nav-bookmark-item { background-color: #fff59d; color: black; padding: 2px 5px; border-radius: 3px; cursor: pointer; font-size: 14px; user-select: none; }
#supermini-bookmarks-row .nav-bookmark-item:hover { background-color: #fff176; }
#supermini-bookmarks-row .nav-bookmark-item.active { font-weight: bold; background-color: #ffeb3b; }
#supermini-message-panel ul { list-style: none; padding: 0; margin: 0;}
#supermini-message-panel li { padding: 3px 2px; cursor: pointer; border-radius: 3px; font-size: calc(var(--acn-font-size-supermini, 15px) * var(--acn-idx-scale-supermini, 1)); font-family: var(--acn-idxfont-family, sans-serif); display: flex; align-items: center; gap: 4px; color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 75%, transparent); transition: background-color 0.15s; }
#supermini-message-panel li:hover { background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 12%, transparent); color: var(--acn-nav-text, CanvasText); }
#supermini-message-panel li.active { font-weight: bold; background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 10%, transparent);color: var(--acn-nav-text, CanvasText); }
#supermini-message-panel li .nav-superm-num.bookmarked { background-color: #fff59d; color: black; padding: 1px 3px; border-radius: 3px; }
#supermini-message-panel li .nav-superm-num { flex: 0 0 auto; width: auto; text-align: left; user-select: none; white-space: nowrap; }
#supermini-message-panel li .nav-superm-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1;}
.nav-superm-userfiles,
.acn-genfiles-row-sm { display: inline-flex; align-items: center; gap: 2px; flex-shrink: 0; margin-right: 2px; }
.nav-superm-userfile-badge,
.acn-genfiles-row-sm .acn-genfile-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 3px; border-radius: 5px; font-size: 11px; font-weight: 600; line-height: 1; cursor: pointer; background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 12%, transparent); color: var(--acn-nav-text, CanvasText); border: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 20%, transparent); box-sizing: border-box; transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.12s ease, opacity 0.15s ease; }
.nav-superm-userfile-badge:hover,
.acn-genfiles-row-sm .acn-genfile-badge:hover { background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 15%, transparent); border-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 35%, transparent); }
.nav-superm-userfile-badge:active,
.acn-genfiles-row-sm .acn-genfile-badge:active { transform: translateY(0); }
#supermini-message-panel .nav-superm-number-wrapper { display: inline-flex; align-items: center; gap: 0; min-width: 38px; justify-content: flex-start; position: relative; flex-shrink: 0; }
#supermini-message-panel .nav-superm-version-arrows { display: inline-flex; align-items: center; gap: 1px; position: relative; }
#supermini-message-panel .nav-superm-version-label { position: absolute; top: -7px; left: 50%; transform: translateX(-50%); font-size: 8px; line-height: 1; font-weight: 600; color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 60%, transparent); user-select: none; pointer-events: none; white-space: nowrap; }
#supermini-message-panel .nav-superm-version-btn { background: none; border: none; padding: 0 1px; font-size: 9px; cursor: pointer; color: var(--acn-nav-text, CanvasText); opacity: 0.5; display: flex; align-items: center; justify-content: center; width: 12px; flex-shrink: 0; }
#supermini-message-panel .nav-superm-version-btn:hover { opacity: 1; background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 15%, transparent); border-radius: 2px; }
#supermini-message-panel .nav-superm-version-btn:disabled { opacity: 0.15; cursor: not-allowed; }
`);
// 03.6 Shared badges and inline metadata
// ---------------------------------------------------------------------------
GM_addStyle(`.acn-message-inline-badge { display: inline-flex; align-items: center; align-self: flex-start; width: fit-content; font-size: 12px; font-weight: 400; padding: 3px 4px 1px 4px; border-radius: 6px; margin: 0 6px 0 0; vertical-align: 0px; letter-spacing: 0.04em; opacity: 0.9; user-select: none; transition: background-color 0.15s ease, border-color 0.15s ease, opacity 0.15s ease, color 0.15s ease; background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 12%, transparent); color: var(--acn-nav-text, CanvasText); border: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 20%, transparent); line-height: 1.4; box-sizing: border-box; }
.acn-message-inline-badge:hover { opacity: 1; background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 15%, transparent); border-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 35%, transparent); }
.acn-message-inline-badge:active { transform: translateY(0); }
#acn-mini-tooltip .nav-file-tag { font-weight: 400; padding: 3px 4px 1px 4px; vertical-align: 0px; }
.acn-message-inline-badge.bookmarked { background-color: #fff59d !important; color: black !important; border-color: #fbc02d !important; }
.acn-message-inline-badge-corner { position: absolute; top: -6px; left: -6px; margin: 0; z-index: 2; pointer-events: none; }
.acn-message-inline-badge-corner-inset { position: absolute; top: 4px; left: 4px; margin: 0; z-index: 2; pointer-events: none; }
#acn-history-status { position: fixed; top: 10px; right: calc(${NAV_WIDTH}px + 12px); z-index: 10000; display: none; padding: 6px 10px; border-radius: 8px; background: var(--acn-nav-bg-solid, Canvas); color: var(--acn-nav-text, CanvasText); border: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 20%, transparent); box-shadow: 0 4px 12px rgba(0,0,0,0.15); font-family: sans-serif; font-size: 14px; pointer-events: none; }
`);
if (allUserSelectors) {
GM_addStyle(`${allUserSelectors} { scroll-margin-top: 10px !important; }`);
}
// 03.7 Generic dateline presentation styles
if (siteUsesDateLine()) {
GM_addStyle(`
#nav-mini-msg-numbers { position: relative; }
#message-nav.max-bar #nav-list { z-index: auto; }
#message-nav.max-bar .nav-max-item { --acn-row-bg: var(--acn-nav-bg, Canvas); padding-left: calc(12px * var(--acn-date-scale-max, 1)); z-index: auto; background-color: var(--acn-row-bg); }
#message-nav.max-bar .nav-max-item.active { --acn-row-bg: color-mix(in srgb, var(--acn-nav-text, CanvasText) 10%, var(--acn-nav-bg, Canvas)); background-color: var(--acn-row-bg); }
#message-nav.max-bar .nav-max-number { position: relative; z-index: 3; background-color: var(--acn-nav-bg-solid, var(--acn-row-bg)); border-radius: 3px; box-shadow: 0 0 0 3px var(--acn-nav-bg-solid, var(--acn-row-bg)); }
#message-nav.max-bar .nav-max-item.active .nav-max-number { background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 10%, var(--acn-nav-bg-solid, Canvas)); box-shadow: 0 0 0 3px color-mix(in srgb, var(--acn-nav-text, CanvasText) 10%, var(--acn-nav-bg-solid, Canvas)); }
#message-nav.max-bar .nav-max-content { position: relative; z-index: 3; }
#supermini-scroll-inner { position: relative; }
#supermini-message-panel li { padding: 3px 8px 3px calc(18px * var(--acn-date-scale-supermini, 1)); gap: 4px; }
.acn-date-decor-layer { position: absolute; top: 0; left: 0; right: 0; pointer-events: none; z-index: 0; }
#message-nav.max-bar .acn-date-decor-layer { z-index: 2; }
.acn-date-line { position: absolute; width: 0; border-left-style: var(--acn-date-line-style, dashed); }
.acn-date-label { position: absolute; writing-mode: vertical-rl; color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 55%, transparent); white-space: nowrap; letter-spacing: 0.5px; font-family: var(--acn-idxfont-family, sans-serif); }
#message-nav.mini-bar .acn-date-line { right: calc(16px * var(--acn-idx-scale-mini, 1)); border-left-width: calc(2px * var(--acn-idx-scale-mini, 1)); }
#message-nav.mini-bar.nav-left .acn-date-line { right: auto; left: calc(16px * var(--acn-idx-scale-mini, 1)); }
#message-nav.mini-bar .acn-date-label { font-size: calc(13px * var(--acn-date-scale-mini, 1)); right: -2px; transform: translateY(-50%); }
#message-nav.mini-bar.nav-left .acn-date-label { right: auto; left: 0; transform: translateY(-50%) rotate(180deg); }
#message-nav.max-bar .acn-date-line { left: calc(20px * var(--acn-idx-scale-max, 1)); z-index: 2; border-left-width: calc(2px * var(--acn-idx-scale-max, 1)); }
#message-nav.max-bar .acn-date-label { font-size: calc(13px * var(--acn-date-scale-max, 1)); left: -1px; transform: translateY(-50%) rotate(180deg); z-index: 3; }
#supermini-message-panel .acn-date-line { left: calc(12px * var(--acn-idx-scale-supermini, 1)); border-left-width: calc(2px * var(--acn-idx-scale-supermini, 1)); }
#supermini-message-panel .acn-date-label { font-size: calc(13px * var(--acn-date-scale-supermini, 1)); left: -5px; transform: translateY(-50%) rotate(180deg); z-index: 3; }
#acn-mini-tooltip .nav-item-date-line { display: block; margin-top: 6px; font-size: 11px; color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 55%, transparent); }
`);
}
// 03.8 File-badge presentation (Claude, ChatGPT, Gemini)
// ---------------------------------------------------------------------------
if (IS_CLAUDE || IS_CHATGPT || IS_GEMINI || IS_KIMI || IS_GROK) {
GM_addStyle(`
#message-nav.mini-bar .nav-mini-item-wrapper { width: 29px; }
.nav-mini-num { position: relative; }
.acn-genfile-indicator { position: absolute; right: 2px; bottom: -2px; width: 8px !important; height: 8px !important; min-width: 8px !important; min-height: 8px !important; max-width: 8px !important; max-height: 8px !important; object-fit: contain !important; display: block !important; border-radius: 2px; pointer-events: none; opacity: 0.9; background: transparent; box-shadow: none; z-index: 2; }
.acn-genfiles-row-sm { display: flex; align-items: center; gap: 3px; margin-left: auto; flex-shrink: 0; }
#message-nav.max-bar .nav-max-item { flex-wrap: wrap; }
.acn-genfiles-row { flex: 0 0 100%; width: 100%; display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 4px; padding-left: 20px; position: relative; z-index: 3; box-sizing: border-box; }
.acn-genfiles-row-favicon { width: 12px !important; height: 12px !important; min-width: 12px !important; min-height: 12px !important; max-width: 12px !important; max-height: 12px !important; object-fit: contain !important; display: block !important; flex: 0 0 12px !important; }
.acn-genfile-badge { display: inline-flex !important; align-items: center; flex-wrap: nowrap; white-space: nowrap; gap: 0; cursor: pointer; overflow: hidden; }
.acn-genfile-badge-favicon { width: 12px !important; height: 12px !important; min-width: 12px !important; min-height: 12px !important; max-width: 12px !important; max-height: 12px !important; object-fit: contain !important; display: block !important; flex: 0 0 12px !important; }
.acn-genfile-badge-ext { display: inline-flex; align-items: center; flex: 0 0 auto; white-space: nowrap; line-height: inherit; }
#supermini-message-panel .acn-genfiles-row-sm { gap: 2px; }
#supermini-message-panel .acn-genfiles-row-sm .acn-genfiles-row-favicon { width: 10px !important; height: 10px !important; min-width: 10px !important; min-height: 10px !important; max-width: 10px !important; max-height: 10px !important; flex: 0 0 10px !important; margin-right: 1px; }
#supermini-message-panel .acn-genfile-badge { gap: 0; }
#supermini-message-panel .acn-genfile-badge-ext { display: inline-flex; }
#supermini-message-panel .acn-genfiles-row-sm .acn-genfile-badge { min-width: 16px; height: 16px; padding: 0 3px; border: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 20%, transparent) !important; border-radius: 5px; background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 12%, transparent) !important; box-shadow: none !important; }
#supermini-message-panel .acn-genfiles-row-sm .acn-genfile-badge:hover { background-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 15%, transparent) !important; border-color: color-mix(in srgb, var(--acn-nav-text, CanvasText) 35%, transparent) !important; }
@keyframes acn-generated-file-target-pulse {
0%, 100% { outline-color: color-mix(in srgb, CanvasText 30%, transparent); }
50% { outline-color: color-mix(in srgb, CanvasText 85%, transparent); }
}
.acn-generated-file-target { outline: 2px solid color-mix(in srgb, CanvasText 70%, transparent) !important; outline-offset: 4px !important; border-radius: 8px !important; animation: acn-generated-file-target-pulse 0.45s ease-in-out 3; }
`);
}
GM_addStyle(`@keyframes acn-restore-spin { to { transform: rotate(360deg); } }
#acn-restore-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 2147483646; display: flex; align-items: center; justify-content: center; pointer-events: none; }
#acn-restore-overlay .acn-restore-box { display: flex; align-items: center; gap: 10px; padding: 10px 16px; border-radius: 8px; background-color: var(--acn-nav-bg-solid, Canvas); border: 1px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 20%, transparent); color: var(--acn-nav-text, CanvasText); font-size: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.15); }
#acn-restore-overlay .acn-restore-spinner { width: 14px; height: 14px; border-radius: 50%; border: 2px solid color-mix(in srgb, var(--acn-nav-text, CanvasText) 25%, transparent); border-top-color: var(--acn-nav-text, CanvasText); animation: acn-restore-spin 0.7s linear infinite; }
#acn-restore-label { white-space: pre-line; }
`);
if (IS_CLAUDE) {
// 03.9 Claude-specific styles
// ---------------------------------------------------------------------------
GM_addStyle(`#message-nav { z-index: 8999; }
#message-nav.mini-bar { width: 52px; }
#message-nav.mini-bar #nav-btn-mini { width: 52px; }
#message-nav.mini-bar #nav-mini-msg-numbers { align-items: flex-start; padding-left: 3px; }
#message-nav.mini-bar.nav-left #nav-mini-msg-numbers { align-items: flex-end; padding-left: 0; padding-right: 3px; }
#message-nav.mini-bar .nav-mini-num { padding: 1px 2px 1px 0px; }
#message-nav.mini-bar.nav-left .nav-mini-num { padding: 1px 0px 1px 2px; }
#message-nav.mini-bar .nav-mini-num.bookmarked { padding: 3px 1px 0px 1px; }
#message-nav.max-bar .nav-max-item.acn-clicked { color: #eab308 !important; font-weight: bold !important; }
.nav-mini-num.acn-clicked { color: #eab308 !important; font-weight: bold !important; }
`);
}
if (IS_CHATGPT) {
GM_addStyle(`#message-nav.mini-bar { width: 52px; }
#message-nav.mini-bar #nav-btn-mini { width: 52px; }
#message-nav.mini-bar #nav-mini-msg-numbers { align-items: flex-start; padding-left: 3px; }
#message-nav.mini-bar.nav-left #nav-mini-msg-numbers { align-items: flex-end; padding-left: 0; padding-right: 3px; }
#message-nav.mini-bar .nav-mini-num { padding: 1px 2px 1px 0px; }
#message-nav.mini-bar.nav-left .nav-mini-num { padding: 1px 0px 1px 2px; }
#message-nav.mini-bar .nav-mini-num.bookmarked { padding: 3px 1px 0px 1px; }
`);
}
// 03.10 Settings helpers and site-specific static states
// ---------------------------------------------------------------------------
GM_addStyle(`
#nav-idxfont-face-options { display: flex; flex-direction: column; gap: 2px; }
.acn-idxfont-custom-area { display: none; }
.acn-idxfont-custom-area.is-visible { display: block; }
.acn-idxfont-custom-row { display: flex; align-items: center; gap: 6px; }
.acn-idxfont-custom-input { margin-top: 0; flex: 1 1 auto; }
.acn-idxfont-match-tick { font-size: 16px; flex-shrink: 0; width: 18px; text-align: center; }
.acn-idxfont-match-tick.is-available { color: #2e7d32; }
.acn-idxfont-match-tick.is-unavailable { color: #c62828; }
.acn-theme-option-btn.acn-text-center { text-align: center; }
html.acn-claude-css-strip [data-autoscroll-container="true"] *,
html.acn-claude-css-strip [data-autoscroll-container="true"] *::before,
html.acn-claude-css-strip [data-autoscroll-container="true"] *::after { animation: none !important; transition: none !important; box-shadow: none !important; filter: none !important; backdrop-filter: none !important; -webkit-backdrop-filter: none !important; }
[data-acn-gpt-strip] *,
[data-acn-gpt-strip] *::before,
[data-acn-gpt-strip] *::after { animation: none !important; transition: none !important; box-shadow: none !important; filter: none !important; backdrop-filter: none !important; -webkit-backdrop-filter: none !important; }
html.acn-claude-dialog-hidden [role="dialog"],
html.acn-claude-dialog-hidden body > *:has([role="dialog"]) { visibility: hidden !important; }
html.acn-claude-dialog-hidden,
html.acn-claude-dialog-hidden body { overflow: visible !important; padding-right: 0 !important; }
`);
// 03.11 Default settings (theme, bar typography, and settings panel)
// ---------------------------------------------------------------------------
GM_addStyle(`:root {
--acn-default-theme: website;
--acn-font-size-mini: ${DEFAULT_SETTINGS.font.mini.baseSize}px;
--acn-font-size-max: ${DEFAULT_SETTINGS.font.max.baseSize}px;
--acn-counter-size-max: ${DEFAULT_SETTINGS.font.max.counterSize}px;
--acn-font-size-supermini: ${DEFAULT_SETTINGS.font.supermini.baseSize}px;
}`);
}
// ============================================================================
// 04. SHARED TIMERS AND GENERAL UTILITIES
// ============================================================================
function safeSetTimeout(callback, delay) {
let start = null;
let frameId;
function step(timestamp) {
if (!start) start = timestamp;
if (timestamp - start >= delay) {
callback();
} else {
frameId = window.requestAnimationFrame(step);
}
}
frameId = window.requestAnimationFrame(step);
return { clear: () => window.cancelAnimationFrame(frameId) };
}
function safeSetInterval(callback, delay) {
let start = null;
let frameId;
function step(timestamp) {
if (!start) start = timestamp;
if (timestamp - start >= delay) {
callback();
start = timestamp;
}
frameId = window.requestAnimationFrame(step);
}
frameId = window.requestAnimationFrame(step);
return { clear: () => window.cancelAnimationFrame(frameId) };
}
// ============================================================================
// 05. SETTINGS DIALOGS
// ============================================================================
// 05.1 Settings state and persistence
// ---------------------------------------------------------------------------
function loadIdxFontSettings() {
try {
const raw = GM_getValue(getFontSettingsKey(), null);
const merged = JSON.parse(JSON.stringify(IDXFONT_DEFAULTS));
if (raw) {
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
['mini', 'max', 'supermini'].forEach(function (mode) {
if (!parsed || !parsed[mode] || typeof parsed[mode] !== 'object') return;
const idxScale = Number(parsed[mode].idxScale);
const dateScale = Number(parsed[mode].dateScale);
if (Number.isFinite(idxScale) && idxScale > 0) merged[mode].idxScale = idxScale;
if (Number.isFinite(dateScale) && dateScale > 0) merged[mode].dateScale = dateScale;
});
if (parsed && typeof parsed.face === 'string') merged.face = parsed.face;
if (merged.face === 'default') merged.face = 'system';
if (parsed && typeof parsed.customFamily === 'string') merged.customFamily = parsed.customFamily;
}
return merged;
} catch (e) {
return JSON.parse(JSON.stringify(IDXFONT_DEFAULTS));
}
}
function getFontSettingsKey() {
return navFontAndSizeScope === 'global' ? NAV_FONT_SETTINGS_GLOBAL_KEY : NAV_FONT_SETTINGS_STORAGE_KEY;
}
function setFontAndSizeScope(scope) {
navFontAndSizeScope = scope === 'per-site' ? 'per-site' : 'global';
GM_setValue(NAV_FONT_AND_SIZE_SCOPE_KEY, navFontAndSizeScope);
idxFontSettings = loadIdxFontSettings();
applyIdxFontSettings();
}
function saveIdxFontSettings() {
GM_setValue(getFontSettingsKey(), JSON.stringify(idxFontSettings));
}
function idxFontFamilyStack() {
const s = idxFontSettings;
if (s.face === 'system') return 'sans-serif';
if (s.face === 'serif') return 'serif';
if (s.face === 'sans-serif') return 'sans-serif';
if (s.face === 'monospace') return 'monospace';
if (s.face === 'custom' && s.customFamily) {
const safeName = s.customFamily.replace(/["\\]/g, '').trim();
return safeName ? '"' + safeName + '", sans-serif' : 'sans-serif';
}
return 'sans-serif';
}
function applyIdxFontSettings() {
const root = document.documentElement;
['mini', 'max', 'supermini'].forEach(function (mode) {
const g = (idxFontSettings && idxFontSettings[mode]) || IDXFONT_DEFAULTS[mode];
const idxRatio = (typeof g.idxScale === 'number' && g.idxScale > 0) ? g.idxScale : 1;
const dateRatio = (typeof g.dateScale === 'number' && g.dateScale > 0) ? g.dateScale : 1;
root.style.setProperty('--acn-idx-scale-' + mode, String(idxRatio));
root.style.setProperty('--acn-date-scale-' + mode, String(dateRatio));
});
root.style.setProperty('--acn-idxfont-family', idxFontFamilyStack());
}
function setIdxFontModeSetting(mode, patch) {
if (!idxFontSettings[mode]) return;
Object.assign(idxFontSettings[mode], patch);
saveIdxFontSettings();
applyIdxFontSettings();
if (typeof updateMessageList === 'function') updateMessageList(true);
}
function setIdxFontFace(face, customFamily) {
idxFontSettings.face = face;
if (typeof customFamily === 'string') idxFontSettings.customFamily = customFamily;
saveIdxFontSettings();
applyIdxFontSettings();
}
function resetIdxFontSettings() {
idxFontSettings = JSON.parse(JSON.stringify(IDXFONT_DEFAULTS));
saveIdxFontSettings();
applyIdxFontSettings();
if (typeof updateMessageList === 'function') updateMessageList(true);
}
function setDateLineStyle(style) {
if (!DATELINE_STYLE_OPTIONS.includes(style)) return;
dateLineStyle = style;
GM_setValue(NAV_DATELINE_STYLE_KEY, style);
document.documentElement.style.setProperty('--acn-date-line-style', dateLineStyleToCssValue(style));
if (typeof updateMessageList === 'function') updateMessageList(true);
}
function resetDefaultSettings() {
navThemeScope = DEFAULT_SETTINGS.theme.scope;
navTheme = DEFAULT_SETTINGS.theme.mode;
navCustomColor = DEFAULT_SETTINGS.theme.customColor;
navFontAndSizeScope = DEFAULT_SETTINGS.font.scope;
idxFontSettings = JSON.parse(JSON.stringify(IDXFONT_DEFAULTS));
dateLineStyle = DATELINE_STYLE_DEFAULT;
GM_setValue(NAV_THEME_SCOPE_KEY, navThemeScope);
GM_setValue(NAV_THEME_GLOBAL_KEY, navTheme);
GM_setValue(NAV_THEME_STORAGE_KEY, navTheme);
GM_setValue(NAV_CUSTOM_COLOR_GLOBAL_KEY, navCustomColor);
GM_setValue(NAV_CUSTOM_COLOR_STORAGE_KEY, navCustomColor);
GM_setValue(NAV_FONT_AND_SIZE_SCOPE_KEY, navFontAndSizeScope);
GM_setValue(NAV_FONT_SETTINGS_GLOBAL_KEY, JSON.stringify(idxFontSettings));
GM_setValue(NAV_FONT_SETTINGS_STORAGE_KEY, JSON.stringify(idxFontSettings));
GM_setValue(NAV_DATELINE_STYLE_KEY, dateLineStyle);
document.documentElement.style.setProperty('--acn-date-line-style', dateLineStyleToCssValue(dateLineStyle));
applyNavTheme();
applyIdxFontSettings();
applyHeaderSolidBackgroundClaude();
initThemeAutoWatcherClaude();
if (typeof updateMessageList === 'function') updateMessageList(true);
}
// 05.2 Theme and color settings
// ---------------------------------------------------------------------------
// 05.3 Font and size settings
// ---------------------------------------------------------------------------
function isDarkBackgroundColor(color) {
const value = String(color || '').trim();
const rgb = value.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i);
let r, g, b;
if (rgb) {
r = Number(rgb[1]);
g = Number(rgb[2]);
b = Number(rgb[3]);
} else {
const colorFn = value.match(/^color\(\s*(?:srgb|srgb-linear|display-p3|a98-rgb|prophoto-rgb|rec2020)\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)/i);
if (colorFn) {
r = Number(colorFn[1]) * 255;
g = Number(colorFn[2]) * 255;
b = Number(colorFn[3]) * 255;
} else {
const hex = value.match(/^#([0-9a-f]{6})$/i);
if (!hex) return false;
const parsed = parseInt(hex[1], 16);
r = (parsed >> 16) & 255;
g = (parsed >> 8) & 255;
b = parsed & 255;
}
}
return (0.2126 * r + 0.7152 * g + 0.0722 * b) < 140;
}
function applyNavTheme() {
const container = document.getElementById('message-nav');
if (!container) return;
let bg = null;
if (navTheme === 'website') {
bg = extractPageBackgroundColorDOMClaude() || 'Canvas';
} else if (navTheme === 'dark' || navTheme === 'light') {
bg = CLAUDE_NAV_THEME_PALETTE[navTheme];
} else if (navTheme === 'transparent') {
bg = 'transparent';
} else if (navTheme === 'custom') {
bg = navCustomColor;
}
if (!bg) bg = 'Canvas';
container.style.setProperty('--acn-nav-bg', bg);
const solidBg = navTheme === 'website'
? (extractPageBackgroundColorDOMClaude() || 'Canvas')
: extractProtectedNavBgDOMClaude();
document.documentElement.style.setProperty('--acn-nav-bg-solid', solidBg);
let effectiveBg = bg;
if (effectiveBg === 'transparent' || effectiveBg === 'Canvas') {
effectiveBg = (navTheme === 'website' || navTheme === 'transparent')
? (extractPageBackgroundColorDOMClaude() || '#ffffff')
: '#ffffff';
}
const systemScheme = navTheme === 'dark'
? 'dark'
: navTheme === 'light'
? 'light'
: (isDarkBackgroundColor(effectiveBg) ? 'dark' : 'light');
container.style.colorScheme = systemScheme;
['acn-mini-tooltip', 'acn-restore-overlay', 'acn-history-status', 'nav-theme-panel'].forEach((id) => {
const el = document.getElementById(id);
if (el) el.style.colorScheme = systemScheme;
});
document.querySelectorAll('.acn-settings-select').forEach((el) => {
el.style.colorScheme = systemScheme;
});
document.documentElement.style.setProperty('--acn-nav-text', 'CanvasText');
}
// 05.4 Settings dialog construction
// ---------------------------------------------------------------------------
function isFontAvailableClaude(fontName) {
const name = (fontName || '').trim();
if (!name) return false;
const testString = 'mmmmmmmmmmlli';
const testSize = '72px';
const baselineFonts = ['monospace', 'sans-serif', 'serif'];
const canvas = isFontAvailableClaude._canvas || (isFontAvailableClaude._canvas = document.createElement('canvas'));
const ctx = canvas.getContext('2d');
const safeName = name.replace(/["\\]/g, '');
return baselineFonts.some((baseline) => {
ctx.font = testSize + ' ' + baseline;
const baseWidth = ctx.measureText(testString).width;
ctx.font = testSize + ' "' + safeName + '", ' + baseline;
const candidateWidth = ctx.measureText(testString).width;
return candidateWidth !== baseWidth;
});
}
function createIdxFontSubPanelClaude() {
const panel = document.createElement('div');
panel.id = 'nav-idxfont-subpanel';
panel.addEventListener('click', (e) => e.stopPropagation());
CLAUDE_IDXFONT_MODE_GROUPS.forEach((group, groupIdx) => {
const label = document.createElement('div');
label.className = 'acn-idxfont-group-label';
label.textContent = group.label;
panel.appendChild(label);
const g = idxFontSettings[group.id];
const idxRow = document.createElement('div');
idxRow.className = 'acn-idxfont-row';
const idxLabel = document.createElement('span');
idxLabel.className = 'acn-idxfont-row-label';
idxLabel.textContent = group.hasCounter ? 'Entries + counter' : 'Entries';
const idxInput = document.createElement('input');
idxInput.type = 'number';
idxInput.min = '50'; idxInput.max = '200'; idxInput.step = '5';
idxInput.className = 'acn-idxfont-size-input';
idxInput.value = Math.round(g.idxScale * 100);
const idxUnit = document.createElement('span');
idxUnit.className = 'acn-idxfont-size-unit';
idxUnit.textContent = '%';
idxInput.addEventListener('change', () => {
let pct = parseInt(idxInput.value, 10);
if (isNaN(pct) || pct <= 0) pct = 100;
pct = Math.max(50, Math.min(200, pct));
idxInput.value = pct;
setIdxFontModeSetting(group.id, { idxScale: pct / 100 });
});
idxRow.appendChild(idxLabel);
idxRow.appendChild(idxInput);
idxRow.appendChild(idxUnit);
panel.appendChild(idxRow);
const dateRow = document.createElement('div');
dateRow.className = 'acn-idxfont-row';
const dateLabel = document.createElement('span');
dateLabel.className = 'acn-idxfont-row-label';
dateLabel.textContent = 'Date label';
const dateInput = document.createElement('input');
dateInput.type = 'number';
dateInput.min = '50'; dateInput.max = '200'; dateInput.step = '5';
dateInput.className = 'acn-idxfont-size-input';
dateInput.value = Math.round(g.dateScale * 100);
const dateUnit = document.createElement('span');
dateUnit.className = 'acn-idxfont-size-unit';
dateUnit.textContent = '%';
dateInput.addEventListener('change', () => {
let pct = parseInt(dateInput.value, 10);
if (isNaN(pct) || pct <= 0) pct = 100;
pct = Math.max(50, Math.min(200, pct));
dateInput.value = pct;
setIdxFontModeSetting(group.id, { dateScale: pct / 100 });
});
dateRow.appendChild(dateLabel);
dateRow.appendChild(dateInput);
dateRow.appendChild(dateUnit);
panel.appendChild(dateRow);
if (group.id === 'mini') {
const hint = document.createElement('div');
hint.className = 'acn-idxfont-hint';
hint.textContent = 'Mini navbar has a fixed total width. A larger date label here may get clipped rather than making room for itself.';
panel.appendChild(hint);
}
const divider = document.createElement('div');
divider.className = 'acn-theme-divider';
panel.appendChild(divider);
});
const faceLabel = document.createElement('div');
faceLabel.className = 'acn-idxfont-group-label';
faceLabel.textContent = 'Font face';
panel.appendChild(faceLabel);
const faceOptionsWrap = document.createElement('div');
faceOptionsWrap.id = 'nav-idxfont-face-options';
const customArea = document.createElement('div');
customArea.className = 'acn-idxfont-custom-area';
customArea.classList.toggle('is-visible', idxFontSettings.face === 'custom');
const customInputRow = document.createElement('div');
customInputRow.className = 'acn-idxfont-custom-row';
const customInput = document.createElement('input');
customInput.type = 'text';
customInput.className = 'acn-idxfont-custom-input';
customInput.placeholder = 'e.g. Times New Roman';
customInput.value = idxFontSettings.customFamily || '';
const matchTick = document.createElement('span');
matchTick.className = 'acn-idxfont-match-tick';
function updateMatchTick() {
const name = customInput.value.trim();
if (!name) { matchTick.textContent = ''; return; }
const available = isFontAvailableClaude(name);
matchTick.textContent = available ? '✓' : '✗';
matchTick.classList.toggle('is-available', available);
matchTick.classList.toggle('is-unavailable', !available);
matchTick.title = available ? 'Font found' : 'No matching font found, check the spelling';
}
updateMatchTick();
let matchTickDebounce = null;
customInput.addEventListener('input', () => {
clearTimeout(matchTickDebounce);
matchTickDebounce = setTimeout(updateMatchTick, 150);
});
customInput.addEventListener('change', () => {
setIdxFontFace('custom', customInput.value);
updateMatchTick();
});
customInput.addEventListener('keyup', (e) => {
if (e.key === 'Enter') {
setIdxFontFace('custom', customInput.value);
updateMatchTick();
}
});
customInputRow.appendChild(customInput);
customInputRow.appendChild(matchTick);
const hint = document.createElement('div');
hint.className = 'acn-idxfont-hint';
hint.textContent = 'Type the font name exactly as your system lists it. Capitalization and spacing matter (for example: "Times New Roman"). The checkmark shows whether a matching installed font was found. Enter to apply.';
customArea.appendChild(customInputRow);
customArea.appendChild(hint);
CLAUDE_IDXFONT_FACE_OPTIONS.forEach((opt) => {
const btn = document.createElement('button');
btn.className = 'acn-theme-option-btn';
btn.textContent = opt.label;
btn.dataset.faceId = opt.id;
if (idxFontSettings.face === opt.id) btn.classList.add('active');
btn.addEventListener('click', (e) => {
e.stopPropagation();
setIdxFontFace(opt.id);
faceOptionsWrap.querySelectorAll('.acn-theme-option-btn').forEach(b => b.classList.toggle('active', b.dataset.faceId === opt.id));
customArea.classList.toggle('is-visible', opt.id === 'custom');
});
faceOptionsWrap.appendChild(btn);
});
panel.appendChild(faceOptionsWrap);
panel.appendChild(customArea);
const resetDivider = document.createElement('div');
resetDivider.className = 'acn-theme-divider';
panel.appendChild(resetDivider);
const resetBtn = document.createElement('button');
resetBtn.className = 'acn-theme-option-btn';
resetBtn.textContent = 'Reset';
resetBtn.classList.add('acn-text-center');
resetBtn.addEventListener('click', (e) => {
e.stopPropagation();
resetIdxFontSettings();
const fresh = createIdxFontSubPanelClaude();
fresh.style.display = panel.style.display;
fresh.style.position = panel.style.position;
fresh.style.top = panel.style.top;
fresh.style.bottom = panel.style.bottom;
fresh.style.left = panel.style.left;
fresh.style.right = panel.style.right;
fresh.style.marginTop = panel.style.marginTop;
fresh.style.marginBottom = panel.style.marginBottom;
if (panel.parentNode) panel.parentNode.replaceChild(fresh, panel);
});
panel.appendChild(resetBtn);
return panel;
}
// 05.5 Settings dialog interaction
// ---------------------------------------------------------------------------
function createSettingsPanelClaude() {
const panel = document.createElement('div');
panel.id = 'nav-theme-panel';
panel.className = 'acn-tabbed-settings';
['pointerdown', 'mousedown', 'mouseup', 'click', 'dblclick'].forEach((eventName) => {
panel.addEventListener(eventName, (e) => e.stopPropagation());
});
let dateLineStyleSelect = null;
const titlebar = document.createElement('div');
titlebar.className = 'acn-settings-titlebar';
const title = document.createElement('span');
title.textContent = 'Settings';
const closeBtn = document.createElement('button');
closeBtn.className = 'acn-settings-close';
closeBtn.textContent = SYMBOL_CLOSE;
closeBtn.title = 'Close';
titlebar.appendChild(title);
titlebar.appendChild(closeBtn);
const tabs = document.createElement('div');
tabs.className = 'acn-settings-tabs';
const themeTab = document.createElement('button');
themeTab.className = 'acn-settings-tab active';
themeTab.textContent = SYMBOL_THEME + ' Theme';
const fontTab = document.createElement('button');
fontTab.className = 'acn-settings-tab';
fontTab.textContent = SYMBOL_FONT + ' Font';
tabs.appendChild(themeTab);
tabs.appendChild(fontTab);
const body = document.createElement('div');
body.className = 'acn-settings-body';
const themePage = document.createElement('div');
themePage.className = 'acn-settings-page active';
const fontPage = document.createElement('div');
fontPage.className = 'acn-settings-page';
function addField(page, labelText, control) {
const field = document.createElement('div');
field.className = 'acn-settings-field';
const label = document.createElement('label');
label.className = 'acn-settings-label';
label.textContent = labelText;
field.appendChild(label);
field.appendChild(control);
page.appendChild(field);
return field;
}
function createSelect(options, value) {
const select = document.createElement('select');
select.className = 'acn-settings-select';
options.forEach((option) => {
const item = document.createElement('option');
item.value = option.id;
item.textContent = option.label;
select.appendChild(item);
});
select.value = value;
return select;
}
function activateTab(name) {
const isTheme = name === 'theme';
themeTab.classList.toggle('active', isTheme);
fontTab.classList.toggle('active', !isTheme);
themePage.classList.toggle('active', isTheme);
fontPage.classList.toggle('active', !isTheme);
}
themeTab.addEventListener('click', () => activateTab('theme'));
fontTab.addEventListener('click', () => activateTab('font'));
const themeSelect = createSelect(CLAUDE_THEME_OPTIONS, navTheme);
const colorInput = document.createElement('input');
colorInput.type = 'color';
colorInput.id = 'nav-theme-custom-color';
colorInput.value = navCustomColor;
const scopeSelect = createSelect(CLAUDE_THEME_SCOPE_OPTIONS, navThemeScope);
scopeSelect.addEventListener('change', () => {
setNavThemeScope(scopeSelect.value);
themeSelect.value = navTheme;
colorInput.value = navCustomColor;
colorField.style.display = navTheme === 'custom' ? 'block' : 'none';
});
addField(themePage, 'Scope', scopeSelect);
addField(themePage, 'Theme mode', themeSelect);
const colorField = addField(themePage, 'Custom color', colorInput);
colorField.style.display = navTheme === 'custom' ? 'block' : 'none';
themeSelect.addEventListener('change', () => {
colorField.style.display = themeSelect.value === 'custom' ? 'block' : 'none';
setNavTheme(themeSelect.value, themeSelect.value === 'custom' ? colorInput.value : undefined);
});
colorInput.addEventListener('input', () => {
themeSelect.value = 'custom';
setNavTheme('custom', colorInput.value);
});
if (siteUsesDateLine()) {
const dateLineDivider = document.createElement('div');
dateLineDivider.className = 'acn-settings-divider';
themePage.appendChild(dateLineDivider);
const dateLineStyleOptions = DATELINE_STYLE_OPTIONS.map((id) => ({
id,
label: id.charAt(0).toUpperCase() + id.slice(1),
}));
dateLineStyleSelect = createSelect(dateLineStyleOptions, dateLineStyle);
addField(themePage, 'Date line style', dateLineStyleSelect);
const dateLineStyleHint = document.createElement('div');
dateLineStyleHint.className = 'acn-settings-hint';
dateLineStyleHint.style.fontSize = '12px';
dateLineStyleHint.textContent = 'Solid shows a small gap every 6 hours within a day, as a rough indicator of what time a message landed.';
themePage.appendChild(dateLineStyleHint);
dateLineStyleSelect.addEventListener('change', () => {
setDateLineStyle(dateLineStyleSelect.value);
});
}
if (IS_CHATGPT) {
const nativeTocDivider = document.createElement('div');
nativeTocDivider.className = 'acn-settings-divider';
themePage.appendChild(nativeTocDivider);
const nativeTocRow = document.createElement('label');
nativeTocRow.className = 'acn-settings-field';
nativeTocRow.style.display = 'flex';
nativeTocRow.style.flexDirection = 'row';
nativeTocRow.style.alignItems = 'center';
nativeTocRow.style.gap = '6px';
nativeTocRow.style.cursor = 'pointer';
const nativeTocCheckbox = document.createElement('input');
nativeTocCheckbox.type = 'checkbox';
nativeTocCheckbox.checked = getChatGPTHideNativeTocSetting();
nativeTocCheckbox.addEventListener('change', () => {
setChatGPTHideNativeTocSetting(nativeTocCheckbox.checked);
});
const nativeTocLabel = document.createElement('span');
nativeTocLabel.className = 'acn-settings-label';
nativeTocLabel.textContent = 'Hide native TOC';
nativeTocRow.appendChild(nativeTocCheckbox);
nativeTocRow.appendChild(nativeTocLabel);
themePage.appendChild(nativeTocRow);
}
const themeResetDivider = document.createElement('div');
themeResetDivider.className = 'acn-settings-divider';
themePage.appendChild(themeResetDivider);
const themeResetBtn = document.createElement('button');
themeResetBtn.className = 'acn-settings-reset';
themeResetBtn.textContent = 'Reset defaults';
themeResetBtn.addEventListener('click', () => {
resetDefaultSettings();
themeSelect.value = navTheme;
scopeSelect.value = navThemeScope;
colorInput.value = navCustomColor;
colorField.style.display = 'none';
if (dateLineStyleSelect) dateLineStyleSelect.value = dateLineStyle;
});
themePage.appendChild(themeResetBtn);
const fontScopeSelect = createSelect(CLAUDE_THEME_SCOPE_OPTIONS, navFontAndSizeScope);
const fontScopeField = addField(fontPage, 'Scope', fontScopeSelect);
const fontScopeHint = document.createElement('div');
fontScopeHint.className = 'acn-settings-hint';
fontScopeField.appendChild(fontScopeHint);
const fontOptions = CLAUDE_IDXFONT_FACE_OPTIONS
.map((option) => ({ id: option.id === 'default' ? 'system' : option.id, label: option.label }));
const selectedFace = idxFontSettings.face === 'default' ? 'system' : idxFontSettings.face;
const faceSelect = createSelect(fontOptions, selectedFace);
addField(fontPage, 'Font family', faceSelect);
const customWrap = document.createElement('div');
customWrap.className = 'acn-settings-inline';
const customInput = document.createElement('input');
customInput.type = 'text';
customInput.className = 'acn-settings-input';
customInput.placeholder = 'e.g. Calibri or JetBrains Mono';
customInput.value = idxFontSettings.customFamily || '';
const matchTick = document.createElement('span');
matchTick.className = 'acn-font-match';
function updateMatchTick() {
const name = customInput.value.trim();
if (!name) { matchTick.textContent = ''; matchTick.title = ''; return; }
const available = isFontAvailableClaude(name);
matchTick.textContent = available ? '✓' : '✗';
matchTick.classList.toggle('is-available', available);
matchTick.classList.toggle('is-unavailable', !available);
matchTick.title = available ? 'Font found' : 'No matching font found';
}
let matchTimer = null;
customInput.addEventListener('input', () => {
clearTimeout(matchTimer);
matchTimer = setTimeout(updateMatchTick, 150);
});
function applyCustomFont() {
const value = customInput.value.trim();
if (value) {
faceSelect.value = 'custom';
updateCustomFontVisibility();
setIdxFontFace('custom', value);
}
updateMatchTick();
}
customInput.addEventListener('change', applyCustomFont);
customInput.addEventListener('keyup', (e) => { if (e.key === 'Enter') applyCustomFont(); });
customWrap.appendChild(customInput);
customWrap.appendChild(matchTick);
const customField = addField(fontPage, 'Custom font (optional)', customWrap);
const customHint = document.createElement('div');
customHint.className = 'acn-settings-hint';
customHint.textContent = 'Type a font name and press Enter to apply it.';
customField.appendChild(customHint);
function updateCustomFontVisibility() {
customField.style.display = faceSelect.value === 'custom' ? 'block' : 'none';
}
faceSelect.addEventListener('change', () => {
updateCustomFontVisibility();
if (faceSelect.value === 'custom') {
const value = customInput.value.trim();
if (value) setIdxFontFace('custom', value);
} else {
setIdxFontFace(faceSelect.value);
}
});
updateCustomFontVisibility();
updateMatchTick();
const divider = document.createElement('div');
divider.className = 'acn-settings-divider';
fontPage.appendChild(divider);
const modeSelect = createSelect([
{ id: 'max', label: 'Maxbar' },
{ id: 'mini', label: 'Minibar' },
{ id: 'supermini', label: 'Superminibar' }
], 'max');
addField(fontPage, 'Bar mode', modeSelect);
function makePercentInput() {
const row = document.createElement('div');
row.className = 'acn-settings-inline';
const input = document.createElement('input');
input.type = 'number';
input.min = '50'; input.max = '200'; input.step = '5';
input.className = 'acn-settings-input acn-settings-percent';
const unit = document.createElement('span');
unit.textContent = '%';
row.appendChild(input); row.appendChild(unit);
return { row, input };
}
const entries = makePercentInput();
const dates = makePercentInput();
const sizeColumns = document.createElement('div');
sizeColumns.className = siteUsesDateLine()
? 'acn-settings-two-columns'
: 'acn-settings-one-column';
const entriesField = addField(sizeColumns, 'Entries + counter', entries.row);
if (siteUsesDateLine()) {
addField(sizeColumns, 'Date labels', dates.row);
}
fontPage.appendChild(sizeColumns);
function normalize(input) {
let value = parseInt(input.value, 10);
if (!Number.isFinite(value)) value = 100;
value = Math.max(50, Math.min(200, value));
input.value = String(value);
return value;
}
function updateEntriesLabel() {
const label = entriesField.querySelector('.acn-settings-label');
if (label) {
label.textContent = modeSelect.value === 'supermini'
? 'Entries'
: 'Entries + counter';
}
}
function loadModeValues() {
const settings = idxFontSettings[modeSelect.value] || IDXFONT_DEFAULTS[modeSelect.value];
entries.input.value = String(Math.round(settings.idxScale * 100));
dates.input.value = String(Math.round(settings.dateScale * 100));
}
modeSelect.addEventListener('change', () => {
updateEntriesLabel();
updateEntriesLabel();
loadModeValues();
});
let dateLineCorrectionTimer = null;
entries.input.addEventListener('input', () => {
const mode = modeSelect.value;
const rawPct = parseInt(entries.input.value, 10);
if (isNaN(rawPct) || rawPct <= 0) return;
const pct = rawPct;
document.documentElement.style.setProperty('--acn-idx-scale-' + mode, String(pct / 100));
const lastPct = dateLineLiveScaleLastPct[mode] || pct;
const ratio = pct / lastPct;
dateLineLiveScaleLastPct[mode] = pct;
if (ratio !== 1) {
['nav-list', 'nav-mini-msg-numbers', 'supermini-message-list'].forEach((id) => {
scaleNavDateDecorationsLive(id, ratio);
});
}
clearTimeout(dateLineCorrectionTimer);
dateLineCorrectionTimer = setTimeout(() => {
dateLineCorrectionTimer = null;
dateLineLiveScaleLastPct[mode] = null;
if (!siteUsesDateLine()) return;
const navList = document.getElementById('nav-list');
if (navList) renderNavDateDecorations(navList);
const miniNums = document.getElementById('nav-mini-msg-numbers');
if (miniNums) renderNavDateDecorations(miniNums);
const superList = document.getElementById('supermini-message-list');
if (superList) renderNavDateDecorations(superList);
}, 16);
});
entries.input.addEventListener('change', () => setIdxFontModeSetting(modeSelect.value, { idxScale: normalize(entries.input) / 100 }));
dates.input.addEventListener('input', () => {
const rawPct = parseInt(dates.input.value, 10);
if (isNaN(rawPct) || rawPct <= 0) return;
document.documentElement.style.setProperty('--acn-date-scale-' + modeSelect.value, String(rawPct / 100));
});
dates.input.addEventListener('change', () => setIdxFontModeSetting(modeSelect.value, { dateScale: normalize(dates.input) / 100 }));
loadModeValues();
fontScopeSelect.addEventListener('change', () => {
setFontAndSizeScope(fontScopeSelect.value);
faceSelect.value = idxFontSettings.face === 'default' ? 'system' : idxFontSettings.face;
customInput.value = idxFontSettings.customFamily || '';
updateCustomFontVisibility();
updateMatchTick();
modeSelect.value = 'max';
loadModeValues();
});
if (IS_CHATGPT) {
const nativeTocSpacer = document.createElement('div');
nativeTocSpacer.id = 'acn-font-tab-toc-spacer';
nativeTocSpacer.style.height = '27px';
fontPage.appendChild(nativeTocSpacer);
}
const resetDivider = document.createElement('div');
resetDivider.className = 'acn-settings-divider';
fontPage.appendChild(resetDivider);
const resetBtn = document.createElement('button');
resetBtn.className = 'acn-settings-reset';
resetBtn.textContent = 'Reset defaults';
resetBtn.addEventListener('click', () => {
resetDefaultSettings();
themeSelect.value = navTheme;
scopeSelect.value = navThemeScope;
colorInput.value = navCustomColor;
colorField.style.display = 'none';
fontScopeSelect.value = navFontAndSizeScope;
faceSelect.value = idxFontSettings.face;
customInput.value = idxFontSettings.customFamily;
updateCustomFontVisibility();
updateMatchTick();
modeSelect.value = 'max';
loadModeValues();
if (dateLineStyleSelect) dateLineStyleSelect.value = dateLineStyle;
});
fontPage.appendChild(resetBtn);
body.appendChild(themePage);
body.appendChild(fontPage);
panel.appendChild(titlebar);
panel.appendChild(tabs);
panel.appendChild(body);
closeBtn.addEventListener('click', () => {
panel.style.display = 'none';
document.getElementById('nav-theme-btn')?.classList.remove('acn-panel-open');
document.getElementById('message-nav')?.classList.remove('acn-panel-open');
});
return panel;
}
function positionThemePanelClaude() {
const panel = document.getElementById('nav-theme-panel');
if (!panel) return;
}
// 05.6 Claude appearance integration
// ---------------------------------------------------------------------------
function getNavThemeKey() {
return navThemeScope === 'global' ? NAV_THEME_GLOBAL_KEY : NAV_THEME_STORAGE_KEY;
}
function getNavCustomColorKey() {
return navThemeScope === 'global' ? NAV_CUSTOM_COLOR_GLOBAL_KEY : NAV_CUSTOM_COLOR_STORAGE_KEY;
}
function setNavTheme(theme, customColor) {
navTheme = theme;
GM_setValue(getNavThemeKey(), theme);
if (theme === 'custom' && customColor) {
navCustomColor = customColor;
GM_setValue(getNavCustomColorKey(), customColor);
}
applyNavTheme();
applyHeaderSolidBackgroundClaude();
const openPanel = document.getElementById('nav-theme-panel');
if (openPanel && openPanel.style.display === 'flex') {
openPanel.style.setProperty('background-color', extractProtectedNavBgDOMClaude(), 'important');
}
const bookmarksPanel = document.getElementById('nav-mini-bookmarks-panel');
if (bookmarksPanel) {
bookmarksPanel.style.setProperty('background-color', extractProtectedNavBgDOMClaude(), 'important');
}
const tooltipEl = document.getElementById('acn-mini-tooltip');
if (tooltipEl && tooltipEl.style.display !== 'none') {
tooltipEl.style.setProperty('background-color', extractProtectedNavBgDOMClaude(), 'important');
}
initThemeAutoWatcherClaude();
}
function setNavThemeScope(scope) {
navThemeScope = scope;
GM_setValue(NAV_THEME_SCOPE_KEY, scope);
navTheme = GM_getValue(getNavThemeKey(), DEFAULT_SETTINGS.theme.mode);
navCustomColor = GM_getValue(getNavCustomColorKey(), DEFAULT_SETTINGS.theme.customColor);
applyNavTheme();
applyHeaderSolidBackgroundClaude();
const openPanel = document.getElementById('nav-theme-panel');
if (openPanel && openPanel.style.display === 'flex') {
openPanel.style.setProperty('background-color', extractProtectedNavBgDOMClaude(), 'important');
}
const bookmarksPanel = document.getElementById('nav-mini-bookmarks-panel');
if (bookmarksPanel) {
bookmarksPanel.style.setProperty('background-color', extractProtectedNavBgDOMClaude(), 'important');
}
const tooltipEl = document.getElementById('acn-mini-tooltip');
if (tooltipEl && tooltipEl.style.display !== 'none') {
tooltipEl.style.setProperty('background-color', extractProtectedNavBgDOMClaude(), 'important');
}
}
function initThemeAutoWatcherClaude() {
if (themeAutoObserver) return;
themeAutoObserver = new MutationObserver(() => {
if (navTheme !== 'website') return;
clearTimeout(themeAutoApplyTimer);
themeAutoApplyTimer = setTimeout(() => {
applyNavTheme();
applyHeaderSolidBackgroundClaude();
}, 80);
});
themeAutoObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'data-theme'] });
themeAutoObserver.observe(document.body, { attributes: true, attributeFilter: ['class', 'data-theme'] });
}
// ============================================================================
// 06. CLAUDE ADAPTER
// ============================================================================
// 06.1 Background-safe timers and keyboard controls
// ---------------------------------------------------------------------------
function realSetTimeoutClaude(callback, delay) {
const id = setTimeout(callback, delay);
return { clear: () => clearTimeout(id) };
}
function sleepClaude(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function updateNavButtonIconsClaude() {
const back = document.getElementById('nav-btn-back');
const forward = document.getElementById('nav-btn-forward');
const comboHeld = claudeNavModifierAlt && claudeNavModifierCtrl;
if (back) back.textContent = comboHeld ? SYMBOL_FIRST : (claudeNavModifierShift ? SYMBOL_PREVIOUS_FAST : SYMBOL_PREVIOUS);
if (forward) forward.textContent = comboHeld ? SYMBOL_LAST : (claudeNavModifierShift ? SYMBOL_NEXT_FAST : SYMBOL_NEXT);
const miniBtn = document.getElementById('nav-btn-mini');
if (miniBtn && isMiniBar) miniBtn.textContent = claudeNavModifierEscape ? SYMBOL_CLOSE : SYMBOL_MAXBAR;
}
function handleNavModifierKeydownClaude(e) {
let changed = false;
if (e.key === 'Alt' && !claudeNavModifierAlt) { claudeNavModifierAlt = true; changed = true; }
if ((e.key === 'Control' || e.key === 'Meta') && !claudeNavModifierCtrl) { claudeNavModifierCtrl = true; changed = true; }
if (e.key === 'Shift' && !claudeNavModifierShift) { claudeNavModifierShift = true; changed = true; }
if (e.key === 'Escape' && !claudeNavModifierEscape) { claudeNavModifierEscape = true; changed = true; }
if (changed) updateNavButtonIconsClaude();
}
function handleNavModifierKeyupClaude(e) {
let changed = false;
if (e.key === 'Alt' && claudeNavModifierAlt) { claudeNavModifierAlt = false; changed = true; }
if ((e.key === 'Control' || e.key === 'Meta') && claudeNavModifierCtrl) { claudeNavModifierCtrl = false; changed = true; }
if (e.key === 'Shift' && claudeNavModifierShift) { claudeNavModifierShift = false; changed = true; }
if (e.key === 'Escape' && claudeNavModifierEscape) { claudeNavModifierEscape = false; changed = true; }
if (changed) updateNavButtonIconsClaude();
}
function resetNavModifiersClaude() {
if (claudeNavModifierAlt || claudeNavModifierCtrl || claudeNavModifierShift || claudeNavModifierEscape) {
claudeNavModifierAlt = false;
claudeNavModifierCtrl = false;
claudeNavModifierShift = false;
claudeNavModifierEscape = false;
updateNavButtonIconsClaude();
}
}
function updateNavButtonIconsGPT() {
const back = document.getElementById('nav-btn-back');
const forward = document.getElementById('nav-btn-forward');
const comboHeld = chatGptNavModifierAlt && chatGptNavModifierCtrl;
if (back) back.textContent = comboHeld ? SYMBOL_FIRST : (chatGptNavModifierShift ? SYMBOL_PREVIOUS_FAST : SYMBOL_PREVIOUS);
if (forward) forward.textContent = comboHeld ? SYMBOL_LAST : (chatGptNavModifierShift ? SYMBOL_NEXT_FAST : SYMBOL_NEXT);
const miniBtn = document.getElementById('nav-btn-mini');
if (miniBtn && isMiniBar) miniBtn.textContent = chatGptNavModifierEscape ? SYMBOL_CLOSE : SYMBOL_MAXBAR;
}
function handleNavModifierKeydownGPT(e) {
let changed = false;
if (e.key === 'Alt' && !chatGptNavModifierAlt) { chatGptNavModifierAlt = true; changed = true; }
if ((e.key === 'Control' || e.key === 'Meta') && !chatGptNavModifierCtrl) { chatGptNavModifierCtrl = true; changed = true; }
if (e.key === 'Shift' && !chatGptNavModifierShift) { chatGptNavModifierShift = true; changed = true; }
if (e.key === 'Escape' && !chatGptNavModifierEscape) { chatGptNavModifierEscape = true; changed = true; }
if (changed) updateNavButtonIconsGPT();
}
function handleNavModifierKeyupGPT(e) {
let changed = false;
if (e.key === 'Alt' && chatGptNavModifierAlt) { chatGptNavModifierAlt = false; changed = true; }
if ((e.key === 'Control' || e.key === 'Meta') && chatGptNavModifierCtrl) { chatGptNavModifierCtrl = false; changed = true; }
if (e.key === 'Shift' && chatGptNavModifierShift) { chatGptNavModifierShift = false; changed = true; }
if (e.key === 'Escape' && chatGptNavModifierEscape) { chatGptNavModifierEscape = false; changed = true; }
if (changed) updateNavButtonIconsGPT();
}
function resetNavModifiersGPT() {
if (chatGptNavModifierAlt || chatGptNavModifierCtrl || chatGptNavModifierShift || chatGptNavModifierEscape) {
chatGptNavModifierAlt = false;
chatGptNavModifierCtrl = false;
chatGptNavModifierShift = false;
chatGptNavModifierEscape = false;
updateNavButtonIconsGPT();
}
}
function startHoldScrollClaude(direction) {
stopHoldScrollClaude();
claudeHoldScrollWasActive = false;
claudeHoldScrollStartTimer = setTimeout(() => {
const list = document.getElementById('nav-list');
const miniNums = document.getElementById('nav-mini-msg-numbers');
if (!list && !miniNums) return;
claudeHoldScrollWasActive = true;
let lastTs = null;
const tick = (ts) => {
if (lastTs !== null) {
const dt = (ts - lastTs) / 1000;
const delta = direction * CLAUDE_HOLD_SCROLL_SPEED_PX_PER_SEC * dt;
if (list) setScrollTop(list, getScrollTop(list) + delta, 'auto');
if (miniNums) setScrollTop(miniNums, getScrollTop(miniNums) + delta, 'auto');
}
lastTs = ts;
claudeHoldScrollRafId = requestAnimationFrame(tick);
};
claudeHoldScrollRafId = requestAnimationFrame(tick);
}, CLAUDE_HOLD_SCROLL_INITIAL_DELAY_MS);
}
function stopHoldScrollClaude() {
if (claudeHoldScrollStartTimer) { clearTimeout(claudeHoldScrollStartTimer); claudeHoldScrollStartTimer = null; }
if (claudeHoldScrollRafId) { cancelAnimationFrame(claudeHoldScrollRafId); claudeHoldScrollRafId = null; }
}
function isSidebarLikeClaude(el) {
return !!(el && el.closest('nav, aside, [role="navigation"], [role="complementary"], [class*="sidebar" i], [class*="side-bar" i]'));
}
function extractSampleStartElementDOMClaude() {
if (IS_CLAUDE) {
const known = document.querySelector('[data-autoscroll-container="true"]');
if (known) return known;
}
const landmark = document.querySelector('main') || document.querySelector('[role="main"]');
if (landmark && !isSidebarLikeClaude(landmark)) return landmark;
const y = Math.round(window.innerHeight * 0.5);
let best = null, bestArea = 0;
[0.5, 0.3, 0.7, 0.85].forEach(function (xf) {
const cand = document.elementFromPoint(Math.round(window.innerWidth * xf), y);
if (!cand || isSidebarLikeClaude(cand)) return;
const rect = cand.getBoundingClientRect();
const area = rect.width * rect.height;
if (area > bestArea) { bestArea = area; best = cand; }
});
return best || document.body;
}
function extractPageBackgroundColorDOMClaude() {
let el = extractSampleStartElementDOMClaude();
while (el) {
const candidate = getComputedStyle(el).backgroundColor;
if (candidate && candidate !== 'rgba(0, 0, 0, 0)' && candidate !== 'transparent') return candidate;
el = el.parentElement;
}
return null;
}
function extractProtectedNavBgDOMClaude() {
if (navTheme === 'transparent') {
return extractPageBackgroundColorDOMClaude() || '#ffffff';
}
const container = document.getElementById('message-nav');
const navThemeBg = container ? getComputedStyle(container).getPropertyValue('--acn-nav-bg').trim() : '';
if (navThemeBg) return navThemeBg;
return extractPageBackgroundColorDOMClaude() || '#ffffff';
}
function applyHeaderSolidBackgroundClaude() {
const header = document.getElementById('nav-header');
const bookmarksBar = document.getElementById('nav-bookmarks-header');
if (!header && !bookmarksBar) return;
const bg = extractProtectedNavBgDOMClaude();
if (header) header.style.setProperty('background-color', bg, 'important');
if (bookmarksBar) bookmarksBar.style.setProperty('background-color', bg, 'important');
}
function applyNavHighlightClassesClaude() {
const list = document.getElementById('nav-list');
if (list) {
Array.from(list.children).forEach((li, idx) => {
const i = idx + 1;
li.classList.toggle('active', i === claudeCurrentVisibleIndex);
li.classList.toggle('acn-clicked', i === claudeLastClickedIndex);
});
}
const miniNums = document.getElementById('nav-mini-msg-numbers');
if (miniNums) {
Array.from(miniNums.children).forEach((wrapper, idx) => {
const numEl = wrapper.querySelector('.nav-mini-num');
if (!numEl) return;
const i = idx + 1;
numEl.classList.toggle('active', i === claudeCurrentVisibleIndex);
numEl.classList.toggle('acn-clicked', i === claudeLastClickedIndex);
});
}
}
function refreshCurrentVisibleHighlightClaude() {
if (!IS_CLAUDE) return;
if (claudeNavigationActive) return;
const now = Date.now();
if (now - claudeCurrentHighlightLastComputedAt < CLAUDE_CURRENT_HIGHLIGHT_THROTTLE_MS) return;
claudeCurrentHighlightLastComputedAt = now;
let uuid = null;
try {
uuid = extractCenteredVisibleMessageUuidDOMClaude();
} catch (e) {
return;
}
if (!uuid || uuid === claudeCurrentVisibleUuid) return;
const idx = claudeUuidToIndex.get(uuid);
if (idx === undefined) return;
claudeCurrentVisibleUuid = uuid;
claudeCurrentVisibleIndex = idx + 1;
applyNavHighlightClassesClaude();
const list = document.getElementById('nav-list');
const item = list && list.children[claudeCurrentVisibleIndex - 1];
scrollNavItemToTopIfOffscreen(list, item);
const miniNums = document.getElementById('nav-mini-msg-numbers');
const miniWrapper = miniNums && miniNums.children[claudeCurrentVisibleIndex - 1];
scrollNavItemToTopIfOffscreen(miniNums, miniWrapper);
updateSuperminiInputNav();
}
function setCssStripClaude(enabled) {
document.documentElement.classList.toggle('acn-claude-css-strip', Boolean(enabled));
}
// 06.2 Reading position, harvesting and navigation overlays
// ---------------------------------------------------------------------------
function extractCenteredVisibleMessageUuidDOMClaude() {
if (!CURRENT_SITE.userMessage || !CURRENT_SITE.userMessage.container) return null;
const root = getConversationScrollRoot();
if (!root) return null;
const rootRect = root.getBoundingClientRect ? root.getBoundingClientRect() : { top: 0, bottom: window.innerHeight, height: window.innerHeight };
const centerY = rootRect.top + (rootRect.height || (rootRect.bottom - rootRect.top)) / 2;
const nodes = IS_CLAUDE ? extractUserMessageNodesDOMClaude() : document.querySelectorAll(CURRENT_SITE.userMessage.container);
let best = null;
let bestDist = Infinity;
for (const node of nodes) {
const rect = node.getBoundingClientRect();
if (rect.bottom < rootRect.top || rect.top > rootRect.bottom) continue;
const dist = Math.abs((rect.top + rect.bottom) / 2 - centerY);
if (dist < bestDist) { bestDist = dist; best = node; }
}
return best ? extractMessageUuidDOMClaude(best) : null;
}
function restoreReadingPositionClaude() {
const saved = readSavedReadingPosition();
if (!saved) {
return false;
}
if (!saved.claudeTopUuid) {
return false;
}
const index = claudeUuidToIndex.get(saved.claudeTopUuid);
if (index === undefined) {
return false;
}
if (promptCacheItems.length > 0 && index === promptCacheItems.length - 1) {
return true;
}
showStatusOverlayClaude('Restoring last reading position...');
showNavFreezeClaude();
realSetTimeoutClaude(() => navigateToMessage(index + 1, { isRestore: true }), CLAUDE_RESTORE_POSITION_DELAY_MS);
return true;
}
async function harvestMountPositionsClaude() {
if (!IS_CLAUDE) return;
const scanBtn = document.getElementById('nav-btn-scan');
if (claudeHarvestingActive) {
claudeHarvestingActive = false;
return;
}
const root = document.querySelector('[data-autoscroll-container="true"]');
if (!root) return;
claudeHarvestingActive = true;
if (scanBtn) {
scanBtn.classList.add('scanning');
scanBtn.textContent = SYMBOL_SCANNING;
scanBtn.title = 'Stop harvesting';
}
showStatusOverlayClaude('Harvesting mount positions...\nPlease keep this tab active');
const originalTop = getScrollTop(root);
const originalBehavior = root.style.scrollBehavior;
root.style.scrollBehavior = 'auto';
try {
const step = Math.max(200, Math.floor(root.clientHeight * 0.8));
const total = promptCacheItems.length;
let top = 0;
while (claudeHarvestingActive) {
setScrollTop(root, top, 'auto');
await sleepClaude(300);
tagCurrentlyMountedMessagesClaude();
if (total) {
showStatusOverlayClaude(`Harvesting mount positions... (message ${Math.min(claudeConfirmedOffsets.size, total)} of ${total})\nKeep this tab focused for best performance`);
}
const maxScroll = Math.max(0, root.scrollHeight - root.clientHeight);
if (top >= maxScroll) break;
top = Math.min(top + step, maxScroll);
}
} finally {
setScrollTop(root, originalTop, 'auto');
root.style.scrollBehavior = originalBehavior;
hideStatusOverlayClaude();
claudeHarvestingActive = false;
if (scanBtn) {
scanBtn.classList.remove('scanning');
scanBtn.textContent = SYMBOL_HARVEST_MOUNT_POSITION;
scanBtn.title = 'Harvest mount positions\nScrolls the whole conversation to record accurate positions for every message';
}
}
}
function showStatusOverlayClaude(text, options = {}) {
const showSpinner = options.showSpinner !== false;
let overlay = document.getElementById('acn-restore-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'acn-restore-overlay';
const box = document.createElement('div');
box.className = 'acn-restore-box';
const spinner = document.createElement('div');
spinner.className = 'acn-restore-spinner';
spinner.id = 'acn-restore-spinner';
const label = document.createElement('span');
label.id = 'acn-restore-label';
box.appendChild(spinner);
box.appendChild(label);
overlay.appendChild(box);
document.documentElement.appendChild(overlay);
}
const spinnerEl = document.getElementById('acn-restore-spinner');
if (spinnerEl) spinnerEl.style.display = showSpinner ? '' : 'none';
const label = document.getElementById('acn-restore-label');
if (label) label.textContent = text;
}
function hideStatusOverlayClaude() {
const overlay = document.getElementById('acn-restore-overlay');
if (overlay) overlay.remove();
}
function showNavFreezeClaude() {
const root = document.querySelector('[data-autoscroll-container="true"]');
if (root) root.style.visibility = 'hidden';
if (claudeCssStripRestoreTimer) { claudeCssStripRestoreTimer.clear(); claudeCssStripRestoreTimer = null; }
setCssStripClaude(true);
}
function hideNavLoadingOverlayClaude() {
const root = document.querySelector('[data-autoscroll-container="true"]');
if (root) root.style.visibility = '';
if (claudeCssStripRestoreTimer) claudeCssStripRestoreTimer.clear();
claudeCssStripRestoreTimer = realSetTimeoutClaude(() => {
claudeCssStripRestoreTimer = null;
setCssStripClaude(false);
}, 150);
}
// 06.3 User-file previews, downloads and version controls
// ---------------------------------------------------------------------------
async function openFilePreviewClaude(index, fileIdx, options = {}) {
const autoDownload = Boolean(options.autoDownload);
const getArticle = () => {
const item = promptCacheItems[index - 1];
return item && item.element && item.element.isConnected ? item.element : null;
};
const root = document.querySelector('[data-autoscroll-container="true"]');
const originalIndex = IS_CLAUDE ? claudeCurrentVisibleIndex : null;
let originalRelativeOffset = null;
if (root && originalIndex) {
const origItem = promptCacheItems[originalIndex - 1];
const origNode = origItem && origItem.element && origItem.element.isConnected ? origItem.element : null;
if (origNode) {
const rootRect = root.getBoundingClientRect();
const liveOffsetTopNow = (origNode.getBoundingClientRect().top - rootRect.top) + root.scrollTop;
originalRelativeOffset = root.scrollTop - liveOffsetTopNow;
}
}
let article = getArticle();
showNavFreezeClaude();
try {
if (!article) {
await navigateToMessageInner(index);
await sleepClaude(300);
article = getArticle();
}
if (!article) { return; }
const promptForLookup = promptCacheItems[index - 1];
const fileForLookup = promptForLookup && promptForLookup.files ? promptForLookup.files[fileIdx] : null;
let thumb = null;
if (fileForLookup && fileForLookup.fileKind && fileForLookup.name) {
thumb = article.querySelector(`[data-testid="${CSS.escape(fileForLookup.name)}"]`);
}
if (!thumb) {
const thumbs = article.querySelectorAll('[data-testid="file-thumbnail"]');
const pastedPosition = promptForLookup && promptForLookup.files
? promptForLookup.files.slice(0, fileIdx).filter(x => !x.fileKind).length
: fileIdx;
thumb = thumbs[pastedPosition] || null;
}
const clickable = thumb ? (thumb.querySelector('button') || thumb) : null;
if (clickable) {
if (autoDownload) document.documentElement.classList.add('acn-claude-dialog-hidden');
clickable.click();
await sleepClaude(300);
if (autoDownload) {
const dialog = document.querySelector('[role="dialog"]:last-of-type, [data-radix-portal] [role="dialog"]:last-of-type');
const scope = dialog || document;
const candidates = Array.from(scope.querySelectorAll('button, [role="button"]'));
const downloadBtn = candidates.find(el => /download/i.test(el.getAttribute('aria-label') || el.textContent || ''));
if (downloadBtn) {
downloadBtn.click();
await sleepClaude(300);
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', bubbles: true }));
await sleepClaude(150);
}
document.documentElement.classList.remove('acn-claude-dialog-hidden');
}
}
if (IS_CLAUDE && originalIndex) {
await ensurePromptLoadedThenFind(originalIndex - 1);
await sleepClaude(300);
const returnedItem = promptCacheItems[originalIndex - 1];
const returnedNode = returnedItem && returnedItem.element && returnedItem.element.isConnected ? returnedItem.element : null;
if (returnedNode && root) {
const rootRect2 = root.getBoundingClientRect();
const currentLiveOffsetTop = (returnedNode.getBoundingClientRect().top - rootRect2.top) + root.scrollTop;
const desiredScrollTop = originalRelativeOffset !== null
? currentLiveOffsetTop + originalRelativeOffset
: currentLiveOffsetTop;
root.scrollTop = Math.max(0, desiredScrollTop);
}
}
} finally {
document.documentElement.classList.remove('acn-claude-dialog-hidden');
hideNavLoadingOverlayClaude();
}
}
function isVersionButtonDisabledClaude(versionInfo, key) {
if (key === 'prevButton') return versionInfo.currentVersion <= 1;
return versionInfo.currentVersion >= versionInfo.totalVersions;
}
async function clickVersionButtonClaude(index, key, targetUuid) {
let cacheIdx = index - 1;
if (targetUuid && claudeUuidToIndex && claudeUuidToIndex.has(targetUuid)) {
cacheIdx = claudeUuidToIndex.get(targetUuid);
}
const prompt = promptCacheItems[cacheIdx];
if (!prompt) {
return false;
}
let btn = prompt.versionInfo && prompt.versionInfo[key];
showNavFreezeClaude();
try {
let justRenavigated = false;
if (!btn || !btn.isConnected) {
await scrollToCachedIndexClaude(cacheIdx);
justRenavigated = true;
for (let i = 0; i < 5 && !btn; i++) {
if (i > 0) await sleepClaude(250);
const refreshed = promptCacheItems[cacheIdx];
const node = refreshed && refreshed.element;
if (node && node.isConnected) {
let scope = extractVersionScopeDOMClaude(node);
if (scope && scope.hasAttribute('data-deferred')) {
const revealBtn = scope.querySelector('button.sr-only[aria-label^="Show message actions" i]');
if (revealBtn) {
revealBtn.focus();
revealBtn.click();
await sleepClaude(200);
scope = extractVersionScopeDOMClaude(node);
}
}
const fresh = scope ? extractVersionControlDOM(scope, CURRENT_SITE.versionControl) : null;
if (fresh) {
refreshed.versionInfo = fresh;
btn = fresh[key];
}
}
}
}
if (!btn || btn.disabled) {
return false;
}
if (justRenavigated) {
await sleepClaude(300);
}
btn.click();
for (let i = 0; i < 6; i++) {
await sleepClaude(300);
buildFastNavbarFromLiveFiberClaude();
}
await scrollToCachedIndexClaude(cacheIdx);
return true;
} finally {
hideNavLoadingOverlayClaude();
}
}
function extractVersionScopeDOMClaude(userArticle) {
if (!IS_CLAUDE || !(userArticle instanceof HTMLElement)) return null;
const selector = CURRENT_SITE.versionControl && CURRENT_SITE.versionControl.container
? CURRENT_SITE.versionControl.container
: '[data-cds="MessageActions"]';
const sameArticleBar = userArticle.querySelector(selector);
if (sameArticleBar) return sameArticleBar;
const articleRect = userArticle.getBoundingClientRect();
const candidates = [];
const pushCandidates = scope => {
if (!(scope instanceof HTMLElement)) return;
try {
scope.querySelectorAll(selector).forEach(bar => {
if (bar instanceof HTMLElement && !candidates.includes(bar)) candidates.push(bar);
});
} catch (_) {}
};
let parent = userArticle.parentElement;
for (let depth = 0; parent && depth < 5; depth += 1, parent = parent.parentElement) {
pushCandidates(parent);
if (candidates.length) break;
}
if (!candidates.length) {
pushCandidates(document.body);
}
if (!candidates.length) return null;
const scored = candidates.map(bar => {
const rect = bar.getBoundingClientRect();
const verticalDistance = rect.top >= articleRect.top
? Math.abs(rect.top - articleRect.bottom)
: Math.abs(articleRect.top - rect.bottom) + 1000;
const horizontalDistance = Math.abs((rect.left + rect.right) / 2 - (articleRect.left + articleRect.right) / 2);
return { bar, score: verticalDistance + horizontalDistance * 0.05 };
}).sort((a, b) => a.score - b.score);
return scored[0] ? scored[0].bar : null;
}
function formatFileSizeClaude(bytes) {
if (!bytes || bytes <= 0) return null;
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
function getOrgIdClaude() {
const match = document.cookie.match(/(?:^|;\s*)lastActiveOrg=([^;]+)/);
return match ? match[1] : null;
}
function getConversationIdClaude() {
const match = window.location.pathname.match(/\/chat\/([a-f0-9-]{36})/i);
return match ? match[1] : null;
}
function buildFileDownloadUrlClaude(f) {
if (!f) return null;
if (f.fileKind === 'image' && f.previewUrl) {
return 'https://claude.ai' + f.previewUrl;
}
if (f.fileKind === 'document' && f.documentUrl) {
return 'https://claude.ai' + f.documentUrl;
}
if (f.fileKind === 'blob' && f.path) {
const orgId = getOrgIdClaude();
const convId = getConversationIdClaude();
if (!orgId || !convId) return null;
return `https://claude.ai/api/organizations/${orgId}/conversations/${convId}/wiggle/download-file?path=${encodeURIComponent(f.path)}`;
}
return null;
}
// 06.4 React Fiber and attachment extraction
// ---------------------------------------------------------------------------
function textContentWithBreaks(el) {
const clone = el.cloneNode(true);
clone.querySelectorAll('br').forEach(br => br.replaceWith('\n'));
return clone;
}
function extractMessageTextDOMClaude(node) {
const parts = node.querySelectorAll(CLAUDE_MESSAGE_TEXT_NODE_SELECTOR);
if (parts.length > 0) {
const pieces = [];
Array.from(parts).forEach(el => {
if (el.tagName === 'OL' || el.tagName === 'UL') {
const liItems = Array.from(el.children).filter(c => c.tagName === 'LI');
liItems.forEach((li, i) => {
const marker = el.tagName === 'OL' ? `${i + 1}. ` : '- ';
pieces.push(marker + extractCleanTextDOM(textContentWithBreaks(li)));
});
} else {
pieces.push(extractCleanTextDOM(textContentWithBreaks(el)));
}
});
return pieces.join('\n').trim();
}
const askUserCard = node.querySelector && node.querySelector('[data-testid="ask-user-answers-card"]');
if (askUserCard) {
const spans = askUserCard.querySelectorAll('.contents > span');
if (spans.length >= 2) {
const question = extractCleanTextDOM(textContentWithBreaks(spans[0]));
const answer = extractCleanTextDOM(textContentWithBreaks(spans[1]));
return `Q: ${question}\nA: ${answer}`;
}
if (spans.length === 1) {
return `Q: ${extractCleanTextDOM(textContentWithBreaks(spans[0]))}`;
}
return '';
}
if (node.querySelector && node.querySelector(CURRENT_SITE.elements.fileThumbnail)) {
return '';
}
const clone = textContentWithBreaks(node);
clone.querySelectorAll('[class*="timestamp" i]').forEach(el => el.remove());
let text = extractCleanTextDOM(clone);
text = text.replace(/\d{4}-\d{2}-\d{2}[^A-Za-z]*\d{0,2}:?\d{0,2}(\s*GMT[^\s]*)?\s*$/, '').trim();
return text;
}
function extractMessageUuidDOMClaude(node) {
const bubble = node.matches && node.matches(CURRENT_SITE.elements.userBubble)
? node
: (node.querySelector ? node.querySelector(CURRENT_SITE.elements.userBubble) : null);
const fileThumb = !bubble && node.querySelector ? node.querySelector(CURRENT_SITE.elements.fileThumbnail) : null;
const startNode = bubble || fileThumb || node;
const fiberKey = Object.keys(startNode).find(k => k.startsWith('__reactFiber$'));
if (!fiberKey) return null;
let fiber = startNode[fiberKey];
let depth = 0;
while (fiber && depth < 15) {
const props = fiber.memoizedProps;
if (props && props.messageUuid) return props.messageUuid;
fiber = fiber.return;
depth++;
}
return null;
}
function extractUserMessageNodesDOMClaude() {
const nodes = document.querySelectorAll(CURRENT_SITE.userMessage.container);
if (nodes.length > 0 || promptCacheItems.length === 0) return Array.from(nodes);
const articles = document.querySelectorAll(CURRENT_SITE.elements.article);
const fallback = [];
articles.forEach(node => {
const uuid = extractMessageUuidDOMClaude(node);
if (uuid && claudeUuidToIndex.has(uuid)) fallback.push(node);
});
return fallback;
}
function parseMessageCreatedAtClaude(m) {
if (!m) return null;
const raw = m.created_at || m.createdAt || m.timestamp || m.date || null;
if (!raw) return null;
const d = new Date(raw);
return isNaN(d.getTime()) ? null : d;
}
function parseMessageUpdatedAtClaude(m) {
if (!m) return null;
const raw = m.updated_at || m.updatedAt || null;
if (!raw) return null;
const d = new Date(raw);
return isNaN(d.getTime()) ? null : d;
}
function extractMessageChainDataDOMClaude(doc) {
const nodes = doc.querySelectorAll(CURRENT_SITE.elements.article);
for (const node of nodes) {
const fiberKey = Object.keys(node).find(k => k.startsWith('__reactFiber$'));
if (!fiberKey) continue;
let fiber = node[fiberKey];
let depth = 0;
while (fiber && depth < 15) {
const props = fiber.memoizedProps;
if (props && props.messageChainData) return props.messageChainData;
fiber = fiber.return;
depth++;
}
}
return null;
}
function getSiteFaviconUrl() {
if (cachedSiteFaviconUrl) return cachedSiteFaviconUrl;
const link = document.querySelector('link[rel~="icon"]');
const resolved = (link && link.href) ? link.href : (location.origin + '/favicon.ico');
if (/Spark\/Dot/i.test(resolved)) return resolved;
cachedSiteFaviconUrl = resolved;
return resolved;
}
function estimateTextLinesClaude(text) {
if (!text) return { normalLines: 0, tableLines: 0, codeLines: 0 };
const CHARS_PER_LINE = 80;
const CODE_CHARS_PER_LINE = 60;
let normalLines = 0, tableLines = 0, codeLines = 0;
let inCode = false;
text.split('\n').forEach(line => {
if (/^\s*```/.test(line)) { inCode = !inCode; codeLines++; return; }
if (inCode) { codeLines += Math.max(1, Math.ceil(line.length / CODE_CHARS_PER_LINE)); return; }
if (/^\s*\|.*\|\s*$/.test(line)) {
tableLines++;
} else {
normalLines += Math.max(1, Math.ceil(line.length / CHARS_PER_LINE));
}
});
return { normalLines, tableLines, codeLines };
}
function estimateMessageHeightClaude(m, heightHintFallback) {
if (m && Array.isArray(m.content)) {
let totalNormalLines = 0, totalTableLines = 0, totalCodeLines = 0;
m.content.filter(b => b.type === 'text').forEach(b => {
const r = estimateTextLinesClaude(b.text);
totalNormalLines += r.normalLines;
totalTableLines += r.tableLines;
totalCodeLines += r.codeLines;
});
if (totalNormalLines > 0 || totalTableLines > 0 || totalCodeLines > 0) {
return 34.1 + 22.85 * totalNormalLines + 46 * totalTableLines + 17.67 * totalCodeLines;
}
}
return heightHintFallback;
}
// 06.5 Native virtualizer offsets and cache stability
// ---------------------------------------------------------------------------
function extractNativeHeightCacheDOMClaude() {
const sizerEl = document.querySelector('[data-rocksteady-sizer]');
if (!sizerEl) return null;
const fk = Object.keys(sizerEl).find(k => k.startsWith('__reactFiber$'));
if (!fk) return null;
let fiber = sizerEl[fk];
let depth = 0;
while (fiber && depth < 8) {
let hook = fiber.memoizedState;
let hookIdx = 0;
while (hook && hookIdx < 40) {
const val = hook.memoizedState;
if (val instanceof Map && val.size > 0) {
const firstKey = val.keys().next().value;
if (typeof firstKey === 'string' && /^[0-9a-f]{8}-/.test(firstKey)) return val;
}
if (val && typeof val === 'object' && Object.keys(val).length === 1 && 'current' in val) {
const inner = val.current;
if (inner instanceof Map && inner.size > 0) {
const firstKey = inner.keys().next().value;
if (typeof firstKey === 'string' && /^[0-9a-f]{8}-/.test(firstKey)) return inner;
}
}
hook = hook.next;
hookIdx++;
}
fiber = fiber.return;
depth++;
}
return null;
}
function extractVirtualizerOffsetTableDOMClaude() {
if (claudeVirtualizerOffsetHooks) {
if (claudeVirtualizerOffsetHooks.offsetsRef.current && claudeVirtualizerOffsetHooks.uuidToIndexRef.current) {
return claudeVirtualizerOffsetHooks;
}
claudeVirtualizerOffsetHooks = null;
}
const feedEl = document.querySelector('[role="feed"]');
if (!feedEl) return null;
const fk = Object.keys(feedEl).find(k => k.startsWith('__reactFiber$'));
if (!fk) return null;
let fiber = feedEl[fk];
let depth = 0;
while (fiber && depth < 40) {
if (fiber.memoizedProps && fiber.memoizedProps.messageChainData) break;
fiber = fiber.return;
depth++;
}
if (!fiber || !fiber.memoizedProps || !fiber.memoizedProps.messageChainData) return null;
const itemCount = fiber.memoizedProps.messageChainData.length;
let hook = fiber.memoizedState;
let offsetsRef = null, uuidToIndexRef = null;
while (hook) {
const v = hook.memoizedState;
const inner = v && typeof v === 'object' && Object.keys(v).length === 1 && 'current' in v ? v.current : undefined;
if (!offsetsRef && Array.isArray(inner) && inner.length === itemCount + 1) {
let monotonic = true;
for (let k = 1; k < Math.min(inner.length, 50); k++) { if (inner[k] < inner[k - 1]) { monotonic = false; break; } }
if (monotonic) offsetsRef = v;
}
if (!uuidToIndexRef && inner instanceof Map && inner.size === itemCount) {
const sample = inner.values().next().value;
if (typeof sample === 'number') uuidToIndexRef = v;
}
hook = hook.next;
}
if (!offsetsRef || !uuidToIndexRef) return null;
claudeVirtualizerOffsetHooks = { offsetsRef, uuidToIndexRef };
return claudeVirtualizerOffsetHooks;
}
function getVirtualizerOffsetClaude(table, uuid) {
if (!table || !uuid) return null;
const idx = table.uuidToIndexRef.current.get(uuid);
if (idx === undefined) return null;
const offset = table.offsetsRef.current[idx];
return typeof offset === 'number' ? offset : null;
}
function refreshOffsetsFromNativeCacheClaude(force) {
if (!IS_CLAUDE || promptCacheItems.length === 0) return;
const now = Date.now();
if (!force && now - lastClaudeOffsetRefresh < 4000) return;
lastClaudeOffsetRefresh = now;
const chainData = extractMessageChainDataDOMClaude(document);
if (!chainData) return;
const nativeHeightCache = extractNativeHeightCacheDOMClaude();
if (!nativeHeightCache || nativeHeightCache.size === 0) return;
const virtualizerTable = extractVirtualizerOffsetTableDOMClaude();
let cumulativeHeight = 0;
chainData.forEach(entry => {
const m = entry.chain && entry.chain.messages && entry.chain.messages[0];
const heightHint = entry.heightHint || 0;
let estimatedHeight = estimateMessageHeightClaude(m, heightHint);
if (m && m.uuid && nativeHeightCache.has(m.uuid)) {
estimatedHeight = nativeHeightCache.get(m.uuid);
}
if (m && m.sender === 'human' && m.uuid && claudeUuidToIndex.has(m.uuid)) {
const idx = claudeUuidToIndex.get(m.uuid);
const item = promptCacheItems[idx];
const vOffset = getVirtualizerOffsetClaude(virtualizerTable, m.uuid);
const newOffsetTop = vOffset !== null ? vOffset : cumulativeHeight;
if (item && item._acnOffsetTop !== newOffsetTop) {
item._acnOffsetTop = newOffsetTop;
}
if (item) item._acnOffsetIsReal = vOffset !== null;
}
cumulativeHeight += estimatedHeight;
});
}
function getHarvestSignatureClaude() {
const first = promptCacheItems[0];
return promptCacheItems.length + '|' + (first ? (first._acnUuid || (first.text || '').slice(0, 20)) : '');
}
function getHarvestFirstUuidClaude() {
const first = promptCacheItems[0];
return first ? (first._acnUuid || null) : null;
}
function scheduleHarvestStabilityCheckClaude(maxAttempts = 25, intervalMs = 400) {
if (!IS_CLAUDE) return;
const myToken = ++claudeStabilityCheckToken;
const targetUrl = window.location.href;
const staleFirstUuid = claudePreviousChatFirstUuid;
let attempts = 0;
let lastSignature = getHarvestSignatureClaude();
function tick() {
if (myToken !== claudeStabilityCheckToken || window.location.href !== targetUrl) {
return;
}
attempts++;
buildFastNavbarFromLiveFiberClaude();
const newSignature = getHarvestSignatureClaude();
const newFirstUuid = getHarvestFirstUuidClaude();
const matchesStaleData = staleFirstUuid !== null && newFirstUuid === staleFirstUuid;
const stable = !matchesStaleData && newSignature === lastSignature;
if (claudeRestorePendingUrl === window.location.href && !matchesStaleData) {
const restored = restoreReadingPositionClaude();
if (restored) {
claudeReadingPositionRestoredUrl = window.location.href;
claudeRestorePendingUrl = null;
}
}
if (stable) return;
if (attempts >= maxAttempts) {
return;
}
lastSignature = newSignature;
realSetTimeoutClaude(tick, intervalMs);
}
realSetTimeoutClaude(tick, intervalMs);
}
// 06.6 Mount tagging and virtualized navigation
// ---------------------------------------------------------------------------
function getComparableUrlClaude() {
return window.location.origin + window.location.pathname;
}
function buildFastNavbarFromLiveFiberClaude() {
const chainData = extractMessageChainDataDOMClaude(document);
if (!chainData) return false;
const nativeHeightCache = extractNativeHeightCacheDOMClaude();
const virtualizerTable = extractVirtualizerOffsetTableDOMClaude();
const items = [];
let cumulativeHeight = 0;
let nativeCacheHits = 0;
let virtualizerOffsetHits = 0;
let virtualizerOffsetExactHits = 0;
chainData.forEach((entry, chainIdx) => {
const m = entry.chain && entry.chain.messages && entry.chain.messages[0];
const heightHint = entry.heightHint || 0;
let estimatedHeight = estimateMessageHeightClaude(m, heightHint);
if (m && m.uuid && nativeHeightCache && nativeHeightCache.has(m.uuid)) {
estimatedHeight = nativeHeightCache.get(m.uuid);
nativeCacheHits++;
}
const vOffsetTop = m && m.uuid ? getVirtualizerOffsetClaude(virtualizerTable, m.uuid) : null;
if (vOffsetTop !== null) {
virtualizerOffsetHits++;
if (m.uuid && nativeHeightCache && nativeHeightCache.has(m.uuid)) virtualizerOffsetExactHits++;
}
if (m && m.sender === 'human') {
let text = m.content && m.content[0] && m.content[0].text;
const files = getBadgeUserMessageDataClaude(m).files;
if (!text && files.length > 0) text = files.map(f => f.name).join(', ') || '[file]';
if (text) {
const hasMultipleVersions = !!(m.nOptions && m.nOptions > 1);
const nextEntry = chainData[chainIdx + 1];
const nextM = nextEntry && nextEntry.chain && nextEntry.chain.messages && nextEntry.chain.messages[0];
const generatedFiles = [
...getFileBadgeAIMessageDataClaude(nextM).generatedFiles,
...getImageBadgeAIMessageDataClaude(nextM).generatedFiles
];
items.push({
element: null,
text,
files,
generatedFiles,
versionInfo: hasMultipleVersions
? { currentVersion: m.selectedOption + 1, totalVersions: m.nOptions, hasMultipleVersions: true }
: null,
_acnUuid: m.uuid || null,
_acnOffsetTop: vOffsetTop !== null ? vOffsetTop : cumulativeHeight,
_acnOffsetIsReal: vOffsetTop !== null,
_acnPosinset: chainIdx + 1,
_acnCreatedAt: parseMessageCreatedAtClaude(m),
_acnUpdatedAt: parseMessageUpdatedAtClaude(m),
});
}
}
cumulativeHeight += estimatedHeight;
});
claudeNativeCacheCoverage = chainData.length > 0 ? nativeCacheHits / chainData.length : 0;
if (items.length === 0) return false;
items.forEach((item, idx) => { item._acnKey = getPromptStableKey({ element: null, text: item.text, files: [] }, idx); item._acnLastSeen = Date.now(); });
promptCacheUrl = window.location.href;
promptCacheItems = items;
promptCacheMap = new Map(items.map(it => [it._acnKey, it]));
claudeUuidToIndex = new Map();
items.forEach((item, idx) => {
if (item._acnUuid && !claudeUuidToIndex.has(item._acnUuid)) claudeUuidToIndex.set(item._acnUuid, idx);
});
claudePosinsetToIndex = new Map();
items.forEach((item, idx) => {
if (item._acnPosinset !== null && !claudePosinsetToIndex.has(item._acnPosinset)) claudePosinsetToIndex.set(item._acnPosinset, idx);
});
rebuildBookmarkedIndicesFromUuidsClaude();
realSetTimeoutClaude(() => updateMessageList(false), 20);
return true;
}
function isClaudeSharePage() {
return IS_CLAUDE && /^\/share\//.test(window.location.pathname);
}
function extractFiberMessageClaude(node, maxDepth = 20) {
if (!(node instanceof HTMLElement)) return null;
const fiberKey = Object.keys(node).find(k => k.startsWith('__reactFiber$'));
if (!fiberKey) return null;
let fiber = node[fiberKey];
let depth = 0;
while (fiber && depth < maxDepth) {
const props = fiber.memoizedProps;
if (props && props.message) return props.message;
fiber = fiber.return;
depth++;
}
return null;
}
function buildShareNavbarFromDOMClaude() {
if (!isClaudeSharePage()) return false;
const humanNodes = Array.from(document.querySelectorAll('[data-cds="UserMessage"]'));
const assistantNodes = Array.from(document.querySelectorAll('div.font-claude-response'));
if (!humanNodes.length && !assistantNodes.length) return false;
const byIndex = new Map();
humanNodes.forEach(node => {
const m = extractFiberMessageClaude(node);
if (m && Number.isFinite(m.index)) byIndex.set(m.index, { m, node, sender: 'human' });
});
assistantNodes.forEach(node => {
const m = extractFiberMessageClaude(node);
if (m && Number.isFinite(m.index)) byIndex.set(m.index, { m, node, sender: 'assistant' });
});
if (!byIndex.size) return false;
const ordered = Array.from(byIndex.keys()).sort((a, b) => a - b).map(k => byIndex.get(k));
const items = [];
for (let i = 0; i < ordered.length; i++) {
const entry = ordered[i];
if (entry.sender !== 'human') continue;
const m = entry.m;
let text = m.content && m.content[0] && m.content[0].text;
const files = getBadgeUserMessageDataClaude(m).files;
if (!text && files.length > 0) text = files.map(f => f.name).join(', ') || '[file]';
if (!text) continue;
const nextEntry = ordered[i + 1];
const nextM = nextEntry && nextEntry.sender === 'assistant' ? nextEntry.m : null;
const generatedFiles = nextM ? [
...getFileBadgeAIMessageDataClaude(nextM).generatedFiles,
...getImageBadgeAIMessageDataClaude(nextM).generatedFiles
] : [];
items.push({
element: entry.node,
text,
files,
generatedFiles,
versionInfo: null,
_acnUuid: m.uuid || null,
_acnOffsetTop: 0,
_acnOffsetIsReal: false,
_acnPosinset: i + 1,
_acnCreatedAt: parseMessageCreatedAtClaude(m),
_acnUpdatedAt: parseMessageUpdatedAtClaude(m),
_acnAssistantElement: nextEntry && nextEntry.sender === 'assistant' ? nextEntry.node : null,
});
}
if (!items.length) return false;
items.forEach((item, idx) => { item._acnKey = getPromptStableKey({ element: null, text: item.text, files: [] }, idx); item._acnLastSeen = Date.now(); });
promptCacheUrl = window.location.href;
promptCacheItems = items;
promptCacheMap = new Map(items.map(it => [it._acnKey, it]));
claudeUuidToIndex = new Map();
items.forEach((item, idx) => {
if (item._acnUuid && !claudeUuidToIndex.has(item._acnUuid)) claudeUuidToIndex.set(item._acnUuid, idx);
});
claudePosinsetToIndex = new Map();
items.forEach((item, idx) => {
if (item._acnPosinset !== null && !claudePosinsetToIndex.has(item._acnPosinset)) claudePosinsetToIndex.set(item._acnPosinset, idx);
});
return true;
}
function refreshLastAssistantGeneratedFilesClaude(chainData) {
if (!Array.isArray(chainData) || !chainData.length || !promptCacheItems.length) return;
if (!claudeUuidToIndex) return;
for (let chainIdx = chainData.length - 1; chainIdx >= 0; chainIdx--) {
const entry = chainData[chainIdx];
const m = entry && entry.chain && entry.chain.messages && entry.chain.messages[0];
if (!m || m.sender !== 'human') continue;
if (!m.uuid || !claudeUuidToIndex.has(m.uuid)) return;
const cacheIndex = claudeUuidToIndex.get(m.uuid);
const cachedItem = promptCacheItems[cacheIndex];
if (!cachedItem) return;
const nextEntry = chainData[chainIdx + 1];
const nextM = nextEntry && nextEntry.chain && nextEntry.chain.messages && nextEntry.chain.messages[0];
const freshGeneratedFiles = [
...getFileBadgeAIMessageDataClaude(nextM).generatedFiles,
...getImageBadgeAIMessageDataClaude(nextM).generatedFiles
];
const oldSig = (cachedItem.generatedFiles || []).map(f => `${f.kind || ''}:${f.name || ''}:${f.ext || ''}`).join('|');
const newSig = freshGeneratedFiles.map(f => `${f.kind || ''}:${f.name || ''}:${f.ext || ''}`).join('|');
if (oldSig !== newSig) cachedItem.generatedFiles = freshGeneratedFiles;
return;
}
}
function resolveCacheIndexClaude(node, text) {
const uuid = extractMessageUuidDOMClaude(node);
if (uuid && claudeUuidToIndex.has(uuid)) return claudeUuidToIndex.get(uuid);
const posinsetRaw = node.getAttribute('aria-posinset');
const posinset = posinsetRaw !== null ? parseInt(posinsetRaw, 10) : null;
if (posinset !== null && claudePosinsetToIndex.has(posinset)) {
const idx = claudePosinsetToIndex.get(posinset);
if (uuid && !claudeUuidToIndex.has(uuid)) {
claudeUuidToIndex.set(uuid, idx);
if (promptCacheItems[idx] && !promptCacheItems[idx]._acnUuid) promptCacheItems[idx]._acnUuid = uuid;
}
return idx;
}
const matchIndices = [];
promptCacheItems.forEach((it, i) => { if (it.text === text) matchIndices.push(i); });
let idx = -1;
if (matchIndices.length === 1) {
idx = matchIndices[0];
} else if (matchIndices.length > 1) {
if (posinset !== null) {
idx = matchIndices.reduce((best, i) => {
const p = promptCacheItems[i]._acnPosinset;
if (p === null) return best;
const bestP = best === -1 ? null : promptCacheItems[best]._acnPosinset;
if (bestP === null) return i;
return Math.abs(p - posinset) < Math.abs(bestP - posinset) ? i : best;
}, -1);
}
if (idx === -1) idx = matchIndices[0];
}
if (idx !== -1 && uuid && !claudeUuidToIndex.has(uuid)) {
claudeUuidToIndex.set(uuid, idx);
if (!promptCacheItems[idx]._acnUuid) promptCacheItems[idx]._acnUuid = uuid;
}
return idx;
}
function tagCurrentlyMountedMessagesClaude() {
const root = document.querySelector('[data-autoscroll-container="true"]');
if (!root || !promptCacheItems.length) return;
const rootRect = root.getBoundingClientRect();
const scrollTop = getScrollTop(root);
const nodes = extractUserMessageNodesDOMClaude();
nodes.forEach(node => {
const textEl = node.querySelector(CURRENT_SITE.userMessage.text);
const text = textEl ? extractMessageTextDOMClaude(textEl) : extractMessageTextDOMClaude(node);
const cacheIndex = resolveCacheIndexClaude(node, text);
if (cacheIndex === -1) return;
const offsetTop = (node.getBoundingClientRect().top - rootRect.top) + scrollTop;
claudeConfirmedOffsets.set(cacheIndex, { offsetTop, lastSeen: Date.now() });
if (promptCacheItems[cacheIndex]) {
promptCacheItems[cacheIndex].element = node;
if (CURRENT_SITE.versionControl) {
const versionScope = extractVersionScopeDOMClaude(node);
if (versionScope) {
const versionInfo = extractVersionControlDOM(versionScope, CURRENT_SITE.versionControl);
if (versionInfo) promptCacheItems[cacheIndex].versionInfo = versionInfo;
}
}
}
});
}
function startPassiveTaggingClaude() {
if (claudeTaggingObserver) return;
const root = document.querySelector('[data-autoscroll-container="true"]');
if (!root) return;
tagCurrentlyMountedMessagesClaude();
claudeTaggingObserver = new MutationObserver(() => {
if (claudeTaggingDebounce) claudeTaggingDebounce.clear();
claudeTaggingDebounce = realSetTimeoutClaude(tagCurrentlyMountedMessagesClaude, 250);
});
claudeTaggingObserver.observe(root, { childList: true, subtree: true });
}
function findNearestConfirmedOffsetClaude(index) {
let best = null;
let bestDist = Infinity;
claudeConfirmedOffsets.forEach((entry, cacheIndex) => {
const dist = Math.abs(cacheIndex - index);
if (dist < bestDist) { bestDist = dist; best = { cacheIndex, ...entry }; }
});
return best;
}
async function scrollToCachedIndexClaude(index) {
{
const alreadyMountedNodes = extractUserMessageNodesDOMClaude();
for (const node of alreadyMountedNodes) {
const textEl = node.querySelector(CURRENT_SITE.userMessage.text);
const text = textEl ? extractMessageTextDOMClaude(textEl) : extractMessageTextDOMClaude(node);
const cacheIdx = resolveCacheIndexClaude(node, text);
if (cacheIdx === index) {
promptCacheItems[index].element = node;
node.scrollIntoView({ block: 'center', behavior: 'auto' });
return node;
}
}
}
refreshOffsetsFromNativeCacheClaude(true);
const target = promptCacheItems[index];
const root = document.querySelector('[data-autoscroll-container="true"]') || getConversationScrollRoot();
if (!target || !root || typeof target._acnOffsetTop !== 'number') {
return null;
}
const myToken = ++claudeNavToken;
const confirmed = claudeConfirmedOffsets.get(index);
const nativeHeightCacheForGate = extractNativeHeightCacheDOMClaude();
const heightIsExact = !!(nativeHeightCacheForGate && target._acnUuid && nativeHeightCacheForGate.has(target._acnUuid));
const isVirtualizerExact = heightIsExact && target._acnOffsetIsReal === true;
if (confirmed || isVirtualizerExact) {
setScrollTop(root, Math.max(0, target._acnOffsetTop));
const deadline = Date.now() + 800;
while (Date.now() < deadline) {
if (myToken !== claudeNavToken) return null;
await sleepClaude(100);
const nodes = extractUserMessageNodesDOMClaude();
const match = nodes.find(node => {
const textEl = node.querySelector(CURRENT_SITE.userMessage.text);
const text = textEl ? extractMessageTextDOMClaude(textEl) : extractMessageTextDOMClaude(node);
return text === target.text;
});
if (match) {
promptCacheItems[index].element = match;
match.scrollIntoView({ block: 'center', behavior: 'auto' });
return match;
}
}
if (confirmed) claudeConfirmedOffsets.delete(index);
}
const currentlyMountedNodes = extractUserMessageNodesDOMClaude();
let liveAnchor = null;
if (currentlyMountedNodes.length > 0) {
const rootRectNow = root.getBoundingClientRect();
const scrollTopNow = getScrollTop(root);
const lastMsgIdx = promptCacheItems.length - 1;
currentlyMountedNodes.forEach(node => {
const textEl = node.querySelector(CURRENT_SITE.userMessage.text);
const text = textEl ? extractMessageTextDOMClaude(textEl) : extractMessageTextDOMClaude(node);
const cacheIdx = resolveCacheIndexClaude(node, text);
if (cacheIdx === -1 || cacheIdx === index || cacheIdx === lastMsgIdx) return;
const liveOffsetTop = (node.getBoundingClientRect().top - rootRectNow.top) + scrollTopNow;
if (!liveAnchor || Math.abs(cacheIdx - index) < Math.abs(liveAnchor.cacheIndex - index)) {
liveAnchor = { cacheIndex: cacheIdx, liveOffsetTop };
}
});
}
const SEED_MAX_DISTANCE = 60;
const nearestConfirmedRaw = findNearestConfirmedOffsetClaude(index);
const nearestConfirmed = (nearestConfirmedRaw && Math.abs(nearestConfirmedRaw.cacheIndex - index) <= SEED_MAX_DISTANCE)
? nearestConfirmedRaw : null;
const liveDist = liveAnchor ? Math.abs(liveAnchor.cacheIndex - index) : Infinity;
const confirmedDist = nearestConfirmed ? Math.abs(nearestConfirmed.cacheIndex - index) : Infinity;
let initialEstimate = target._acnOffsetTop;
if (!isVirtualizerExact) {
if (liveAnchor && liveDist <= confirmedDist) {
initialEstimate = liveAnchor.liveOffsetTop + (target._acnOffsetTop - promptCacheItems[liveAnchor.cacheIndex]._acnOffsetTop);
} else if (nearestConfirmed && promptCacheItems[nearestConfirmed.cacheIndex]) {
initialEstimate = nearestConfirmed.offsetTop + (target._acnOffsetTop - promptCacheItems[nearestConfirmed.cacheIndex]._acnOffsetTop);
}
}
let bestEstimate = initialEstimate;
let haveRealCorrection = false;
let probeCount = 0;
const visited = new Set();
const roundKey = v => Math.round(v / 100) * 100;
const localOffsets = [0, 800, -800, 2000, -2000, 4000, -4000];
const wideOffsets = [0, 1500, -1500, 4000, -4000, 9000, -9000, 16000, -16000];
let currentTarget = initialEstimate;
const maxAttempts = 14;
const POLL_INTERVAL_MS = 150;
const POLL_MAX_MS = 1000;
let lastNearestCacheIndex = null;
let prevNearestCacheIndex = null;
let stuckRepeats = 0;
let fallbackNode = null;
let fallbackCacheIndex = null;
let boundBefore = null;
let boundAfter = null;
let inBisectionMode = false;
const collectMounted = () => {
const nodes = extractUserMessageNodesDOMClaude();
return nodes.map(node => {
const textEl = node.querySelector(CURRENT_SITE.userMessage.text);
const text = textEl ? extractMessageTextDOMClaude(textEl) : extractMessageTextDOMClaude(node);
const cacheIndex = resolveCacheIndexClaude(node, text);
return { node, cacheIndex, text };
}).filter(m => m.cacheIndex !== -1);
};
const lastMessageIndex = promptCacheItems.length - 1;
const excludeNoise = list => list.filter(m => m.cacheIndex !== lastMessageIndex);
let aborted = false;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
if (myToken !== claudeNavToken) { aborted = true; break; }
const maxScroll = Math.max(0, root.scrollHeight - root.clientHeight);
const clientHeight = root.clientHeight || 800;
let baseTarget = Math.min(Math.max(0, currentTarget), maxScroll);
let mounted = [];
let requestedForLog = baseTarget;
if (inBisectionMode) {
const clamped = Math.min(Math.max(0, baseTarget), maxScroll);
requestedForLog = clamped;
setScrollTop(root, clamped);
const pollDeadline = Date.now() + POLL_MAX_MS;
while (Date.now() < pollDeadline) {
if (myToken !== claudeNavToken) { aborted = true; break; }
await sleepClaude(POLL_INTERVAL_MS);
mounted = collectMounted();
if (excludeNoise(mounted).length > 0) break;
}
if (aborted) break;
} else {
if (visited.has(roundKey(baseTarget))) {
baseTarget = Math.min(Math.max(0, baseTarget + (baseTarget >= bestEstimate ? 600 : -600)), maxScroll);
}
visited.add(roundKey(baseTarget));
const viewportPlacements = [
baseTarget - clientHeight * 0.5,
baseTarget - clientHeight * 0.15,
baseTarget - clientHeight * 0.85,
];
let placementUsed = null;
for (const placement of viewportPlacements) {
if (myToken !== claudeNavToken) { aborted = true; break; }
const clamped = Math.min(Math.max(0, placement), maxScroll);
setScrollTop(root, clamped);
const quickDeadline = Date.now() + 1800;
while (Date.now() < quickDeadline) {
if (myToken !== claudeNavToken) { aborted = true; break; }
await sleepClaude(100);
mounted = collectMounted();
if (excludeNoise(mounted).length > 0) break;
}
if (aborted || excludeNoise(mounted).length > 0) { placementUsed = clamped; break; }
}
if (aborted) break;
if (excludeNoise(mounted).length === 0) {
const clampedTarget = Math.min(Math.max(0, viewportPlacements[0]), maxScroll);
requestedForLog = clampedTarget;
setScrollTop(root, clampedTarget);
const pollDeadline = Date.now() + POLL_MAX_MS;
while (Date.now() < pollDeadline) {
if (myToken !== claudeNavToken) { aborted = true; break; }
await sleepClaude(POLL_INTERVAL_MS);
mounted = collectMounted();
if (excludeNoise(mounted).length > 0) break;
}
if (aborted) break;
} else {
requestedForLog = placementUsed;
}
}
const exact = mounted.find(m => m.cacheIndex === index);
if (exact) {
promptCacheItems[index].element = exact.node;
mounted.forEach(m => { if (promptCacheItems[m.cacheIndex]) promptCacheItems[m.cacheIndex].element = m.node; });
exact.node.scrollIntoView({ block: 'center', behavior: 'auto' });
return exact.node;
}
const meaningfulMounted = excludeNoise(mounted);
if (meaningfulMounted.length > 0) {
const closest = meaningfulMounted.reduce((best, m) =>
Math.abs(m.cacheIndex - index) < Math.abs(best.cacheIndex - index) ? m : best
);
if (fallbackCacheIndex === null || Math.abs(closest.cacheIndex - index) < Math.abs(fallbackCacheIndex - index)) {
fallbackNode = closest.node;
fallbackCacheIndex = closest.cacheIndex;
}
}
if (meaningfulMounted.length === 0) {
const table = haveRealCorrection ? localOffsets : wideOffsets;
const anchor = haveRealCorrection ? bestEstimate : initialEstimate;
currentTarget = anchor + table[Math.min(probeCount, table.length - 1)];
probeCount++;
continue;
}
const nearest = meaningfulMounted.reduce((best, m) =>
Math.abs(m.cacheIndex - index) < Math.abs(best.cacheIndex - index) ? m : best
);
if (attempt === 0 && nearestConfirmed && !isVirtualizerExact) {
const landedFar = Math.abs(nearest.cacheIndex - nearestConfirmed.cacheIndex) > 20;
if (landedFar) {
claudeConfirmedOffsets.delete(nearestConfirmed.cacheIndex);
}
}
const boundBeforeSnapshot = boundBefore ? boundBefore.scrollTop : null;
const boundAfterSnapshot = boundAfter ? boundAfter.scrollTop : null;
const actualScrollTop = getScrollTop(root);
meaningfulMounted.forEach(m => {
if (m.cacheIndex < index) {
if (!boundBefore || actualScrollTop > boundBefore.scrollTop) boundBefore = { scrollTop: actualScrollTop, cacheIndex: m.cacheIndex };
} else if (m.cacheIndex > index) {
if (!boundAfter || actualScrollTop < boundAfter.scrollTop) boundAfter = { scrollTop: actualScrollTop, cacheIndex: m.cacheIndex };
}
});
if (inBisectionMode) {
const stalled = boundBefore && boundAfter &&
boundBefore.scrollTop === boundBeforeSnapshot &&
boundAfter.scrollTop === boundAfterSnapshot;
if (stalled) {
break;
}
if (boundBefore && boundAfter && Math.abs(boundAfter.scrollTop - boundBefore.scrollTop) < 150) {
break;
}
if (boundBefore && boundAfter) {
currentTarget = (boundBefore.scrollTop + boundAfter.scrollTop) / 2;
} else {
const nearestCachedItem = promptCacheItems[nearest.cacheIndex];
if (!nearestCachedItem || typeof nearestCachedItem._acnOffsetTop !== 'number') {
probeCount = 0;
continue;
}
const rootRect2 = root.getBoundingClientRect();
const delta = target._acnOffsetTop - nearestCachedItem._acnOffsetTop;
const liveTop = (nearest.node.getBoundingClientRect().top - rootRect2.top) + getScrollTop(root);
currentTarget = liveTop + delta;
}
bestEstimate = currentTarget;
probeCount = 0;
continue;
}
if (nearest.cacheIndex === lastNearestCacheIndex || nearest.cacheIndex === prevNearestCacheIndex) {
stuckRepeats++;
} else {
stuckRepeats = 0;
}
prevNearestCacheIndex = lastNearestCacheIndex;
lastNearestCacheIndex = nearest.cacheIndex;
const rootRect = root.getBoundingClientRect();
const nearestCachedItemMain = promptCacheItems[nearest.cacheIndex];
if (!nearestCachedItemMain || typeof nearestCachedItemMain._acnOffsetTop !== 'number') {
probeCount = 0;
continue;
}
const knownOffsetDelta = target._acnOffsetTop - nearestCachedItemMain._acnOffsetTop;
const nearestLiveOffsetTop = (nearest.node.getBoundingClientRect().top - rootRect.top) + getScrollTop(root);
if (stuckRepeats >= 1 && boundBefore && boundAfter) {
inBisectionMode = true;
currentTarget = (boundBefore.scrollTop + boundAfter.scrollTop) / 2;
} else if (stuckRepeats >= 1) {
const bigJump = 3000 * Math.sign(knownOffsetDelta || 1) * stuckRepeats;
currentTarget = nearestLiveOffsetTop + knownOffsetDelta + bigJump;
lastNearestCacheIndex = null;
prevNearestCacheIndex = null;
} else {
const overshootMargin = 400 * Math.sign(knownOffsetDelta || 1);
currentTarget = nearestLiveOffsetTop + knownOffsetDelta + overshootMargin;
}
bestEstimate = currentTarget;
haveRealCorrection = true;
probeCount = 0;
}
if (fallbackNode) {
if (promptCacheItems[index]) promptCacheItems[index].element = fallbackNode;
if (!aborted) fallbackNode.scrollIntoView({ block: 'center', behavior: 'auto' });
return aborted ? null : fallbackNode;
}
return null;
}
// ============================================================================
// 06.7 File data and file actions
// ---------------------------------------------------------------------------
function extractUserFilesDataClaude(m) {
const files = [];
if (Array.isArray(m.attachments)) {
m.attachments.forEach(a => {
const isPasted = !a.file_name;
let ext;
if (isPasted) {
ext = 'Pasted';
} else {
const extMatch = a.file_name.match(/\.([a-z0-9]{1,8})$/i);
ext = extMatch ? extMatch[1] : (a.file_type || 'txt');
}
files.push({
ext,
name: a.file_name || 'Pasted text',
size: formatFileSizeClaude(a.file_size),
preview: a.extracted_content ? a.extracted_content.slice(0, 200) : null,
});
});
}
if (Array.isArray(m.files)) {
m.files.forEach(f => {
const name = f.file_name || 'file';
const extMatch = name.match(/\.([a-z0-9]{1,8})$/i);
files.push({
ext: extMatch ? extMatch[1] : '',
name,
size: formatFileSizeClaude(f.size_bytes),
preview: null,
fileKind: f.file_kind || null,
path: f.path || null,
previewUrl: f.preview_url || null,
documentUrl: (f.document_asset && f.document_asset.url) || null,
thumbnailUrl: (f.thumbnail_asset && f.thumbnail_asset.url) || null,
});
});
}
return files.slice(0, 3);
}
function getVersionControlDataClaude(container) {
if (!CURRENT_SITE.versionControl) return null;
const scope = extractVersionScopeDOMClaude(container);
return scope ? extractVersionControlDOM(scope, CURRENT_SITE.versionControl) : null;
}
function getBadgeUserMessageDataClaude(userMessage) {
return { files: extractUserFilesDataClaude(userMessage) };
}
function getFileBadgeAIMessageDataClaude(assistantMessage) {
return {
generatedFiles: assistantMessage && assistantMessage.sender === 'assistant'
? extractGeneratedFilesDataClaude(assistantMessage).filter(file => file && file.kind !== 'inline-image')
: []
};
}
function getImageBadgeAIMessageDataClaude(assistantMessage) {
return {
generatedFiles: assistantMessage && assistantMessage.sender === 'assistant'
? extractGeneratedFilesDataClaude(assistantMessage).filter(file => file && file.kind === 'inline-image')
: []
};
}
function extractGeneratedFilesDataClaude(m) {
if (!m || !Array.isArray(m.content)) return [];
const files = [];
m.content.forEach(block => {
if (block.type === 'tool_result' && Array.isArray(block.content)) {
block.content.forEach(c => {
if (c.type === 'local_resource') {
const displayName = c.name || (c.file_path ? c.file_path.split('/').pop() : 'file');
const technicalName = c.file_path ? c.file_path.split('/').pop() : displayName;
const extMatch = technicalName.match(/\.([a-z0-9]{1,8})$/i);
files.push({ name: displayName, ext: extMatch ? extMatch[1].toLowerCase() : '', mimeType: c.mime_type || null, filePath: c.file_path || null, uuid: c.uuid || null });
}
});
}
});
let inlineImageOrdinal = 0;
m.content.forEach(block => {
if (block.type === 'tool_result' && Array.isArray(block.content)) {
block.content.forEach(c => {
if (c.type === 'image' && c.file_uuid) {
files.push({
name: 'image-' + (inlineImageOrdinal + 1),
ext: 'img',
mimeType: null,
filePath: null,
uuid: null,
kind: 'inline-image',
fileUuid: c.file_uuid,
ordinal: inlineImageOrdinal,
});
inlineImageOrdinal++;
}
});
}
});
m.content.forEach(block => {
if (block.type === 'tool_use' && block.name === 'visualize:show_widget' && block.input && typeof block.input.widget_code === 'string') {
const code = block.input.widget_code;
const isSvg = code.trim().startsWith('<svg');
files.push({
name: block.input.title || 'widget',
ext: isSvg ? 'svg' : 'html',
mimeType: isSvg ? 'image/svg+xml' : 'text/html',
filePath: null,
uuid: null,
kind: 'widget',
widgetCode: code,
});
}
});
const artifactsById = new Map();
m.content.forEach(block => {
if (block.type === 'tool_use' && block.name === 'artifacts' && block.input && block.input.id) {
const id = block.input.id;
const existing = artifactsById.get(id) || {};
artifactsById.set(id, {
id,
title: block.input.title || existing.title || null,
type: block.input.type || existing.type || null,
});
}
});
artifactsById.forEach((input, id) => {
files.push({
name: input.title || id,
ext: '',
mimeType: null,
filePath: null,
uuid: null,
kind: 'artifact',
artifactId: id,
artifactType: input.type || null,
});
});
return files;
}
async function downloadUserFileClaude(f, index, fileIdx) {
const directUrl = buildFileDownloadUrlClaude(f);
let fileExists = Boolean(directUrl);
if (directUrl) {
try {
const resp = await fetch(directUrl, { method: 'GET', redirect: 'manual' });
fileExists = resp.type === 'opaqueredirect' || resp.ok;
} catch (err) {
fileExists = false;
}
}
if (fileExists) {
const a = document.createElement('a');
a.href = directUrl;
a.download = f.name || '';
a.rel = 'noopener';
document.body.appendChild(a);
a.click();
a.remove();
} else {
await openFilePreviewClaude(index, fileIdx, { autoDownload: true });
}
}
async function focusAndDownloadGeneratedFileClaude(index, fileIdx, file) {
if (!IS_CLAUDE) return false;
const aiConfig = CURRENT_SITE.aiMessage;
if (!aiConfig || !aiConfig.relationRoot || !aiConfig.file || !aiConfig.fileDownload) return false;
const getUserArticle = () => {
const item = promptCacheItems[index - 1];
return item && item.element && item.element.isConnected ? item.element : null;
};
const getAssistantArticle = (userArticle) => {
if (!userArticle) return null;
const allArticles = Array.from(document.querySelectorAll(aiConfig.relationRoot));
const idx = allArticles.indexOf(userArticle);
return idx >= 0 && idx + 1 < allArticles.length ? allArticles[idx + 1] : null;
};
showNavFreezeClaude();
try {
await navigateToMessageInner(index);
await sleepClaude(300);
const userArticle = getUserArticle();
if (!userArticle) return false;
let assistantArticle = getAssistantArticle(userArticle);
for (let i = 0; i < 8 && !assistantArticle; i++) {
await sleepClaude(250);
assistantArticle = getAssistantArticle(userArticle);
}
if (!assistantArticle) return false;
const blocks = Array.from(assistantArticle.querySelectorAll(aiConfig.file));
let block = null;
if (file && file.name) block = blocks.find(el => el.textContent && el.textContent.includes(file.name)) || null;
if (!block && Number.isFinite(fileIdx)) block = blocks[fileIdx] || null;
block = block || blocks[0] || null;
if (!block) return false;
await focusFileElement(block, { wait: sleepClaude });
const downloadBtn = block.querySelector(aiConfig.fileDownload);
if (downloadBtn) downloadBtn.click();
return true;
} finally {
hideNavLoadingOverlayClaude();
}
}
async function openArtifactPanelClaude(index, fileIdx) {
const aiConfig = CURRENT_SITE.aiMessage;
if (!aiConfig || !aiConfig.relationRoot || !aiConfig.file || !aiConfig.fileOpen) return false;
const getUserArticle = () => {
const item = promptCacheItems[index - 1];
return item && item.element && item.element.isConnected ? item.element : null;
};
const getAssistantArticle = (userArticle) => {
if (!userArticle) return null;
const allArticles = Array.from(document.querySelectorAll(aiConfig.relationRoot));
const idx = allArticles.indexOf(userArticle);
if (idx === -1 || idx + 1 >= allArticles.length) return null;
return allArticles[idx + 1];
};
const root = aiConfig.scrollRoot ? document.querySelector(aiConfig.scrollRoot) : null;
const originalIndex = IS_CLAUDE ? claudeCurrentVisibleIndex : null;
let originalRelativeOffset = null;
if (root && originalIndex) {
const origItem = promptCacheItems[originalIndex - 1];
const origNode = origItem && origItem.element && origItem.element.isConnected ? origItem.element : null;
if (origNode) {
const rootRect = root.getBoundingClientRect();
const liveOffsetTopNow = (origNode.getBoundingClientRect().top - rootRect.top) + root.scrollTop;
originalRelativeOffset = root.scrollTop - liveOffsetTopNow;
}
}
showNavFreezeClaude();
try {
await navigateToMessageInner(index);
await sleepClaude(300);
const userArticle = getUserArticle();
if (!userArticle) return false;
let assistantArticle = getAssistantArticle(userArticle);
for (let i = 0; i < 8 && !assistantArticle; i++) {
await sleepClaude(250);
assistantArticle = getAssistantArticle(userArticle);
}
if (!assistantArticle) return false;
const blocks = assistantArticle.querySelectorAll(aiConfig.file);
const block = blocks[fileIdx];
if (!block) return false;
const openBtn = block.querySelector(aiConfig.fileOpen) || block;
openBtn.click();
return true;
} finally {
if (IS_CLAUDE && originalIndex) {
await ensurePromptLoadedThenFind(originalIndex - 1);
await sleepClaude(300);
const returnedItem = promptCacheItems[originalIndex - 1];
const returnedNode = returnedItem && returnedItem.element && returnedItem.element.isConnected ? returnedItem.element : null;
if (returnedNode && root) {
const rootRect2 = root.getBoundingClientRect();
const currentLiveOffsetTop = (returnedNode.getBoundingClientRect().top - rootRect2.top) + root.scrollTop;
const desiredScrollTop = originalRelativeOffset !== null ? currentLiveOffsetTop + originalRelativeOffset : currentLiveOffsetTop;
root.scrollTop = Math.max(0, desiredScrollTop);
}
}
hideNavLoadingOverlayClaude();
}
}
async function openInlineImagePreviewClaude(index, fileIdx, file) {
if (!IS_CLAUDE) return false;
const aiConfig = CURRENT_SITE.aiMessage;
if (!aiConfig || !aiConfig.relationRoot || !aiConfig.image) return false;
const getUserArticle = () => {
const item = promptCacheItems[index - 1];
return item && item.element && item.element.isConnected ? item.element : null;
};
const getAssistantArticle = (userArticle) => {
if (!userArticle) return null;
const allArticles = Array.from(document.querySelectorAll(aiConfig.relationRoot));
const idx = allArticles.indexOf(userArticle);
if (idx === -1 || idx + 1 >= allArticles.length) return null;
return allArticles[idx + 1];
};
const root = aiConfig.scrollRoot ? document.querySelector(aiConfig.scrollRoot) : null;
const originalIndex = IS_CLAUDE ? claudeCurrentVisibleIndex : null;
let originalRelativeOffset = null;
if (root && originalIndex) {
const origItem = promptCacheItems[originalIndex - 1];
const origNode = origItem && origItem.element && origItem.element.isConnected ? origItem.element : null;
if (origNode) {
const rootRect = root.getBoundingClientRect();
const liveOffsetTopNow = (origNode.getBoundingClientRect().top - rootRect.top) + root.scrollTop;
originalRelativeOffset = root.scrollTop - liveOffsetTopNow;
}
}
showNavFreezeClaude();
try {
await navigateToMessageInner(index);
await sleepClaude(300);
const userArticle = getUserArticle();
if (!userArticle) return false;
let assistantArticle = getAssistantArticle(userArticle);
for (let i = 0; i < 8 && !assistantArticle; i++) {
await sleepClaude(250);
assistantArticle = getAssistantArticle(userArticle);
}
if (!assistantArticle) return false;
const tiles = Array.from(assistantArticle.querySelectorAll(aiConfig.image));
const ordinal = file && Number.isFinite(file.ordinal) ? file.ordinal : fileIdx;
const tile = tiles[ordinal];
if (!tile) return false;
tile.click();
return true;
} finally {
if (IS_CLAUDE && originalIndex) {
await ensurePromptLoadedThenFind(originalIndex - 1);
await sleepClaude(300);
const returnedItem = promptCacheItems[originalIndex - 1];
const returnedNode = returnedItem && returnedItem.element && returnedItem.element.isConnected ? returnedItem.element : null;
if (returnedNode && root) {
const rootRect2 = root.getBoundingClientRect();
const currentLiveOffsetTop = (returnedNode.getBoundingClientRect().top - rootRect2.top) + root.scrollTop;
const desiredScrollTop = originalRelativeOffset !== null ? currentLiveOffsetTop + originalRelativeOffset : currentLiveOffsetTop;
root.scrollTop = Math.max(0, desiredScrollTop);
}
}
hideNavLoadingOverlayClaude();
}
}
function downloadWidgetFileClaude(file) {
if (!file || typeof file.widgetCode !== 'string') return false;
const isSvg = file.widgetCode.trim().startsWith('<svg');
const blob = new Blob([file.widgetCode], { type: isSvg ? 'image/svg+xml' : 'text/html' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = (file.name || 'widget') + '.' + (isSvg ? 'svg' : 'html');
document.body.appendChild(a);
a.click();
a.remove();
return true;
}
function activateGeneratedFileShareClaude(file, index, fileIdx) {
const aiConfig = CURRENT_SITE.aiMessage;
if (!aiConfig) return false;
const item = promptCacheItems[index - 1];
const assistantEl = item && item._acnAssistantElement;
if (!assistantEl || !assistantEl.isConnected) return false;
if (file && file.kind === 'inline-image' && aiConfig.image) {
const tiles = Array.from(assistantEl.querySelectorAll(aiConfig.image));
const ordinal = Number.isFinite(file.ordinal) ? file.ordinal : fileIdx;
const tile = tiles[ordinal];
if (!tile) return false;
tile.click();
return true;
}
if (aiConfig.file) {
const blocks = Array.from(assistantEl.querySelectorAll(aiConfig.file));
let block = null;
if (file && file.name) block = blocks.find(el => el.textContent && el.textContent.includes(file.name)) || null;
if (!block && Number.isFinite(fileIdx)) block = blocks[fileIdx] || null;
block = block || blocks[0] || null;
if (!block) return false;
const openButton = aiConfig.fileOpen ? block.querySelector(aiConfig.fileOpen) : null;
const downloadButton = aiConfig.fileDownload ? block.querySelector(aiConfig.fileDownload) : null;
const target = openButton || downloadButton || block;
target.click();
return true;
}
return false;
}
if (IS_CLAUDE) currentFileAdapter = CLAUDE_FILE_ADAPTER;
// ============================================================================
// 07. CHATGPT
// ============================================================================
// ============================================================================
// 07.1 Conversation & URL
// ============================================================================
function extractConversationRootDOMGPT() {
return document.querySelector('infinite-scroller.chat-history, #thread, main#main, [data-radix-scroll-area-viewport]')
|| document.querySelector('main')
|| document.body;
}
function extractTurnIdDOMGPT(node) {
if (!(node instanceof HTMLElement)) return '';
const turn = node.closest('[data-turn-id], [data-turn-id-container], [data-message-id], [data-testid^="conversation-turn"]') || node;
return turn.getAttribute('data-turn-id')
|| turn.getAttribute('data-turn-id-container')
|| turn.getAttribute('data-message-id')
|| turn.getAttribute('data-testid')
|| '';
}
function extractConversationSnapshotDOMGPT() {
if (!IS_CHATGPT) return null;
const root = extractConversationRootDOMGPT();
const allUserNodes = root
? Array.from(root.querySelectorAll('user-query, div[data-message-author-role="user"]')).filter(isElementVisibleForNav)
: [];
const userNodes = collapseMessageVersionsGPT(allUserNodes.map(node => ({ element: node })))
.map(item => item.element)
.filter(Boolean);
const tocButtons = Array.from(document.querySelectorAll('button[data-toc-item-index]'))
.filter(button => button instanceof HTMLElement && button.isConnected);
const tocGroup = tocButtons.length ? tocButtons[0].parentElement : null;
const sampleNodes = userNodes.length <= 4
? userNodes
: [userNodes[0], userNodes[1], userNodes[userNodes.length - 2], userNodes[userNodes.length - 1]];
const nodeIds = sampleNodes.map(extractTurnIdDOMGPT).filter(Boolean);
const textHashes = sampleNodes.map(node => stableHashText(extractCleanTextDOM(node).slice(0, 240)));
const tocIndices = tocButtons
.map((button, index) => extractTOCIndexDOMGPT(button, index))
.filter(Number.isFinite)
.sort((a, b) => a - b);
const identitySignature = nodeIds.join(',');
const textSignature = textHashes.join(',');
const tocSignature = tocIndices.join(',');
return {
root,
tocGroup,
firstUserNode: userNodes[0] || null,
lastUserNode: userNodes[userNodes.length - 1] || null,
hasContent: userNodes.length > 0 || tocButtons.length > 0,
identitySignature,
textSignature,
tocSignature,
signature: [userNodes.length, tocSignature, identitySignature, textSignature].join('::')
};
}
function hasConversationSnapshotChangedGPT(previous, current, elapsedMs = 0) {
if (!previous) return true;
if (!current) return false;
if (!previous.hasContent) return true;
if (previous.root && current.root && previous.root !== current.root) return true;
if (previous.tocGroup && current.tocGroup && previous.tocGroup !== current.tocGroup) return true;
if (previous.hasContent && !current.hasContent) return true;
if (previous.identitySignature && current.identitySignature && previous.identitySignature !== current.identitySignature) return true;
const oldNodesDetached = Boolean(
(previous.tocGroup && !previous.tocGroup.isConnected)
|| (previous.firstUserNode && !previous.firstUserNode.isConnected)
|| (previous.lastUserNode && !previous.lastUserNode.isConnected)
);
if (oldNodesDetached && previous.signature !== current.signature) return true;
if (elapsedMs >= 180 && previous.textSignature && current.textSignature && previous.textSignature !== current.textSignature) return true;
if (elapsedMs >= 250 && previous.tocSignature !== current.tocSignature) return true;
return false;
}
function isTransientConversationUrlGPT(url = window.location.href) {
if (!IS_CHATGPT) return false;
try {
const parsed = new URL(url, window.location.origin);
return /^\/c\/WEB:/i.test(parsed.pathname);
} catch (_) {
return false;
}
}
function isNewChatRootUrlGPT(url) {
if (!IS_CHATGPT) return false;
try {
const parsed = new URL(url, window.location.origin);
return parsed.origin === window.location.origin && parsed.pathname === '/';
} catch (_) {
return false;
}
}
function getComparableConversationUrlGPT() {
const rawUrl = window.location.href;
if (isTransientConversationUrlGPT(rawUrl)) {
chatGptSawTransientWebUrl = true;
return lastUrl;
}
return rawUrl;
}
function hideNavigatorDuringConversationTransitionGPT() {
const container = document.getElementById('message-nav');
if (container) container.style.display = 'none';
updateShiftStyles(false);
document.body.classList.remove('navigator-expanded');
}
function resetConversationStateGPT() {
lastPromptsContent = '';
activeMessageIndex = -1;
cachedPrompts = [];
promptCacheUrl = '';
promptCacheItems = [];
promptCacheMap = new Map();
historyLoadedUrl = '';
resetTOCStateGPT(true);
}
function beginConversationTransitionGPT(nextUrl) {
const previousSnapshot = chatGptQueuedPreviousSnapshot
|| chatGptLastCommittedConversationSnapshot
|| extractConversationSnapshotDOMGPT();
chatGptQueuedPreviousSnapshot = null;
chatGptNavigationToken += 1;
chatGptPendingConversationUrl = nextUrl;
chatGptPendingConversationSnapshot = previousSnapshot;
resetConversationStateGPT();
hideNavigatorDuringConversationTransitionGPT();
return chatGptNavigationToken;
}
async function waitForConversationReadyGPT(token, expectedUrl) {
const startedAt = Date.now();
while (token === chatGptNavigationToken && window.location.href === expectedUrl) {
if (chatGptPendingConversationUrl !== expectedUrl) return true;
const currentSnapshot = extractConversationSnapshotDOMGPT();
const elapsedMs = Date.now() - startedAt;
if (hasConversationSnapshotChangedGPT(chatGptPendingConversationSnapshot, currentSnapshot, elapsedMs)) {
chatGptPendingConversationUrl = '';
chatGptPendingConversationSnapshot = null;
return true;
}
if (elapsedMs >= CHATGPT_CONVERSATION_READY_TIMEOUT_MS) return false;
await sleep(CHATGPT_CONVERSATION_READY_POLL_MS);
}
return false;
}
function commitConversationSnapshotGPT() {
if (!IS_CHATGPT || chatGptPendingConversationUrl) return;
chatGptLastCommittedConversationSnapshot = extractConversationSnapshotDOMGPT();
}
function queuePreviousConversationSnapshotGPT(preferCurrent = true) {
if (!IS_CHATGPT) return;
const currentSnapshot = extractConversationSnapshotDOMGPT();
chatGptQueuedPreviousSnapshot = preferCurrent
? (currentSnapshot || chatGptLastCommittedConversationSnapshot)
: (chatGptLastCommittedConversationSnapshot || currentSnapshot);
}
function isTOCEnabledGPT() {
return Boolean(IS_CHATGPT && CURRENT_SITE.nativePromptToc);
}
function resetTOCStateGPT(force = false) {
if (!IS_CHATGPT) return;
const comparableUrl = getComparableConversationUrlGPT();
if (!force && chatGptTocCacheUrl === comparableUrl) return;
chatGptTocCacheUrl = comparableUrl;
ensureChatGPTApiConversationCache(true);
chatGptStickyActiveOrdinal = -1;
chatGptHarvestedOffsets.clear();
chatGptTocTextCache.clear();
chatGptTocDetailCache.clear();
chatGptTocHoverPromise = null;
chatGptTocHoverSignature = '';
chatGptTocHoverAttemptedSignature = '';
chatGptTocSyntheticHoverGroup = null;
chatGptTocInitialHarvestDone = false;
chatGptTocPostActionSequence += 1;
chatGptTocPostActionPending = null;
chatGptTocPostActionTimers.forEach(timer => timer && timer.clear && timer.clear());
chatGptTocPostActionTimers = [];
if (chatGptTocObserver) chatGptTocObserver.disconnect();
chatGptTocObserver = null;
chatGptTocObservedGroup = null;
if (chatGptTocRefreshTimer) {
chatGptTocRefreshTimer.clear();
chatGptTocRefreshTimer = null;
}
}
function resetTOCStateIfNeededGPT() {
resetTOCStateGPT(false);
}
// ============================================================================
// 07.2 Backend-API full-conversation fetch & cache
// ============================================================================
let chatGptApiConversationCache = null;
let chatGptApiFetchInFlight = null;
let chatGptApiLastFetchAttemptAt = 0;
const CHATGPT_API_RETRY_THROTTLE_MS = 4000;
function getChatGPTConversationIdFromUrlAPI() {
if (!IS_CHATGPT) return null;
const match = window.location.pathname.match(/\/c\/([^/?#]+)/);
return match ? match[1] : null;
}
function formatFileSizeGPT(bytes) {
if (!bytes || bytes <= 0) return null;
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
return (bytes / (1024 * 1024 * 1024)).toFixed(1) + ' GB';
}
async function fetchChatGPTAccessTokenAPI() {
try {
const res = await fetch('/api/auth/session', { credentials: 'include' });
if (!res.ok) return '';
const session = await res.json();
return (session && session.accessToken) || '';
} catch (err) {
return '';
}
}
async function fetchChatGPTConversationAPI(conversationId) {
try {
const token = await fetchChatGPTAccessTokenAPI();
const headers = token ? { Authorization: `Bearer ${token}` } : {};
const res = await fetch(`/backend-api/conversation/${conversationId}`, { credentials: 'include', headers });
if (!res.ok) return null;
return await res.json();
} catch (err) {
return null;
}
}
function buildChatGPTMessageListFromMappingAPI(conversation) {
const mapping = conversation.mapping || {};
const childrenByParent = new Map();
Object.values(mapping).forEach(node => {
if (!node.parent) return;
if (!childrenByParent.has(node.parent)) childrenByParent.set(node.parent, []);
childrenByParent.get(node.parent).push(node.id);
});
let nodeId = conversation.current_node;
const path = [];
while (nodeId && mapping[nodeId]) {
path.push(mapping[nodeId]);
nodeId = mapping[nodeId].parent;
}
path.reverse();
const messages = [];
const allTurnIds = [];
let currentUserOrdinalFiles = null;
function findConfirmedDownloadFilenamesGPT(fromIndex) {
for (let i = fromIndex + 1; i < path.length; i++) {
const n = path[i];
const msg = n.message;
if (!msg || !msg.author) continue;
if (msg.author.role === 'user') break;
if (msg.author.role !== 'assistant') continue;
const content = msg.content;
if (!content || content.content_type !== 'text' || !Array.isArray(content.parts)) continue;
if (msg.end_turn !== true) continue;
if (!msg.metadata || msg.metadata.can_save !== true) continue;
const text = content.parts.join(' ');
const linkPattern = /sandbox:\/mnt\/data\/([^\s"'\n)]+)/g;
const names = [];
let m;
while ((m = linkPattern.exec(text))) {
names.push(m[1]);
}
if (names.length) return { names, downloadMessageId: n.id };
}
return { names: [], downloadMessageId: null };
}
for (let pathIndex = 0; pathIndex < path.length; pathIndex++) {
const node = path[pathIndex];
const message = node.message;
if (!message || !message.author) continue;
const role = message.author.role;
if (role === 'tool') {
const isSuccessfulExecutionOutput = message.content
&& message.content.content_type === 'execution_output'
&& message.metadata
&& message.metadata.aggregate_result
&& message.metadata.aggregate_result.status === 'success';
if (isSuccessfulExecutionOutput && currentUserOrdinalFiles) {
const { names, downloadMessageId } = findConfirmedDownloadFilenamesGPT(pathIndex);
names.forEach(name => {
const extMatch = name.match(/\.([a-zA-Z0-9]{1,10})$/);
const ext = extMatch ? extMatch[1].toLowerCase() : 'file';
const isImageExt = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg'].includes(ext);
currentUserOrdinalFiles.push({
name,
ext,
kind: isImageExt ? 'chatgpt-generated-image' : 'chatgpt-generated-file',
toolMessageId: message.id,
codeMessageId: node.parent || null,
downloadMessageId,
sandboxPath: '/mnt/data/' + name
});
});
}
const isImageAssetPointer = message.content
&& message.content.content_type === 'multimodal_text'
&& Array.isArray(message.content.parts);
if (isImageAssetPointer && currentUserOrdinalFiles) {
message.content.parts.forEach(part => {
if (!part || typeof part !== 'object' || part.content_type !== 'image_asset_pointer' || typeof part.asset_pointer !== 'string') return;
const assetMatch = part.asset_pointer.match(/^sediment:\/\/(file_[a-f0-9]+)$/i);
if (!assetMatch) return;
const assetId = assetMatch[1];
const title = (message.metadata && typeof message.metadata.image_gen_title === 'string' && message.metadata.image_gen_title.trim()) || 'Generated image';
const mime = typeof part.mime_type === 'string' ? part.mime_type : '';
const ext = (mime.includes('/') ? mime.split('/')[1] : '') || 'png';
currentUserOrdinalFiles.push({
name: title,
displayName: `${title}.${ext}`,
ext,
kind: 'chatgpt-generated-image',
toolMessageId: message.id,
attachmentId: assetId
});
});
}
continue;
}
if (!['user', 'assistant'].includes(role)) continue;
allTurnIds.push({ id: message.id, role: message.author.role, parentId: node.parent || null });
if (role === 'user') currentUserOrdinalFiles = [];
const parts = Array.isArray(message.content && message.content.parts) ? message.content.parts : [];
const text = parts.filter(part => typeof part === 'string').join('\n').trim();
if (!text && role !== 'user') continue;
const siblings = node.parent ? (childrenByParent.get(node.parent) || [node.id]) : [node.id];
const siblingIndex = siblings.indexOf(node.id);
let apiUserFiles;
if (role === 'user') {
const rawAttachments = message.metadata && Array.isArray(message.metadata.attachments)
? message.metadata.attachments
: [];
apiUserFiles = rawAttachments
.filter(a => a && typeof a.name === 'string')
.map(a => {
const isImage = typeof a.mime_type === 'string' && a.mime_type.startsWith('image/');
const extMatch = a.name.match(/\.([a-zA-Z0-9]+)$/);
return {
name: a.name,
ext: extMatch ? extMatch[1].toLowerCase() : (isImage ? 'img' : 'file'),
size: formatFileSizeGPT(typeof a.size === 'number' ? a.size : null),
mimeType: typeof a.mime_type === 'string' ? a.mime_type : null,
libraryFileId: typeof a.library_file_id === 'string' ? a.library_file_id : null,
attachmentId: typeof a.id === 'string' ? a.id : null,
sourceType: isImage ? 'chatgpt-user-image' : 'chatgpt-user-file'
};
});
}
const writingBlocksField = message.metadata && message.metadata.writing_blocks;
if (role === 'assistant' && writingBlocksField && typeof writingBlocksField === 'object' && currentUserOrdinalFiles) {
Object.entries(writingBlocksField).forEach(([blockId, block]) => {
if (!block) return;
const rawTitle = typeof block.title === 'string' ? block.title.trim() : '';
const ordinal = Number.isFinite(parseInt(block.index, 10)) ? parseInt(block.index, 10) : 0;
const hasRealTitle = rawTitle && rawTitle.toLowerCase() !== 'untitled';
const name = hasRealTitle ? rawTitle : `Writing block ${ordinal + 1}`;
const displayName = hasRealTitle ? `Writing block: ${rawTitle}` : `Writing block ${ordinal + 1}`;
currentUserOrdinalFiles.push({
name,
displayName,
ext: 'writing block',
kind: 'chatgpt-writing-block',
aiMessageTurnId: message.id,
ordinal,
writingBlockId: blockId
});
});
}
messages.push({
id: message.id,
nodeId: node.id,
parentId: node.parent || null,
role: message.author.role,
text,
siblingIds: siblings,
siblingIndex: siblingIndex >= 0 ? siblingIndex : 0,
siblingCount: siblings.length,
createTime: typeof message.create_time === 'number' ? message.create_time : null,
apiGeneratedFiles: role === 'user' ? currentUserOrdinalFiles : undefined,
apiUserFiles: role === 'user' ? apiUserFiles : undefined
});
}
return { messages, allTurnIds };
}
function getChatGPTApiVersionInfoForUserOrdinal(ordinalIndex) {
const cache = chatGptApiConversationCache;
if (!cache || cache.conversationId !== getChatGPTConversationIdFromUrlAPI()) return null;
const userMessages = cache.userMessages;
if (!Array.isArray(userMessages)) return null;
const entry = userMessages[ordinalIndex];
if (!entry || !(entry.siblingCount > 1)) return null;
return {
currentVersion: entry.siblingIndex + 1,
totalVersions: entry.siblingCount,
hasMultipleVersions: true
};
}
function getChatGPTApiUserOrdinalByMessageId(messageId) {
if (!messageId) return -1;
const cache = chatGptApiConversationCache;
if (!cache || cache.conversationId !== getChatGPTConversationIdFromUrlAPI()) return -1;
if (!cache.userOrdinalById) {
cache.userOrdinalById = new Map();
(cache.userMessages || []).forEach((m, i) => cache.userOrdinalById.set(m.id, i));
}
return cache.userOrdinalById.has(messageId) ? cache.userOrdinalById.get(messageId) : -1;
}
function resolveChatGPTApiUserOrdinalForAnyMessageId(messageId) {
if (!messageId) return -1;
const cache = chatGptApiConversationCache;
if (!cache || cache.conversationId !== getChatGPTConversationIdFromUrlAPI()) return -1;
if (!cache.allTurnsById) {
cache.allTurnsById = new Map();
(cache.allTurnIds || []).forEach(m => cache.allTurnsById.set(m.id, m));
}
let msg = cache.allTurnsById.get(messageId);
let guard = 0;
while (msg && msg.role !== 'user' && guard < 50) {
msg = msg.parentId ? cache.allTurnsById.get(msg.parentId) : null;
guard++;
}
if (!msg || msg.role !== 'user') return -1;
return getChatGPTApiUserOrdinalByMessageId(msg.id);
}
async function ensureChatGPTApiConversationCache(forceRefresh = false) {
if (!IS_CHATGPT) return null;
const conversationId = getChatGPTConversationIdFromUrlAPI();
if (!conversationId) {
chatGptApiConversationCache = null;
return null;
}
if (!forceRefresh && chatGptApiConversationCache && chatGptApiConversationCache.conversationId === conversationId) {
return chatGptApiConversationCache;
}
if (chatGptApiFetchInFlight && chatGptApiFetchInFlight.conversationId === conversationId) {
return chatGptApiFetchInFlight.promise;
}
if (!forceRefresh && Date.now() - chatGptApiLastFetchAttemptAt < CHATGPT_API_RETRY_THROTTLE_MS) {
return chatGptApiConversationCache;
}
chatGptApiLastFetchAttemptAt = Date.now();
const promise = (async () => {
const conversation = await fetchChatGPTConversationAPI(conversationId);
if (!conversation) return null;
const { messages, allTurnIds } = buildChatGPTMessageListFromMappingAPI(conversation);
const result = {
conversationId,
current_node: conversation.current_node,
fetchedAt: Date.now(),
messages,
allTurnIds,
userMessages: messages.filter(m => m.role === 'user')
};
chatGptApiConversationCache = result;
return result;
})();
chatGptApiFetchInFlight = { conversationId, promise };
const result = await promise;
if (chatGptApiFetchInFlight && chatGptApiFetchInFlight.conversationId === conversationId) {
chatGptApiFetchInFlight = null;
}
return result;
}
function isChatGPTApiReadyGPT() {
const cache = chatGptApiConversationCache;
return Boolean(
cache
&& cache.conversationId === getChatGPTConversationIdFromUrlAPI()
&& Array.isArray(cache.userMessages)
&& cache.userMessages.length
);
}
function parseMessageCreatedAtGPT(createTime) {
if (typeof createTime !== 'number' || !isFinite(createTime)) return null;
const d = new Date(createTime * 1000);
return isNaN(d.getTime()) ? null : d;
}
if (IS_CHATGPT && typeof unsafeWindow !== 'undefined') {
unsafeWindow.__acnChatGPTApiCache = () => chatGptApiConversationCache;
unsafeWindow.__acnChatGPTApiRefresh = () => ensureChatGPTApiConversationCache(true);
unsafeWindow.__acnChatGPTNumTurnsRewriteActive = () => chatGptNumTurnsRewriteActive;
unsafeWindow.__acnChatGPTScrollRoot = () => getConversationScrollRoot();
}
// ============================================================================
// 07.3 Force-render and sentinel pagination
// ============================================================================
function locateChatGPTForceRenderHooks() {
const debug = typeof unsafeWindow !== 'undefined' && unsafeWindow.__acnChatGPTScrollDebug;
const scrollRoot = getConversationScrollRoot();
const turnSelector = CURRENT_SITE.virtualScroll && CURRENT_SITE.virtualScroll.messageContainer;
let msg = null;
if (scrollRoot && turnSelector) {
const turn = scrollRoot.querySelector(turnSelector);
if (turn) msg = turn.closest('[data-message-author-role="user"]') || turn.querySelector('[data-message-author-role="user"]') || turn;
}
if (!msg) msg = (scrollRoot || document).querySelector('[data-message-author-role="user"]');
if (!msg) {
if (debug) console.log('[ACN-GPT-HOOKS]', { fail: 'no message element found', scopedToScrollRoot: Boolean(scrollRoot), usedTurnSelector: Boolean(scrollRoot && turnSelector), bundleFingerprint: getChatGPTBundleFingerprintGPT() });
return null;
}
const fiberKey = Object.keys(msg).find(k => k.startsWith('__reactFiber$'));
if (!fiberKey) {
if (debug) console.log('[ACN-GPT-HOOKS]', { fail: 'no __reactFiber$ key on the found message element', bundleFingerprint: getChatGPTBundleFingerprintGPT() });
return null;
}
function propsLookRight(props) {
if (!props || typeof props !== 'object') return false;
let hasScrollContainerRefShape = false;
let hasConversationShape = false;
for (const key in props) {
const val = props[key];
if (!hasScrollContainerRefShape && val && typeof val === 'object' && 'current' in val
&& (val.current === null || val.current instanceof HTMLElement)) {
hasScrollContainerRefShape = true;
}
if (!hasConversationShape && val && typeof val === 'object' && !(val instanceof HTMLElement)
&& ('id' in val || 'conversationId' in val)) {
hasConversationShape = true;
}
}
return hasScrollContainerRefShape && hasConversationShape;
}
let fiber = msg[fiberKey];
let depth = 0;
let componentsChecked = 0;
let hookShapeMatchesRejectedByPropCheck = 0;
while (fiber && depth < 400) {
let hook = fiber.memoizedState;
let idx = 0;
const hooks = [];
while (hook && idx < 40) {
const hasDispatch = Boolean(hook.queue && typeof hook.queue.dispatch === 'function');
const isRefShape = !hasDispatch && hook.memoizedState && typeof hook.memoizedState === 'object'
&& 'current' in hook.memoizedState && Object.keys(hook.memoizedState).length === 1;
hooks.push({ hook, hasDispatch, isRefShape, value: hook.memoizedState });
hook = hook.next;
idx++;
}
if (hooks.length >= 4) {
componentsChecked++;
for (let i = 0; i + 3 < hooks.length; i++) {
if (hooks[i].hasDispatch && hooks[i + 1].isRefShape && hooks[i + 2].isRefShape && hooks[i + 3].hasDispatch) {
if (!propsLookRight(fiber.memoizedProps)) {
hookShapeMatchesRejectedByPropCheck++;
break;
}
if (debug) {
const name = fiber.type && (fiber.type.displayName || fiber.type.name);
console.log('[ACN-GPT-HOOKS]', { success: true, componentName: name, depth, componentsChecked, bundleFingerprint: getChatGPTBundleFingerprintGPT() });
}
return {
turnIdDispatch: hooks[i].hook.queue.dispatch,
requestCounterRef: hooks[i + 1].value,
pendingScrollRef: hooks[i + 2].value,
forceRenderDispatch: hooks[i + 3].hook.queue.dispatch
};
}
}
}
fiber = fiber.return;
depth++;
}
if (debug) {
console.log('[ACN-GPT-HOOKS]', {
fail: 'no validated component found in ancestor chain (hook shape + prop shape must both match)',
reachedDepth: depth, componentsChecked, hookShapeMatchesRejectedByPropCheck,
bundleFingerprint: getChatGPTBundleFingerprintGPT()
});
}
return null;
}
function forceRenderChatGPTTurn(messageId) {
const hooks = locateChatGPTForceRenderHooks();
if (!hooks) return false;
hooks.requestCounterRef.current += 1;
const payload = { messageId, requestId: hooks.requestCounterRef.current, turnId: messageId };
hooks.pendingScrollRef.current = payload;
hooks.turnIdDispatch(messageId);
hooks.forceRenderDispatch(payload);
return true;
}
function clearChatGPTForceRenderedTurn() {
const hooks = locateChatGPTForceRenderHooks();
if (!hooks) return;
hooks.forceRenderDispatch(null);
}
let chatGptHarvestedOffsets = new Map();
let chatGptNavToken = 0;
let chatGptLastPassiveHarvestAt = 0;
function harvestChatGPTDocumentOffsetsTick() {
if (!IS_CHATGPT) return false;
const root = getConversationScrollRoot();
if (!root) return false;
const selector = CURRENT_SITE.virtualScroll && CURRENT_SITE.virtualScroll.messageContainer;
if (!selector) return false;
const scrollTop = getScrollTop(root);
let recorded = 0;
document.querySelectorAll(selector).forEach(el => {
if (!(el instanceof HTMLElement)) return;
const idEl = el.closest('[data-message-id]') || el.querySelector('[data-message-id]');
const messageId = idEl && idEl.getAttribute('data-message-id');
if (!messageId) return;
const rect = el.getBoundingClientRect();
chatGptHarvestedOffsets.set(messageId, scrollTop + rect.top);
recorded++;
});
return recorded > 0;
}
function scrollToOffsetWithCorrectionGPT(root, targetOffset, maxDurationMs = 900) {
return new Promise(resolve => {
const desiredCenterOffset = window.innerHeight * 0.5;
const calc = () => targetOffset - desiredCenterOffset;
setScrollTop(root, calc(), 'auto');
const startTime = performance.now();
let stableFrames = 0;
const step = () => {
if (performance.now() - startTime > maxDurationMs) { resolve(); return; }
const current = getScrollTop(root);
const desired = Math.max(0, calc());
if (Math.abs(current - desired) > 2) {
stableFrames = 0;
setScrollTop(root, desired, 'auto');
} else {
stableFrames++;
}
if (stableFrames >= 4) { resolve(); return; }
requestAnimationFrame(step);
};
requestAnimationFrame(step);
});
}
async function navigateViaForceRenderChatGPT(targetOrdinal, timeoutMs = 3000, myToken = null, skipCentering = false) {
if (!IS_CHATGPT || !isChatGPTApiReadyGPT()) return null;
if (myToken !== null && myToken !== chatGptNavToken) return null;
const apiCache = chatGptApiConversationCache;
const targetMsg = apiCache && Array.isArray(apiCache.userMessages) ? apiCache.userMessages[targetOrdinal] : null;
if (!targetMsg) return null;
const root = getConversationScrollRoot();
if (!root) return null;
const debug = typeof unsafeWindow !== 'undefined' && unsafeWindow.__acnChatGPTScrollDebug;
const startTime = performance.now();
const findMounted = () => {
const selector = CURRENT_SITE.virtualScroll && CURRENT_SITE.virtualScroll.messageContainer;
if (!selector) return null;
const el = Array.from(document.querySelectorAll(selector)).find(candidate => {
const idEl = candidate.closest('[data-message-id]') || candidate.querySelector('[data-message-id]');
return idEl && idEl.getAttribute('data-message-id') === targetMsg.id;
});
return el instanceof HTMLElement ? el : null;
};
const already = findMounted();
if (already) {
if (debug) console.log('[ACN-GPT-FORCE-RENDER]', { targetOrdinal, messageId: targetMsg.id, result: 'already mounted' });
if (!skipCentering) await scrollElementIntoViewWithCorrectionForOffsetGPT(root, already, 3000, targetMsg.id);
return already;
}
if (myToken !== null && myToken !== chatGptNavToken) return null;
if (!forceRenderChatGPTTurn(targetMsg.id)) {
if (debug) console.log('[ACN-GPT-FORCE-RENDER]', { targetOrdinal, messageId: targetMsg.id, result: 'could not locate hooks' });
return null;
}
if (debug) console.log('[ACN-GPT-FORCE-RENDER]', { targetOrdinal, messageId: targetMsg.id, result: 'dispatched, waiting for mount' });
return new Promise(resolve => {
let settled = false;
const finish = async (el) => {
if (settled) return;
settled = true;
observer.disconnect();
clearTimeout(timeoutId);
if (debug) {
console.log('[ACN-GPT-FORCE-RENDER]', {
targetOrdinal, messageId: targetMsg.id,
result: el ? 'success' : 'timeout, not mounted',
elapsedMs: Math.round(performance.now() - startTime)
});
}
if (el && !skipCentering) await scrollElementIntoViewWithCorrectionForOffsetGPT(root, el, 3000, targetMsg.id);
if (myToken === null || myToken === chatGptNavToken) clearChatGPTForceRenderedTurn();
resolve(el || null);
};
const check = () => {
if (settled) return;
const found = findMounted();
if (found) finish(found);
};
const observer = new MutationObserver(check);
observer.observe(root, { childList: true, subtree: true });
const timeoutId = setTimeout(() => finish(null), timeoutMs);
check();
});
}
function installChatGPTPaginationFetchWatcher() {
const target = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
if (target.fetch && target.fetch.__acnPaginationWrapped) return;
if (!chatGptPaginationFetchListeners) chatGptPaginationFetchListeners = [];
chatGptOriginalFetchForPagination = target.fetch;
const origFetch = chatGptOriginalFetchForPagination;
const wrapped = async function (...args) {
let url = typeof args[0] === 'string' ? args[0] : (args[0] && args[0].url) || '';
const isPagination = /\/messages\?before=/.test(url);
if (isPagination && chatGptNumTurnsRewriteActive && /num_turns=\d+/.test(url)) {
const rewritten = url.replace(/num_turns=\d+/, 'num_turns=100');
if (typeof args[0] === 'string') {
args[0] = rewritten;
} else if (args[0] && args[0].url) {
args[0] = new Request(rewritten, args[0]);
}
url = rewritten;
}
const response = await origFetch.apply(this, args);
if (isPagination && chatGptPaginationFetchListeners) {
const listeners = chatGptPaginationFetchListeners;
chatGptPaginationFetchListeners = [];
listeners.forEach(fn => fn());
}
return response;
};
wrapped.__acnPaginationWrapped = true;
target.fetch = wrapped;
}
function removeChatGPTPaginationFetchWatcher() {
if (!chatGptPaginationFetchListeners) return;
const target = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
target.fetch = chatGptOriginalFetchForPagination;
chatGptPaginationFetchListeners = null;
chatGptOriginalFetchForPagination = null;
}
function waitForChatGPTPaginationFetch(timeoutMs = 2500) {
installChatGPTPaginationFetchWatcher();
return new Promise(resolve => {
let done = false;
const finish = (realFetch) => { if (!done) { done = true; resolve(Boolean(realFetch)); } };
if (chatGptPaginationFetchListeners) chatGptPaginationFetchListeners.push(() => finish(true));
setTimeout(() => finish(false), timeoutMs);
});
}
async function loadOlderChatGPTMessagesViaSentinel(maxRounds = 40) {
const debug = typeof unsafeWindow !== 'undefined' && unsafeWindow.__acnChatGPTScrollDebug;
const root = getConversationScrollRoot();
let consecutiveMisses = 0;
chatGptNumTurnsRewriteActive = true;
try {
for (let round = 0; round < maxRounds; round++) {
const sentinel = document.querySelector('[data-testid="conversation-pagination-sentinel"]');
if (!sentinel) {
if (debug) console.log('[ACN-GPT-SENTINEL]', { round, result: 'no sentinel — reached the true beginning or pagination not active, doing final settle scroll' });
consecutiveMisses = 0;
if (root) {
root.scrollTop = 0;
await sleep(400);
}
applyChatGPTNativeTocHidingGPT();
return false;
}
const rootRectBeforeGPT = root.getBoundingClientRect();
const sentinelRectBeforeGPT = sentinel.getBoundingClientRect();
const sentinelAlreadyInViewGPT = sentinelRectBeforeGPT.top >= rootRectBeforeGPT.top && sentinelRectBeforeGPT.bottom <= rootRectBeforeGPT.bottom;
if (sentinelAlreadyInViewGPT) {
setScrollTop(root, getScrollTop(root) + 2000, 'auto');
await sleep(500);
}
const fetchPromise = waitForChatGPTPaginationFetch();
sentinel.scrollIntoView({ block: 'center', behavior: 'auto' });
const fired = await fetchPromise;
if (fired) {
consecutiveMisses = 0;
} else {
consecutiveMisses++;
}
if (debug) {
console.log('[ACN-GPT-SENTINEL]', { round, fired, consecutiveMisses });
}
if (consecutiveMisses >= 15) {
if (debug) console.log('[ACN-GPT-SENTINEL]', { round, result: 'stalled, aborting after 15 consecutive no-fetch rounds' });
consecutiveMisses = 0;
return null;
}
}
return true;
} finally {
chatGptNumTurnsRewriteActive = false;
}
}
function getChatGPTPaginationRoundsStorageKey() {
const convId = chatGptApiConversationCache && chatGptApiConversationCache.conversationId;
return convId ? `acn_gpt_pagination_rounds_${convId}` : null;
}
function readChatGPTPaginationRoundsMap() {
const key = getChatGPTPaginationRoundsStorageKey();
if (!key) return {};
try {
const raw = GM_getValue(key, null);
return raw ? JSON.parse(raw) : {};
} catch {
return {};
}
}
function recordChatGPTPaginationRounds(messageId, rounds) {
const key = getChatGPTPaginationRoundsStorageKey();
if (!key || !messageId || rounds <= 0) return;
const map = readChatGPTPaginationRoundsMap();
map[messageId] = rounds;
try {
GM_setValue(key, JSON.stringify(map));
} catch { /* storage full or unavailable; not critical */ }
}
function getChatGPTHideNativeTocSetting() {
try { return GM_getValue('acn_gpt_hide_native_toc', false); } catch { return false; }
}
function setChatGPTHideNativeTocSetting(value) {
try { GM_setValue('acn_gpt_hide_native_toc', Boolean(value)); } catch { /* storage unavailable; setting won't persist but still applies this session */ }
applyChatGPTNativeTocHidingGPT();
installChatGPTNativeTocHideObserver();
}
function applyChatGPTNativeTocHidingGPT() {
if (!IS_CHATGPT) return;
const group = extractTOCDOMGPT();
if (!group || !group.style) return;
group.style.display = getChatGPTHideNativeTocSetting() ? 'none' : '';
}
let chatGptNativeTocHideObserver = null;
function installChatGPTNativeTocHideObserver() {
if (!IS_CHATGPT || chatGptNativeTocHideObserver) return;
const observer = new MutationObserver((mutations) => {
if (!getChatGPTHideNativeTocSetting()) return;
for (const m of mutations) {
for (const node of m.addedNodes) {
if (!(node instanceof HTMLElement)) continue;
if (node.classList && node.classList.contains('inset-e-4') && node.classList.contains('z-20')) {
node.style.display = 'none';
}
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
chatGptNativeTocHideObserver = observer;
}
async function navigateWithPaginationChatGPT(targetOrdinal) {
if (!IS_CHATGPT || !isChatGPTApiReadyGPT()) return null;
const usePersisted = typeof unsafeWindow !== 'undefined' && unsafeWindow.__acnChatGPTUsePersistedRounds;
const myToken = ++chatGptNavToken;
let located = await navigateViaForceRenderChatGPT(targetOrdinal, 3000, myToken);
if (located) return located;
const apiCache = chatGptApiConversationCache;
const targetMsg = apiCache && Array.isArray(apiCache.userMessages) ? apiCache.userMessages[targetOrdinal] : null;
if (!targetMsg) return null;
const knownRounds = usePersisted ? readChatGPTPaginationRoundsMap()[targetMsg.id] : undefined;
let roundsSoFar = 0;
if (typeof knownRounds === 'number' && knownRounds > 0) {
if (myToken !== chatGptNavToken) return null;
const stillHasSentinel = await loadOlderChatGPTMessagesViaSentinel(knownRounds);
roundsSoFar = knownRounds;
if (myToken !== chatGptNavToken) return null;
located = await navigateViaForceRenderChatGPT(targetOrdinal, 1500, myToken);
if (located) {
if (usePersisted) recordChatGPTPaginationRounds(targetMsg.id, roundsSoFar);
return located;
}
if (!stillHasSentinel) return null;
}
for (let round = 0; round < 40; round++) {
if (myToken !== chatGptNavToken) return null;
const stillHasSentinel = await loadOlderChatGPTMessagesViaSentinel(1);
roundsSoFar++;
if (myToken !== chatGptNavToken) return null;
located = await navigateViaForceRenderChatGPT(targetOrdinal, 800, myToken);
if (located) {
if (usePersisted) recordChatGPTPaginationRounds(targetMsg.id, roundsSoFar);
return located;
}
if (!stillHasSentinel) return null;
}
return null;
}
async function navigateToHarvestedChatGPTMessageGPT(targetOrdinal) {
if (!IS_CHATGPT || !isChatGPTApiReadyGPT()) return null;
const apiCache = chatGptApiConversationCache;
const targetMsg = apiCache && Array.isArray(apiCache.userMessages) ? apiCache.userMessages[targetOrdinal] : null;
if (!targetMsg) return null;
return scrollToHarvestedMessageIdGPT(targetMsg.id);
}
async function scrollToHarvestedMessageIdGPT(messageId, timeoutMs = 4000) {
if (!IS_CHATGPT || !messageId) return null;
const root = getConversationScrollRoot();
if (!root) return null;
const selector = CURRENT_SITE.virtualScroll && CURRENT_SITE.virtualScroll.messageContainer;
if (!selector) return null;
const findMounted = () => {
const el = Array.from(document.querySelectorAll(selector)).find(candidate => {
const idEl = candidate.closest('[data-message-id]') || candidate.querySelector('[data-message-id]');
return idEl && idEl.getAttribute('data-message-id') === messageId;
});
return el instanceof HTMLElement ? el : null;
};
const already = findMounted();
if (already) return already;
const harvestedOffset = chatGptHarvestedOffsets.get(messageId);
if (typeof harvestedOffset !== 'number') return null;
return new Promise(resolve => {
let settled = false;
const finish = async (el) => {
if (settled) return;
settled = true;
observer.disconnect();
clearTimeout(timeoutId);
if (el) await scrollElementIntoViewWithCorrectionForOffsetGPT(root, el, 3000, messageId);
resolve(el || null);
};
const check = () => {
if (settled) return;
const found = findMounted();
if (found) finish(found);
};
const observer = new MutationObserver(check);
observer.observe(root, { childList: true, subtree: true });
const timeoutId = setTimeout(() => finish(null), timeoutMs);
scrollToOffsetWithCorrectionGPT(root, harvestedOffset).then(check);
});
}
function resolveGPTTextCenterTarget(element) {
if (!IS_CHATGPT || !(element instanceof HTMLElement)) return element;
const msgConfig = CURRENT_SITE.userMessage || {};
if (!msgConfig.container || !msgConfig.text) return element;
let userContainer = null;
try {
userContainer = element.matches(msgConfig.container) ? element : element.querySelector(msgConfig.container);
} catch (e) {
return element;
}
if (!userContainer) return element;
const textEl = userContainer.querySelector(msgConfig.text);
if (textEl instanceof HTMLElement && isElementVisibleForNav(textEl) && textEl.textContent && textEl.textContent.trim()) {
return textEl;
}
return element;
}
function scrollElementIntoViewWithCorrectionForOffsetGPT(root, element, maxDurationMs = 500, debugMessageId = null) {
const debug = typeof unsafeWindow !== 'undefined' && unsafeWindow.__acnChatGPTScrollDebug;
return new Promise(resolve => {
if (!element || !element.isConnected) {
if (debug) console.log('[ACN-GPT-CENTER]', { fail: 'element missing or not connected at start' });
resolve();
return;
}
const centerTarget = resolveGPTTextCenterTarget(element) || element;
const desiredCenter = () => root.getBoundingClientRect().top + (root.clientHeight || window.innerHeight) * 0.5;
const elementCenter = () => {
const rect = centerTarget.getBoundingClientRect();
return rect.top + rect.height / 2;
};
const calc = () => getScrollTop(root) + elementCenter() - desiredCenter();
if (debug) {
const rect = element.getBoundingClientRect();
const cs = window.getComputedStyle(element);
console.log('[ACN-GPT-CENTER] stage:start ' + JSON.stringify({
debugMessageId,
rootTag: root.tagName, rootId: root.id,
rootClientHeight: root.clientHeight, initialElementCenter: Math.round(elementCenter()),
initialDesiredCenter: Math.round(desiredCenter()), initialScrollTop: getScrollTop(root),
elRectTop: Math.round(rect.top), elRectHeight: Math.round(rect.height),
elOffsetHeight: element.offsetHeight, elOffsetWidth: element.offsetWidth,
elDisplay: cs.display, elVisibility: cs.visibility,
elTag: element.tagName, elClass: (element.className || '').toString().slice(0, 120),
elTestId: element.getAttribute && element.getAttribute('data-testid'),
elIsConnected: element.isConnected,
elTextPreview: (element.textContent || '').slice(0, 60),
centerTargetIsSameAsElement: centerTarget === element,
centerTargetTag: centerTarget.tagName,
centerTargetTextPreview: (centerTarget.textContent || '').slice(0, 60)
}));
}
setScrollTop(root, calc(), 'auto');
const startTime = performance.now();
let stableFrames = 0;
let iterations = 0;
const step = () => {
iterations++;
if (!element.isConnected) {
if (debug) console.log('[ACN-GPT-CENTER]', { fail: 'element disconnected mid-correction', iterations });
resolve();
return;
}
if (performance.now() - startTime > maxDurationMs) {
if (debug) {
console.log('[ACN-GPT-CENTER]', {
result: 'timed out', iterations, finalDrift: Math.round(elementCenter() - desiredCenter()),
finalElementCenter: Math.round(elementCenter()), finalDesiredCenter: Math.round(desiredCenter())
});
}
resolve();
return;
}
const drift = elementCenter() - desiredCenter();
if (debug) {
console.log('[ACN-GPT-CENTER] trace ' + JSON.stringify({
iterations, t: Math.round(performance.now() - startTime),
scrollTop: getScrollTop(root), elementCenter: Math.round(elementCenter()),
drift: Math.round(drift)
}));
}
if (Math.abs(drift) > 1) {
const beforeScrollTop = getScrollTop(root);
const requested = calc();
setScrollTop(root, requested, 'auto');
const afterScrollTop = getScrollTop(root);
const wasClamped = Math.abs(requested - afterScrollTop) > 1;
const noRealMovement = Math.abs(afterScrollTop - beforeScrollTop) < 1;
stableFrames = (wasClamped && noRealMovement) ? stableFrames + 1 : 0;
} else {
stableFrames++;
}
if (stableFrames >= 4) {
if (debug) console.log('[ACN-GPT-CENTER]', { result: 'settled', iterations, finalDrift: Math.round(drift) });
resolve();
return;
}
requestAnimationFrame(step);
};
requestAnimationFrame(step);
});
}
function findMountedElementByMessageIdGPT(messageId) {
if (!IS_CHATGPT || !messageId) return null;
const selector = CURRENT_SITE.virtualScroll && CURRENT_SITE.virtualScroll.messageContainer;
if (!selector) return null;
const el = Array.from(document.querySelectorAll(selector)).find(candidate => {
const idEl = candidate.closest('[data-message-id]') || candidate.querySelector('[data-message-id]');
return idEl && idEl.getAttribute('data-message-id') === messageId;
});
return el instanceof HTMLElement ? el : null;
}
async function forceRenderAndWaitForMessageIdGPT(messageId, timeoutMs = 1200) {
if (!IS_CHATGPT || !messageId) return null;
const root = getConversationScrollRoot();
if (!root) return null;
const selector = CURRENT_SITE.virtualScroll && CURRENT_SITE.virtualScroll.messageContainer;
if (!selector) return null;
const debug = typeof unsafeWindow !== 'undefined' && unsafeWindow.__acnChatGPTScrollDebug;
const startTime = performance.now();
const findMounted = () => {
const el = Array.from(document.querySelectorAll(selector)).find(candidate => {
const idEl = candidate.closest('[data-message-id]') || candidate.querySelector('[data-message-id]');
return idEl && idEl.getAttribute('data-message-id') === messageId;
});
return el instanceof HTMLElement ? el : null;
};
const already = findMounted();
if (already) {
if (debug) console.log('[ACN-GPT-FORCE-RENDER-ASSISTANT]', { messageId, result: 'already mounted' });
return already;
}
if (!forceRenderChatGPTTurn(messageId)) {
if (debug) console.log('[ACN-GPT-FORCE-RENDER-ASSISTANT]', { messageId, result: 'could not locate hooks' });
return null;
}
if (debug) console.log('[ACN-GPT-FORCE-RENDER-ASSISTANT]', { messageId, result: 'dispatched, waiting for mount' });
return new Promise(resolve => {
let settled = false;
const finish = (el) => {
if (settled) return;
settled = true;
observer.disconnect();
clearTimeout(timeoutId);
if (debug) {
console.log('[ACN-GPT-FORCE-RENDER-ASSISTANT]', {
messageId,
result: el ? 'success' : 'timeout, not mounted',
elapsedMs: Math.round(performance.now() - startTime)
});
}
resolve(el || null);
};
const check = () => {
if (settled) return;
const found = findMounted();
if (found) finish(found);
};
const observer = new MutationObserver(check);
observer.observe(root, { childList: true, subtree: true });
const timeoutId = setTimeout(() => finish(null), timeoutMs);
check();
});
}
function getChatGPTBundleFingerprintGPT() {
const scripts = Array.from(document.querySelectorAll('script[src*="conversation-small-"]'));
const match = scripts[0] && scripts[0].src.match(/conversation-small-([a-zA-Z0-9]+)\.js/);
return match ? match[1] : null;
}
function verifyChatGPTElementMatchesOrdinalGPT(el, ordinal) {
if (!el) return false;
const idEl = el.closest('[data-message-id]') || el.querySelector('[data-message-id]');
const messageId = idEl && idEl.getAttribute('data-message-id');
if (!messageId) return false;
return resolveChatGPTApiUserOrdinalForAnyMessageId(messageId) === ordinal;
}
// ============================================================================
// 07.4 Active-message resolution
// ============================================================================
function findActiveOrdinalFromVisibleDOMGPT() {
if (!isChatGPTApiReadyGPT()) return -1;
const selector = CURRENT_SITE.virtualScroll && CURRENT_SITE.virtualScroll.messageContainer;
if (!selector) return -1;
const elements = Array.from(document.querySelectorAll(selector)).filter(el => el instanceof HTMLElement);
if (!elements.length) return -1;
const root = getConversationScrollRoot();
const rootRect = root && root.getBoundingClientRect
? root.getBoundingClientRect()
: { top: 0, bottom: window.innerHeight, height: window.innerHeight };
const top = Math.max(0, rootRect.top || 0);
const bottom = Math.min(window.innerHeight, rootRect.bottom || window.innerHeight);
const focusY = top + Math.max(1, bottom - top) * 0.5;
const resolved = [];
elements.forEach(el => {
const idEl = el.closest('[data-message-id]') || el.querySelector('[data-message-id]');
const messageId = idEl && idEl.getAttribute('data-message-id');
if (!messageId) return;
const ordinal = resolveChatGPTApiUserOrdinalForAnyMessageId(messageId);
if (ordinal < 0) return;
resolved.push({ ordinal, rect: el.getBoundingClientRect() });
});
if (!resolved.length) return -1;
const isOnScreen = rect => rect.bottom > top && rect.top < bottom;
const containsFocus = rect => rect.top <= focusY && rect.bottom >= focusY;
if (root && getScrollTop(root) <= 1) {
const onScreenSorted = resolved.filter(item => isOnScreen(item.rect)).sort((a, b) => a.ordinal - b.ordinal);
if (onScreenSorted.length) {
const topmost = onScreenSorted[0];
chatGptStickyActiveOrdinal = topmost.ordinal;
if (typeof unsafeWindow !== 'undefined' && unsafeWindow.__acnChatGPTActiveDebug) {
console.log('[ACN-GPT-ACTIVE]', { tier: 'topBoundary', ordinal: topmost.ordinal });
}
return topmost.ordinal;
}
}
if (chatGptLastKnownPromptCount > 0) {
const lastOrdinal = chatGptLastKnownPromptCount - 1;
const lastOrdinalOnScreen = resolved.some(item => item.ordinal === lastOrdinal && isOnScreen(item.rect));
const now = Date.now();
if (lastOrdinalOnScreen) chatGptLastNearBottomAt = now;
const recentlyOnScreen = (now - chatGptLastNearBottomAt) < 900;
if (lastOrdinalOnScreen || recentlyOnScreen) {
chatGptStickyActiveOrdinal = lastOrdinal;
if (typeof unsafeWindow !== 'undefined' && unsafeWindow.__acnChatGPTActiveDebug) {
console.log('[ACN-GPT-ACTIVE]', { tier: 'nearBottom', ordinal: lastOrdinal, lastOrdinalOnScreen, bridged: !lastOrdinalOnScreen });
}
return lastOrdinal;
}
}
const strongMatches = resolved.filter(item => containsFocus(item.rect));
if (strongMatches.length) {
const best = strongMatches.reduce((a, b) => ((b.rect.bottom - b.rect.top) < (a.rect.bottom - a.rect.top) ? b : a));
chatGptStickyActiveOrdinal = best.ordinal;
if (typeof unsafeWindow !== 'undefined' && unsafeWindow.__acnChatGPTActiveDebug) {
console.log('[ACN-GPT-ACTIVE]', { tier: 'strongMatch', ordinal: best.ordinal, mountedCount: elements.length });
}
return best.ordinal;
}
if (chatGptStickyActiveOrdinal >= 0) {
const stillOnScreen = resolved.some(item => item.ordinal === chatGptStickyActiveOrdinal && isOnScreen(item.rect));
if (stillOnScreen) {
if (typeof unsafeWindow !== 'undefined' && unsafeWindow.__acnChatGPTActiveDebug) {
console.log('[ACN-GPT-ACTIVE]', { tier: 'sticky', ordinal: chatGptStickyActiveOrdinal, mountedCount: elements.length });
}
return chatGptStickyActiveOrdinal;
}
}
const onScreenCandidates = resolved.filter(item => isOnScreen(item.rect));
const pool = onScreenCandidates.length ? onScreenCandidates : resolved;
let best = null;
let bestDistance = Infinity;
pool.forEach(item => {
const targetY = Math.min(Math.max(focusY, item.rect.top), item.rect.bottom);
const distance = Math.abs(targetY - focusY);
if (distance < bestDistance) {
bestDistance = distance;
best = item;
}
});
if (!best) return -1;
chatGptStickyActiveOrdinal = best.ordinal;
if (typeof unsafeWindow !== 'undefined' && unsafeWindow.__acnChatGPTActiveDebug) {
console.log('[ACN-GPT-ACTIVE]', {
tier: 'nearest',
focusY: Math.round(focusY),
chosenTop: Math.round(best.rect.top),
chosenBottom: Math.round(best.rect.bottom),
usedOnScreenPool: onScreenCandidates.length > 0,
ordinal: best.ordinal,
mountedCount: elements.length
});
}
return best.ordinal;
}
// ============================================================================
// 07.5 TOC discovery & indexing
// ============================================================================
function normalizeTOCTextGPT(text) {
return String(text || '').replace(/\s+/g, ' ').trim();
}
function isTOCNumberLabelGPT(text) {
return /^Prompts?\s+\d+$/i.test(normalizeTOCTextGPT(text));
}
function extractTOCIndexDOMGPT(button, fallbackIndex) {
if (button && button.dataset) {
const dataIndex = Number.parseInt(button.dataset.tocItemIndex, 10);
if (Number.isFinite(dataIndex) && dataIndex >= 0) return dataIndex;
}
const aria = normalizeTOCTextGPT(button && button.getAttribute ? button.getAttribute('aria-label') : '');
const match = /^Prompts?\s+(\d+)$/i.exec(aria);
if (match && match[1]) {
const n = Number.parseInt(match[1], 10);
if (Number.isFinite(n)) return Math.max(0, n - 1);
}
return fallbackIndex;
}
function isTOCButtonGPT(button) {
if (!isTOCEnabledGPT()) return false;
if (!(button instanceof HTMLElement) || button.tagName.toLowerCase() !== 'button') return false;
const rawIndex = button.getAttribute('data-toc-item-index');
return rawIndex !== null && Number.isFinite(Number.parseInt(rawIndex, 10));
}
function extractTOCDOMGPT() {
if (!isTOCEnabledGPT()) return null;
const selector = CURRENT_SITE.nativePromptToc.button || 'button[data-toc-item-index]';
const allButtons = Array.from(document.querySelectorAll(selector)).filter(isTOCButtonGPT);
if (!allButtons.length) return null;
const groups = new Map();
allButtons.forEach(button => {
const parent = button.parentElement;
if (!parent) return;
if (!groups.has(parent)) groups.set(parent, []);
groups.get(parent).push(button);
});
const ranked = Array.from(groups.entries()).map(([group, buttons]) => {
const uniqueCount = new Set(buttons.map((button, i) => extractTOCIndexDOMGPT(button, i))).size;
const rect = group.getBoundingClientRect();
const visibleBonus = rect.width > 0 && rect.height > 0 ? 10000 : 0;
return { group, score: visibleBonus + uniqueCount * 100 + buttons.length };
}).sort((a, b) => b.score - a.score);
return ranked[0] ? ranked[0].group : null;
}
function extractTOCButtonsDOMGPT() {
if (!isTOCEnabledGPT()) return [];
resetTOCStateIfNeededGPT();
const group = extractTOCDOMGPT();
if (!group) return [];
const selector = CURRENT_SITE.nativePromptToc.button || 'button[data-toc-item-index]';
const entries = Array.from(group.querySelectorAll(selector))
.filter(isTOCButtonGPT)
.map((button, fallbackIndex) => ({
button,
index: extractTOCIndexDOMGPT(button, fallbackIndex),
fallbackIndex
}))
.sort((a, b) => (a.index - b.index) || (a.fallbackIndex - b.fallbackIndex));
const seen = new Set();
const buttons = entries.filter(entry => {
if (seen.has(entry.index)) return false;
seen.add(entry.index);
return true;
}).map(entry => entry.button);
cacheTOCTextGPT(buttons);
observeTOCGPT(group);
return buttons;
}
function getTOCSignatureGPT(buttons) {
return buttons.map((button, fallbackIndex) => extractTOCIndexDOMGPT(button, fallbackIndex)).join(',');
}
function extractActiveTOCIndexDOMGPT(buttons = null) {
const domOrdinal = findActiveOrdinalFromVisibleDOMGPT();
if (domOrdinal >= 0) return domOrdinal;
const list = buttons || extractTOCButtonsDOMGPT();
const activeButton = list.find(button => button.hasAttribute('data-toc-active'));
if (!activeButton) return -1;
return extractTOCIndexDOMGPT(activeButton, list.indexOf(activeButton));
}
function syncActiveTOCIndexGPT(buttons = null) {
if (chatGptPendingConversationUrl) return false;
const list = buttons || extractTOCButtonsDOMGPT();
const index = extractActiveTOCIndexDOMGPT(list);
if (index < 0 || index >= list.length) return false;
const total = cachedPrompts.length || list.length;
const next = index + 1;
if (next < 1 || next > total || activeMessageIndex === next) return false;
applyUniversalActiveMessage(next, { scrollBar: true });
return true;
}
// ============================================================================
// 07.6 TOC text, cache & observer
// ============================================================================
function extractTOCTextDOMGPT(buttons) {
const labels = new Map();
buttons.forEach((button, fallbackIndex) => {
const index = extractTOCIndexDOMGPT(button, fallbackIndex);
const label = normalizeTOCTextGPT(button.getAttribute('aria-label'));
if (label && !isTOCNumberLabelGPT(label)) labels.set(index, label);
});
return labels;
}
function cacheTOCTextGPT(buttons) {
const labels = extractTOCTextDOMGPT(buttons);
labels.forEach((label, index) => chatGptTocTextCache.set(index, label));
return labels;
}
function getTOCPreviewGPT(index) {
const detail = chatGptTocDetailCache.get(index);
if (detail && detail.text) return detail.text;
const cached = chatGptTocTextCache.get(index);
return cached || '';
}
function scheduleTOCRefreshGPT(force = true) {
if (chatGptTocRefreshTimer) chatGptTocRefreshTimer.clear();
chatGptTocRefreshTimer = safeSetTimeout(() => {
chatGptTocRefreshTimer = null;
if (typeof updateMessageList === 'function') updateMessageList(force);
}, 80);
}
function observeTOCGPT(group) {
if (!group || chatGptTocObservedGroup === group) return;
if (chatGptTocObserver) chatGptTocObserver.disconnect();
chatGptTocObservedGroup = group;
chatGptTocObserver = new MutationObserver(mutations => {
let labelsChanged = false;
let activeChanged = false;
let structureChanged = false;
mutations.forEach(mutation => {
if (mutation.type === 'childList') structureChanged = true;
if (mutation.type === 'attributes' && mutation.attributeName === 'aria-label') labelsChanged = true;
if (mutation.type === 'attributes' && mutation.attributeName === 'data-toc-active') activeChanged = true;
if (mutation.type === 'attributes' && mutation.attributeName === 'data-toc-item-index') structureChanged = true;
});
const buttons = extractTOCButtonsDOMGPT();
if (structureChanged) {
chatGptTocHoverAttemptedSignature = '';
chatGptTocHoverSignature = '';
closeTOCGPT(group);
}
if (labelsChanged || structureChanged) cacheTOCTextGPT(buttons);
const indexChanged = activeChanged ? syncActiveTOCIndexGPT(buttons) : false;
if (chatGptTocPostActionPending && (labelsChanged || structureChanged || activeChanged)) {
schedulePostActionHarvestGPT(70);
}
if (labelsChanged || structureChanged || indexChanged) scheduleTOCRefreshGPT(true);
});
chatGptTocObserver.observe(group, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['aria-label', 'data-toc-active', 'data-toc-item-index']
});
}
function dispatchTOCHoverGPT(group) {
if (!(group instanceof HTMLElement)) return;
const rect = group.getBoundingClientRect();
const view = group.ownerDocument.defaultView || window;
const eventInit = {
bubbles: true,
cancelable: true,
composed: true,
view,
clientX: Math.round(rect.left + rect.width / 2),
clientY: Math.round(rect.top + rect.height / 2)
};
if (view.PointerEvent) {
group.dispatchEvent(new view.PointerEvent('pointerover', { ...eventInit, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
group.dispatchEvent(new view.PointerEvent('pointerenter', { ...eventInit, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
}
group.dispatchEvent(new view.MouseEvent('mouseover', eventInit));
group.dispatchEvent(new view.MouseEvent('mouseenter', eventInit));
group.dispatchEvent(new view.MouseEvent('mousemove', eventInit));
}
function dispatchTOCLeaveGPT(group) {
if (!(group instanceof HTMLElement)) return;
const rect = group.getBoundingClientRect();
const view = group.ownerDocument.defaultView || window;
const eventInit = {
bubbles: true,
cancelable: true,
composed: true,
view,
clientX: Math.max(0, Math.round(rect.left - 8)),
clientY: Math.max(0, Math.round(rect.top - 8))
};
if (view.PointerEvent) {
group.dispatchEvent(new view.PointerEvent('pointerout', { ...eventInit, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
group.dispatchEvent(new view.PointerEvent('pointerleave', { ...eventInit, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
}
group.dispatchEvent(new view.MouseEvent('mouseout', eventInit));
group.dispatchEvent(new view.MouseEvent('mouseleave', eventInit));
}
function closeTOCGPT(extraGroup = null) {
if (!isTOCEnabledGPT()) return;
const groups = new Set();
if (extraGroup instanceof HTMLElement) groups.add(extraGroup);
if (chatGptTocSyntheticHoverGroup instanceof HTMLElement) groups.add(chatGptTocSyntheticHoverGroup);
const currentGroup = extractTOCDOMGPT();
if (currentGroup instanceof HTMLElement) groups.add(currentGroup);
groups.forEach(group => dispatchTOCLeaveGPT(group));
chatGptTocSyntheticHoverGroup = null;
}
function cancelPostActionTimersGPT() {
chatGptTocPostActionTimers.forEach(timer => timer && timer.clear && timer.clear());
chatGptTocPostActionTimers = [];
}
function getTOCMaxIndexGPT(buttons = null) {
const list = buttons || extractTOCButtonsDOMGPT();
let maxIndex = -1;
list.forEach((button, fallbackIndex) => {
maxIndex = Math.max(maxIndex, extractTOCIndexDOMGPT(button, fallbackIndex));
});
return maxIndex;
}
function extractMessageIndexDOMGPT(element, buttons = null) {
if (!(element instanceof Element)) return -1;
const list = buttons || extractTOCButtonsDOMGPT();
const availableIndices = list.map((button, fallbackIndex) => extractTOCIndexDOMGPT(button, fallbackIndex));
const roots = [];
const userRoot = element.closest('user-query, div[data-message-author-role="user"], [data-turn="user"], section[data-turn="user"], [data-turn-id-container]');
if (userRoot) roots.push(userRoot);
roots.push(element);
for (const root of roots) {
const badge = root.querySelector && root.querySelector('.acn-message-inline-badge');
if (!badge) continue;
const number = Number.parseInt(String(badge.textContent || '').trim(), 10);
const index = Number.isFinite(number) ? number - 1 : -1;
if (index >= 0 && (!availableIndices.length || availableIndices.includes(index))) return index;
}
for (const root of roots) {
const nodeId = extractTurnIdDOMGPT(root);
for (const [index, detail] of chatGptTocDetailCache.entries()) {
if (!detail || !detail.element) continue;
const sameElement = detail.element === root || detail.element.contains(root) || root.contains(detail.element);
const detailNodeId = extractTurnIdDOMGPT(detail.element);
const sameNodeId = Boolean(nodeId && detailNodeId && nodeId === detailNodeId);
if ((sameElement || sameNodeId) && (!availableIndices.length || availableIndices.includes(index))) return index;
}
}
const inputAndSend = CURRENT_SITE.inputAndSend || {};
const editInputSelector = inputAndSend.editInput || 'textarea[aria-label="Edit message"]';
const editTextarea = element.matches(editInputSelector)
? element
: element.querySelector && element.querySelector(editInputSelector);
const editText = normalizeTOCTextGPT(editTextarea ? editTextarea.value : '');
if (editText) {
const candidates = availableIndices.filter(index => {
const known = normalizeTOCTextGPT(getTOCPreviewGPT(index));
return known && isLikelySameMessageTextGPT(editText, known);
});
if (candidates.length === 1) return candidates[0];
}
return extractActiveTOCIndexDOMGPT(list);
}
function extractEditMessageRootDOMGPT(sendButton) {
const inputAndSend = CURRENT_SITE.inputAndSend || {};
const editInputSelector = inputAndSend.editInput || 'textarea[aria-label="Edit message"]';
let node = sendButton instanceof Element ? sendButton : null;
for (let depth = 0; node && depth < 10; depth += 1, node = node.parentElement) {
if (node.querySelector && node.querySelector(editInputSelector)) return node;
}
return null;
}
function schedulePostActionHarvestGPT(delay = 260) {
const pending = chatGptTocPostActionPending;
if (!pending) return;
const sequence = pending.sequence;
const timer = safeSetTimeout(() => {
chatGptTocPostActionTimers = chatGptTocPostActionTimers.filter(item => item !== timer);
runPostActionHarvestGPT(sequence);
}, delay);
chatGptTocPostActionTimers.push(timer);
}
function markPostActionGPT(kind, targetIndex = -1, expectedText = '') {
if (!IS_CHATGPT) return;
const buttons = extractTOCButtonsDOMGPT();
const sequence = ++chatGptTocPostActionSequence;
const baselineSignature = getTOCSignatureGPT(buttons);
const baselineMaxIndex = getTOCMaxIndexGPT(buttons);
if (kind === TOC_OPEN_REASON_GPT.ADD_NEW_MESSAGE && targetIndex < 0) targetIndex = baselineMaxIndex + 1;
const baselineDetail = targetIndex >= 0 ? chatGptTocDetailCache.get(targetIndex) : null;
const baselineVersion = getVersionSignatureGPT(baselineDetail && baselineDetail.versionInfo);
cancelPostActionTimersGPT();
chatGptTocPostActionPending = {
sequence,
kind,
targetIndex,
baselineSignature,
baselineMaxIndex,
baselineVersion,
expectedText: normalizeTOCTextGPT(expectedText),
startedAt: Date.now(),
harvesting: false
};
chatGptTocHoverAttemptedSignature = '';
chatGptTocHoverSignature = '';
[300, 650, 1200, 1800].forEach(delay => schedulePostActionHarvestGPT(delay));
}
async function runPostActionHarvestGPT(sequence) {
const pending = chatGptTocPostActionPending;
if (!pending || pending.sequence !== sequence || pending.harvesting) return false;
const buttons = extractTOCButtonsDOMGPT();
if (!buttons.length) return false;
const elapsed = Date.now() - pending.startedAt;
const currentSignature = getTOCSignatureGPT(buttons);
const currentMaxIndex = getTOCMaxIndexGPT(buttons);
if (pending.kind === TOC_OPEN_REASON_GPT.ADD_NEW_MESSAGE
&& currentMaxIndex <= pending.baselineMaxIndex
&& currentSignature === pending.baselineSignature
&& elapsed < 1000) {
return false;
}
if (pending.kind === TOC_OPEN_REASON_GPT.EDIT_MESSAGE) {
if (elapsed < 240) return false;
if (pending.expectedText && elapsed < 1500) {
const visibleNow = extractVisibleMessagesDOMGPT();
const editedTurnMounted = visibleNow.some(prompt =>
isLikelySameMessageTextGPT(prompt.text, pending.expectedText)
);
if (!editedTurnMounted) return false;
}
}
pending.harvesting = true;
const success = await openTOCGPT(pending.kind, { buttons, force: true, targetIndex: pending.targetIndex });
if (!chatGptTocPostActionPending || chatGptTocPostActionPending.sequence !== sequence) return success;
const currentButtons = extractTOCButtonsDOMGPT();
const visiblePrompts = extractVisibleMessagesDOMGPT();
const targetIndex = pending.targetIndex >= 0
? pending.targetIndex
: extractActiveTOCIndexDOMGPT(currentButtons);
if (pending.kind === TOC_OPEN_REASON_GPT.EDIT_MESSAGE) {
updateAdditionalDataGPT(ADDITIONAL_DATA_REASON_GPT.EDIT_MESSAGE, {
targetIndex,
buttons: currentButtons,
realPrompts: visiblePrompts
});
const livePrompt = extractMessageDOMByIndexGPT(visiblePrompts, currentButtons, targetIndex);
const liveText = normalizeTOCTextGPT(livePrompt && livePrompt.text);
const textReady = Boolean(livePrompt && (!pending.expectedText || isLikelySameMessageTextGPT(liveText, pending.expectedText)));
const freshVersion = livePrompt ? refreshVersionControlDataGPT(livePrompt) : null;
const versionReady = Boolean(freshVersion);
if ((!textReady || !versionReady) && elapsed < 2200) {
pending.harvesting = false;
schedulePostActionHarvestGPT(220);
return false;
}
if (livePrompt && textReady) {
refreshMessageGPT([livePrompt], currentButtons, targetIndex, true);
} else {
refreshMessageGPT(visiblePrompts, currentButtons, targetIndex, true);
}
} else {
updateAdditionalDataGPT(ADDITIONAL_DATA_REASON_GPT.ADD_NEW_MESSAGE, {
targetIndex,
buttons: currentButtons,
realPrompts: visiblePrompts
});
refreshMessageGPT(visiblePrompts, currentButtons, targetIndex, true);
}
cacheTOCTextGPT(currentButtons);
chatGptTocPostActionPending = null;
cancelPostActionTimersGPT();
scheduleTOCRefreshGPT(true);
return success;
}
// ============================================================================
// 07.7 TOC open/close lifecycle
// ============================================================================
function handleAddNewMessageGPT(expectedText = '') {
markPostActionGPT(TOC_OPEN_REASON_GPT.ADD_NEW_MESSAGE, -1, expectedText);
}
function handleEditMessageGPT(targetIndex, expectedText = '') {
markPostActionGPT(TOC_OPEN_REASON_GPT.EDIT_MESSAGE, targetIndex, expectedText);
}
function isComposerEnterGPT(event) {
if (!IS_CHATGPT || event.key !== 'Enter' || event.shiftKey || event.isComposing) return false;
if (event.defaultPrevented) return false;
const target = event.target;
if (!(target instanceof HTMLElement)) return false;
const inputAndSend = CURRENT_SITE.inputAndSend || {};
const composerInputSelector = inputAndSend.composerInput || '#prompt-textarea';
return Boolean(target.matches(composerInputSelector) || target.closest(composerInputSelector));
}
function handleComposerKeydownGPT(event) {
if (!isComposerEnterGPT(event)) return;
const target = event.target;
const expectedText = target instanceof HTMLTextAreaElement
? target.value
: (target instanceof HTMLElement ? target.innerText || target.textContent || '' : '');
handleAddNewMessageGPT(expectedText);
}
function handleSendClickGPT(event) {
if (!IS_CHATGPT) return;
const target = event.target;
if (!(target instanceof Element)) return;
const button = target.closest('button');
if (!button) return;
const inputAndSend = CURRENT_SITE.inputAndSend || {};
const composerInputSelector = inputAndSend.composerInput || '#prompt-textarea';
const composerSendSelector = inputAndSend.composerSendButton || 'button[data-testid="send-button"]';
const editInputSelector = inputAndSend.editInput || 'textarea[aria-label="Edit message"]';
const editSendSelector = inputAndSend.editSendButton || 'button.btn-primary';
if (button.matches(composerSendSelector)) {
const composer = document.querySelector(composerInputSelector);
const expectedText = composer instanceof HTMLTextAreaElement
? composer.value
: (composer instanceof HTMLElement ? composer.innerText || composer.textContent || '' : '');
handleAddNewMessageGPT(expectedText);
return;
}
if (!button.matches(editSendSelector)) return;
const editRoot = extractEditMessageRootDOMGPT(button);
if (!editRoot) return;
const editIndex = extractMessageIndexDOMGPT(editRoot);
const editTextarea = editRoot.querySelector(editInputSelector);
const expectedText = editTextarea instanceof HTMLTextAreaElement ? editTextarea.value : '';
handleEditMessageGPT(editIndex, expectedText);
}
function openTOCGPT(reason = TOC_OPEN_REASON_GPT.FIRST_OPEN, options = {}) {
const buttons = Array.isArray(options.buttons) ? options.buttons : extractTOCButtonsDOMGPT();
if (!buttons.length) return Promise.resolve(false);
const force = Boolean(options.force || reason !== TOC_OPEN_REASON_GPT.FIRST_OPEN);
const missing = buttons.some((button, fallbackIndex) => {
const index = extractTOCIndexDOMGPT(button, fallbackIndex);
return !chatGptTocTextCache.has(index);
});
if (!force && !missing) return Promise.resolve(true);
const group = buttons[0].parentElement;
if (!group) return Promise.resolve(false);
const signature = getTOCSignatureGPT(buttons);
if (!force && chatGptTocHoverAttemptedSignature === signature && !chatGptTocHoverPromise) return Promise.resolve(false);
if (chatGptTocHoverPromise && chatGptTocHoverSignature === signature) return chatGptTocHoverPromise;
chatGptTocHoverSignature = signature;
chatGptTocHoverAttemptedSignature = signature;
chatGptTocHoverPromise = new Promise(resolve => {
const wasPhysicallyHovered = group.matches(':hover');
chatGptTocSyntheticHoverGroup = group;
const startedAt = Date.now();
let sawLabelMutation = false;
let done = false;
let timer = null;
let settleTimer = null;
let closeTimer = null;
const closeSyntheticHover = () => {
if (wasPhysicallyHovered) return;
closeTOCGPT(group);
};
const finish = success => {
if (done) return;
done = true;
observer.disconnect();
if (timer) window.clearTimeout(timer);
if (settleTimer) window.clearTimeout(settleTimer);
if (closeTimer) window.clearTimeout(closeTimer);
cacheTOCTextGPT(buttons.filter(button => button.isConnected));
closeSyntheticHover();
chatGptTocHoverPromise = null;
if (success) scheduleTOCRefreshGPT(true);
resolve(success);
};
const check = () => {
const connectedButtons = buttons.filter(button => button.isConnected);
if (connectedButtons.length !== buttons.length) return;
const labels = extractTOCTextDOMGPT(connectedButtons);
if (labels.size !== connectedButtons.length) return;
if (!force || sawLabelMutation || Date.now() - startedAt >= 110) finish(true);
};
const observer = new MutationObserver(mutations => {
if (mutations.some(mutation => mutation.type === 'attributes' && mutation.attributeName === 'aria-label')) {
sawLabelMutation = true;
}
check();
});
observer.observe(group, { subtree: true, attributes: true, attributeFilter: ['aria-label'] });
dispatchTOCHoverGPT(group);
settleTimer = window.setTimeout(check, force ? 120 : 20);
closeTimer = window.setTimeout(() => {
closeTimer = null;
closeSyntheticHover();
}, force ? 180 : 120);
if (!force) check();
timer = window.setTimeout(() => finish(false), force ? 1000 : 800);
});
return chatGptTocHoverPromise;
}
function cleanupTOCBadgesGPT() {
if (!isTOCEnabledGPT()) return;
extractTOCButtonsDOMGPT().forEach(button => {
button.querySelectorAll('.acn-message-inline-badge').forEach(badge => badge.remove());
});
}
function hasUsableTOCGPT() {
return isTOCEnabledGPT() && extractTOCButtonsDOMGPT().length > 0;
}
// ============================================================================
// 07.8 Message detail & version control
// ============================================================================
function extractBestVisibleMessageDOMGPT(prompts) {
if (!prompts || !prompts.length) return null;
const root = getConversationScrollRoot();
const rootRect = root && root.getBoundingClientRect
? root.getBoundingClientRect()
: { top: 0, height: window.innerHeight };
const targetY = rootRect.top + Math.min(140, Math.max(40, rootRect.height * 0.2));
return prompts.map(prompt => {
const rect = prompt.element.getBoundingClientRect();
return { prompt, score: Math.abs(rect.top - targetY) };
}).sort((a, b) => a.score - b.score)[0].prompt;
}
function extractVersionControlDOMGPT(element) {
if (!(element instanceof Element) || !CURRENT_SITE.versionControl) return null;
const vc = CURRENT_SITE.versionControl;
const userSelector = '[data-turn="user"], section[data-turn="user"], [data-message-author-role="user"], user-query';
const userTurn = element.closest(userSelector) || element.querySelector?.(userSelector) || null;
if (!(userTurn instanceof Element)) return null;
const scopes = [];
const addScope = scope => {
if (!(scope instanceof Element) || scopes.includes(scope)) return;
const users = Array.from(scope.querySelectorAll(userSelector));
if (scope.matches(userSelector)) users.unshift(scope);
const uniqueUsers = Array.from(new Set(users));
if (uniqueUsers.length <= 1 || (uniqueUsers.length === 1 && uniqueUsers[0] === userTurn)) scopes.push(scope);
};
addScope(userTurn);
let node = userTurn.parentElement;
for (let depth = 0; node && node !== document.body && depth < 10; depth += 1, node = node.parentElement) {
const users = Array.from(node.querySelectorAll(userSelector));
if (users.length > 1) break;
addScope(node);
}
for (const scope of scopes) {
const controls = vc.container ? Array.from(scope.querySelectorAll(vc.container)) : [scope];
for (const control of controls) {
const info = extractVersionControlDOM(control, vc);
if (info && info.hasMultipleVersions) return info;
}
}
return null;
}
function getVersionSignatureGPT(versionInfo) {
if (!versionInfo) return '';
return `${versionInfo.currentVersion || ''}/${versionInfo.totalVersions || ''}`;
}
function refreshVersionControlDataGPT(prompt) {
if (!prompt || !(prompt.element instanceof Element)) return null;
const fresh = extractVersionControlDOMGPT(prompt.element);
if (fresh) prompt.versionInfo = fresh;
else delete prompt.versionInfo;
return fresh;
}
function extractMountedMessageMapDOMGPT(realPrompts = null, buttons = null) {
const list = Array.isArray(buttons) ? buttons : extractTOCButtonsDOMGPT();
const prompts = Array.isArray(realPrompts) ? realPrompts : extractVisibleMessagesDOMGPT();
const livePrompts = prompts.filter(prompt => prompt && prompt.element instanceof Element && prompt.element.isConnected);
const mapped = mapDOMMessagesToTOCGPT(livePrompts, list);
const liveMapped = new Map();
mapped.forEach((prompt, index) => {
if (prompt && prompt.element instanceof Element && prompt.element.isConnected) liveMapped.set(index, prompt);
});
return liveMapped;
}
function getVersionControlDataGPT(reason, options = {}) {
if (!IS_CHATGPT || !CURRENT_SITE.versionControl) return false;
const buttons = Array.isArray(options.buttons) ? options.buttons : extractTOCButtonsDOMGPT();
if (!buttons.length) return false;
const targetIndex = Number.isFinite(options.targetIndex) ? options.targetIndex : -1;
const mounted = extractMountedMessageMapDOMGPT(options.realPrompts, buttons);
let changed = false;
mounted.forEach((prompt, index) => {
if (reason === ADDITIONAL_DATA_REASON_GPT.ADD_NEW_MESSAGE && targetIndex >= 0 && index !== targetIndex) return;
if (reason === ADDITIONAL_DATA_REASON_GPT.MESSAGE_MOUNT && targetIndex >= 0 && index !== targetIndex) return;
if (reason === ADDITIONAL_DATA_REASON_GPT.EDIT_MESSAGE && index !== targetIndex) return;
if (reason === ADDITIONAL_DATA_REASON_GPT.SWITCH_VERSION && targetIndex >= 0 && index < targetIndex) return;
const fresh = extractVersionControlDOMGPT(prompt.element);
const next = fresh && fresh.hasMultipleVersions ? fresh : null;
const previous = chatGptTocDetailCache.get(index) || null;
const previousSignature = getVersionSignatureGPT(previous && previous.versionInfo);
const nextSignature = getVersionSignatureGPT(next);
prompt.versionInfo = next;
if (previous) {
previous.versionInfo = next;
previous.element = prompt.element;
previous.textElement = prompt.textElement || prompt.element;
previous.updatedAt = Date.now();
} else {
cacheMessageDetailGPT(index, { ...prompt, versionInfo: next });
}
if (previousSignature !== nextSignature) changed = true;
});
return changed;
}
function extractUserFileBadgeDOMGPT(prompt) {
if (!prompt || !(prompt.element instanceof Element)) return [];
const container = prompt.element;
const msgConfig = CURRENT_SITE.userMessage || {};
const files = [];
if (msgConfig.file) {
Array.from(container.querySelectorAll(msgConfig.file)).filter(isElementVisibleForNav).forEach(fileEl => {
const info = extractFileInfoDOM(fileEl, msgConfig);
if (info.name || info.ext !== 'file') files.push(info);
});
}
if (msgConfig.image) {
Array.from(container.querySelectorAll(msgConfig.image)).forEach(imgEl => {
if (!(imgEl instanceof HTMLImageElement) || imgEl.closest('#message-nav, #acn-mini-tooltip')) return;
files.push({
ext: 'img',
name: imgEl.getAttribute('alt') || 'Image',
url: imgEl.currentSrc || imgEl.src || imgEl.getAttribute('src') || '',
sourceType: 'chatgpt-user-image'
});
});
}
return files;
}
function getBadgeUserMessageDataGPT(reason, options = {}) {
if (!IS_CHATGPT) return false;
if (reason === ADDITIONAL_DATA_REASON_GPT.EDIT_MESSAGE || reason === ADDITIONAL_DATA_REASON_GPT.SWITCH_VERSION) return false;
const buttons = Array.isArray(options.buttons) ? options.buttons : extractTOCButtonsDOMGPT();
if (!buttons.length) return false;
const targetIndex = Number.isFinite(options.targetIndex) ? options.targetIndex : -1;
const mounted = extractMountedMessageMapDOMGPT(options.realPrompts, buttons);
let changed = false;
mounted.forEach((prompt, index) => {
if (reason === ADDITIONAL_DATA_REASON_GPT.ADD_NEW_MESSAGE && targetIndex >= 0 && index !== targetIndex) return;
if (reason === ADDITIONAL_DATA_REASON_GPT.MESSAGE_MOUNT && targetIndex >= 0 && index !== targetIndex) return;
const previous = chatGptTocDetailCache.get(index) || null;
const nextFiles = extractUserFileBadgeDOMGPT(prompt);
const oldSignature = (previous && Array.isArray(previous.files) ? previous.files : []).map(file => `${file.ext || ''}:${file.name || ''}`).join('|');
const newSignature = nextFiles.map(file => `${file.ext || ''}:${file.name || ''}`).join('|');
prompt.files = nextFiles;
if (previous) {
previous.files = nextFiles;
previous.element = prompt.element;
previous.textElement = prompt.textElement || prompt.element;
previous.updatedAt = Date.now();
} else {
cacheMessageDetailGPT(index, { ...prompt, files: nextFiles });
}
if (oldSignature !== newSignature) changed = true;
});
return changed;
}
function getFileBadgeAIMessageDataGPT(reason, options = {}) {
if (!IS_CHATGPT) return false;
const buttons = Array.isArray(options.buttons) ? options.buttons : extractTOCButtonsDOMGPT();
if (!buttons.length) return false;
const targetIndex = Number.isFinite(options.targetIndex) ? options.targetIndex : -1;
const mounted = extractMountedMessageMapDOMGPT(options.realPrompts, buttons);
let changed = false;
mounted.forEach((prompt, index) => {
if (reason === ADDITIONAL_DATA_REASON_GPT.ADD_NEW_MESSAGE && targetIndex >= 0 && index !== targetIndex) return;
if (reason === ADDITIONAL_DATA_REASON_GPT.EDIT_MESSAGE && targetIndex >= 0 && index !== targetIndex) return;
if (reason === ADDITIONAL_DATA_REASON_GPT.MESSAGE_MOUNT && targetIndex >= 0 && index !== targetIndex) return;
if (reason === ADDITIONAL_DATA_REASON_GPT.SWITCH_VERSION && targetIndex >= 0 && index < targetIndex) return;
const previous = chatGptTocDetailCache.get(index) || null;
const oldGenerated = previous && Array.isArray(previous.generatedFiles) ? previous.generatedFiles : [];
const oldFiles = oldGenerated.filter(file => file && file.kind !== 'chatgpt-generated-image');
const oldImages = oldGenerated.filter(file => file && file.kind === 'chatgpt-generated-image');
const nextFiles = extractFileBadgeAIMessageDOMGPT(prompt.element);
const oldSignature = oldFiles.map(file => `${file.ext || ''}:${file.name || ''}:${file.kind || ''}:${file.aiMessageTurnId || ''}`).join('|');
const newSignature = nextFiles.map(file => `${file.ext || ''}:${file.name || ''}:${file.kind || ''}:${file.aiMessageTurnId || ''}`).join('|');
const combined = [...nextFiles, ...oldImages];
prompt.generatedFiles = combined;
if (previous) {
previous.generatedFiles = combined;
previous.element = prompt.element;
previous.textElement = prompt.textElement || prompt.element;
previous.updatedAt = Date.now();
} else {
cacheMessageDetailGPT(index, { ...prompt, generatedFiles: combined });
}
if (oldSignature !== newSignature) changed = true;
});
return changed;
}
function getImageBadgeAIMessageDataGPT(reason, options = {}) {
if (!IS_CHATGPT) return false;
const buttons = Array.isArray(options.buttons) ? options.buttons : extractTOCButtonsDOMGPT();
if (!buttons.length) return false;
const targetIndex = Number.isFinite(options.targetIndex) ? options.targetIndex : -1;
const mounted = extractMountedMessageMapDOMGPT(options.realPrompts, buttons);
let changed = false;
mounted.forEach((prompt, index) => {
if (reason === ADDITIONAL_DATA_REASON_GPT.ADD_NEW_MESSAGE && targetIndex >= 0 && index !== targetIndex) return;
if (reason === ADDITIONAL_DATA_REASON_GPT.EDIT_MESSAGE && targetIndex >= 0 && index !== targetIndex) return;
if (reason === ADDITIONAL_DATA_REASON_GPT.MESSAGE_MOUNT && targetIndex >= 0 && index !== targetIndex) return;
if (reason === ADDITIONAL_DATA_REASON_GPT.SWITCH_VERSION && targetIndex >= 0 && index < targetIndex) return;
const previous = chatGptTocDetailCache.get(index) || null;
const oldGenerated = previous && Array.isArray(previous.generatedFiles) ? previous.generatedFiles : [];
const oldFiles = oldGenerated.filter(file => file && file.kind !== 'chatgpt-generated-image');
const oldImages = oldGenerated.filter(file => file && file.kind === 'chatgpt-generated-image');
const nextImages = extractImageBadgeAIMessageDOMGPT(prompt.element);
const oldSignature = oldImages.map(file => `${file.ext || ''}:${file.name || ''}:${file.kind || ''}:${file.aiMessageTurnId || ''}:${file.imageKey || ''}`).join('|');
const newSignature = nextImages.map(file => `${file.ext || ''}:${file.name || ''}:${file.kind || ''}:${file.aiMessageTurnId || ''}:${file.imageKey || ''}`).join('|');
const combined = [...oldFiles, ...nextImages];
prompt.generatedFiles = combined;
if (previous) {
previous.generatedFiles = combined;
previous.element = prompt.element;
previous.textElement = prompt.textElement || prompt.element;
previous.updatedAt = Date.now();
} else {
cacheMessageDetailGPT(index, { ...prompt, generatedFiles: combined });
}
if (oldSignature !== newSignature) changed = true;
});
return changed;
}
function updateAdditionalDataGPT(reason, options = {}) {
if (!IS_CHATGPT) return false;
let changed = false;
if (getVersionControlDataGPT(reason, options)) changed = true;
if (reason !== ADDITIONAL_DATA_REASON_GPT.EDIT_MESSAGE && reason !== ADDITIONAL_DATA_REASON_GPT.SWITCH_VERSION) {
if (getBadgeUserMessageDataGPT(reason, options)) changed = true;
}
if (getFileBadgeAIMessageDataGPT(reason, options)) changed = true;
if (getImageBadgeAIMessageDataGPT(reason, options)) changed = true;
return changed;
}
async function switchVersionGPT(messageIndex, key) {
const targetIndex = Number(messageIndex) - 1;
if (!Number.isFinite(targetIndex) || targetIndex < 0) return false;
const source = promptCacheItems[targetIndex] || cachedPrompts[targetIndex] || chatGptTocDetailCache.get(targetIndex);
const versionInfo = source && source.versionInfo;
const button = versionInfo && versionInfo[key];
if (!(button instanceof HTMLElement) || button.disabled) return false;
const baseline = getVersionSignatureGPT(versionInfo);
button.click();
let settled = 0;
for (let attempt = 0; attempt < 18; attempt++) {
await sleep(attempt < 4 ? 90 : 140);
const buttons = extractTOCButtonsDOMGPT();
const realPrompts = extractVisibleMessagesDOMGPT();
const metadataChanged = updateAdditionalDataGPT(ADDITIONAL_DATA_REASON_GPT.SWITCH_VERSION, {
targetIndex,
buttons,
realPrompts
});
seedTOCMessageCacheGPT(realPrompts);
const refreshed = chatGptTocDetailCache.get(targetIndex);
const current = getVersionSignatureGPT(refreshed && refreshed.versionInfo);
if (current && current !== baseline) settled += 1;
else if (metadataChanged) settled += 1;
else if (settled > 0) settled += 1;
if (settled >= 3) break;
}
updateMessageList(true);
return true;
}
function extractMessageDOMByIndexGPT(realPrompts, buttons, targetIndex) {
if (!Array.isArray(realPrompts) || !realPrompts.length || !Array.isArray(buttons) || !buttons.length) return null;
const availableIndices = buttons.map((button, fallbackIndex) => extractTOCIndexDOMGPT(button, fallbackIndex));
if (!availableIndices.includes(targetIndex)) return null;
for (const prompt of realPrompts) {
if (extractMessageIndexFromBadgeDOMGPT(prompt, availableIndices) === targetIndex) return prompt;
}
const existing = chatGptTocDetailCache.get(targetIndex);
if (existing && existing.element) {
const existingNodeId = extractTurnIdDOMGPT(existing.element);
const samePrompt = realPrompts.find(prompt => {
if (!prompt || !prompt.element) return false;
if (prompt.element === existing.element) return true;
const nodeId = extractTurnIdDOMGPT(prompt.element);
return Boolean(existingNodeId && nodeId && existingNodeId === nodeId);
});
if (samePrompt) return samePrompt;
}
const tocText = normalizeTOCTextGPT(chatGptTocTextCache.get(targetIndex) || '');
if (tocText) {
const textMatches = realPrompts.filter(prompt => isLikelySameMessageTextGPT(prompt.text, tocText));
if (textMatches.length === 1) return textMatches[0];
}
const activeIndex = extractActiveTOCIndexDOMGPT(buttons);
if (activeIndex === targetIndex) return extractBestVisibleMessageDOMGPT(realPrompts);
if (targetIndex === availableIndices[availableIndices.length - 1]) return realPrompts[realPrompts.length - 1] || null;
return null;
}
function refreshMessageGPT(realPrompts, buttons, targetIndex, force = false) {
if (!Number.isFinite(targetIndex) || targetIndex < 0) return false;
const livePrompt = extractMessageDOMByIndexGPT(realPrompts, buttons, targetIndex);
if (!livePrompt || !livePrompt.element || !livePrompt.element.isConnected) return false;
const liveText = normalizeTOCTextGPT(livePrompt.text || '');
if (!liveText && !(Array.isArray(livePrompt.files) && livePrompt.files.length)) return false;
const existing = chatGptTocDetailCache.get(targetIndex);
const liveNodeId = extractTurnIdDOMGPT(livePrompt.element);
const oldNodeId = existing && existing.element ? extractTurnIdDOMGPT(existing.element) : '';
const liveVersion = livePrompt.versionInfo
? `${livePrompt.versionInfo.currentVersion || ''}/${livePrompt.versionInfo.totalVersions || ''}`
: '';
const oldVersion = existing && existing.versionInfo
? `${existing.versionInfo.currentVersion || ''}/${existing.versionInfo.totalVersions || ''}`
: '';
const oldText = existing ? normalizeTOCTextGPT(existing.text || '') : '';
const liveFiles = (Array.isArray(livePrompt.files) ? livePrompt.files : [])
.map(file => `${file.ext || ''}:${file.name || ''}`)
.join('|');
const oldFiles = (existing && Array.isArray(existing.files) ? existing.files : [])
.map(file => `${file.ext || ''}:${file.name || ''}`)
.join('|');
const liveGeneratedFiles = (Array.isArray(livePrompt.generatedFiles) ? livePrompt.generatedFiles : [])
.map(file => `${file.ext || ''}:${file.name || ''}`)
.join('|');
const oldGeneratedFiles = (existing && Array.isArray(existing.generatedFiles) ? existing.generatedFiles : [])
.map(file => `${file.ext || ''}:${file.name || ''}`)
.join('|');
const changed = force
|| !existing
|| existing.element !== livePrompt.element
|| liveNodeId !== oldNodeId
|| liveText !== oldText
|| liveVersion !== oldVersion
|| liveFiles !== oldFiles
|| liveGeneratedFiles !== oldGeneratedFiles;
if (!changed) return false;
for (const [index, detail] of Array.from(chatGptTocDetailCache.entries())) {
if (index === targetIndex || !detail) continue;
const detailNodeId = detail.element ? extractTurnIdDOMGPT(detail.element) : '';
const sameElement = Boolean(detail.element && detail.element === livePrompt.element);
const sameNodeId = Boolean(liveNodeId && detailNodeId && liveNodeId === detailNodeId);
if (sameElement || sameNodeId) chatGptTocDetailCache.delete(index);
}
commitMessageDetailGPT(targetIndex, livePrompt);
return true;
}
function refreshActiveMessageGPT(realPrompts, buttons) {
const activeIndex = extractActiveTOCIndexDOMGPT(buttons);
if (activeIndex < 0) return false;
const pending = chatGptTocPostActionPending;
if (pending && pending.targetIndex >= 0 && activeIndex !== pending.targetIndex) return false;
return refreshMessageGPT(realPrompts, buttons, activeIndex, false);
}
function extractMessageIndexFromBadgeDOMGPT(prompt, availableIndices = null) {
if (!prompt) return -1;
const roots = [prompt.textElement, prompt.element].filter(Boolean);
let badge = null;
for (const root of roots) {
if (!(root instanceof Element)) continue;
if (root.classList && root.classList.contains('acn-message-inline-badge')) {
badge = root;
} else {
badge = root.querySelector('.acn-message-inline-badge');
}
if (badge) break;
}
if (!badge) return -1;
const number = Number.parseInt(String(badge.textContent || '').trim(), 10);
const index = Number.isFinite(number) ? number - 1 : -1;
if (index < 0) return -1;
if (Array.isArray(availableIndices) && !availableIndices.includes(index)) return -1;
return index;
}
// ============================================================================
// 07.9 TOC <-> DOM mapping & message construction
// ============================================================================
function mapDOMMessagesToTOCGPT(realPrompts, buttons) {
const mapped = new Map();
const usedPrompts = new Set();
const availableIndices = buttons.map((button, fallbackIndex) => extractTOCIndexDOMGPT(button, fallbackIndex));
const availableSet = new Set(availableIndices);
const assign = (index, prompt) => {
if (!availableSet.has(index) || !prompt || mapped.has(index) || usedPrompts.has(prompt)) return false;
if (!chatGptTocDetailCache.has(index)) cacheMessageDetailGPT(index, prompt);
mapped.set(index, prompt);
usedPrompts.add(prompt);
return true;
};
if (realPrompts.length === buttons.length) {
realPrompts.forEach((prompt, position) => assign(availableIndices[position], prompt));
return mapped;
}
realPrompts.forEach(prompt => {
const badgeIndex = extractMessageIndexFromBadgeDOMGPT(prompt, availableIndices);
if (badgeIndex >= 0) assign(badgeIndex, prompt);
});
realPrompts.forEach(prompt => {
if (usedPrompts.has(prompt) || !prompt.element) return;
for (const [index, detail] of chatGptTocDetailCache.entries()) {
if (!availableSet.has(index) || mapped.has(index)) continue;
if (detail && detail.element === prompt.element) {
assign(index, prompt);
break;
}
}
});
realPrompts.forEach(prompt => {
if (usedPrompts.has(prompt)) return;
const candidates = availableIndices.filter(index => {
if (mapped.has(index)) return false;
const knownText = getTOCPreviewGPT(index);
return knownText && isLikelySameMessageTextGPT(prompt.text, knownText);
});
if (candidates.length === 1) assign(candidates[0], prompt);
});
const activeIndex = extractActiveTOCIndexDOMGPT(buttons);
if (activeIndex >= 0 && !mapped.has(activeIndex)) {
const remaining = realPrompts.filter(prompt => !usedPrompts.has(prompt));
const best = extractBestVisibleMessageDOMGPT(remaining);
if (best) assign(activeIndex, best);
}
chatGptTocDetailCache.forEach((detail, index) => {
if (!availableSet.has(index) || mapped.has(index) || !detail) return;
mapped.set(index, detail);
});
return mapped;
}
function buildTOCMessageItemsGPT(realPrompts = []) {
const buttons = extractTOCButtonsDOMGPT();
if (buttons.length) {
syncActiveTOCIndexGPT(buttons);
cacheTOCTextGPT(buttons);
refreshActiveMessageGPT(realPrompts, buttons);
}
const forceInitialHarvest = !chatGptTocInitialHarvestDone;
if (forceInitialHarvest) chatGptTocInitialHarvestDone = true;
if (buttons.length) {
openTOCGPT(TOC_OPEN_REASON_GPT.FIRST_OPEN, { buttons, force: forceInitialHarvest });
const metadataReason = forceInitialHarvest
? ADDITIONAL_DATA_REASON_GPT.FIRST_OPEN
: ADDITIONAL_DATA_REASON_GPT.MESSAGE_MOUNT;
updateAdditionalDataGPT(metadataReason, { buttons, realPrompts });
}
const apiCache = chatGptApiConversationCache;
const apiReady = Boolean(
apiCache
&& apiCache.conversationId === getChatGPTConversationIdFromUrlAPI()
&& Array.isArray(apiCache.userMessages)
&& apiCache.userMessages.length
);
if (!apiReady) ensureChatGPTApiConversationCache();
if (apiReady) {
const knownIds = apiCache.userMessageIdSet
|| (apiCache.userMessageIdSet = new Set(apiCache.userMessages.map(m => m.id)));
const hasNewUserMessage = realPrompts.some(prompt => {
const idEl = prompt.element.closest('[data-message-id]') || prompt.element.querySelector('[data-message-id]');
const messageId = idEl && idEl.getAttribute('data-message-id');
return messageId && !knownIds.has(messageId);
});
if (hasNewUserMessage) ensureChatGPTApiConversationCache(true);
}
if (!buttons.length && !apiReady) return realPrompts;
const realByIndex = mapDOMMessagesToTOCGPT(realPrompts, buttons);
const cfg = CURRENT_SITE.nativePromptToc || {};
if (apiReady) {
const buttonsByIndex = new Map(buttons.map((button, fallbackIndex) => [extractTOCIndexDOMGPT(button, fallbackIndex), button]));
const realByMessageId = buildRealPromptsByMessageIdGPT(realPrompts);
const apiItems = apiCache.userMessages.map((apiMsg, index) =>
buildTOCItemForIndexGPT(index, buttonsByIndex.get(index) || null, apiMsg.text, realByIndex, cfg, realByMessageId.get(apiMsg.id) || null, apiMsg.createTime, apiMsg.apiGeneratedFiles, apiMsg.apiUserFiles)
);
const knownIds = apiCache.userMessageIdSet || new Set(apiCache.userMessages.map(m => m.id));
const pendingPrompts = realPrompts.filter(prompt => {
const idEl = prompt.element.closest('[data-message-id]') || prompt.element.querySelector('[data-message-id]');
const messageId = idEl && idEl.getAttribute('data-message-id');
return messageId && !knownIds.has(messageId);
});
const visibleDomIds = new Set();
realPrompts.forEach(prompt => {
const idEl = prompt.element.closest('[data-message-id]') || prompt.element.querySelector('[data-message-id]');
const messageId = idEl && idEl.getAttribute('data-message-id');
if (messageId) visibleDomIds.add(messageId);
});
let staleTrailingCount = 0;
if (pendingPrompts.length > 0) {
for (let k = apiCache.userMessages.length - 1; k >= 0; k--) {
if (visibleDomIds.has(apiCache.userMessages[k].id)) break;
staleTrailingCount++;
}
}
const replaceCount = Math.min(staleTrailingCount, pendingPrompts.length);
if (replaceCount > 0) apiItems.length -= replaceCount;
if (typeof unsafeWindow !== 'undefined' && unsafeWindow.__acnChatGPTActiveDebug) {
console.log('[ACN-GPT-PENDING]', { realPromptsLength: realPrompts.length, apiItemsLength: apiItems.length, pendingPromptsLength: pendingPrompts.length, staleTrailingCount, replaceCount });
}
pendingPrompts.forEach((prompt, i) => {
apiItems.push(buildTOCItemForIndexGPT(apiItems.length + i, null, prompt.text, realByIndex, cfg, prompt, Date.now() / 1000, null));
});
return apiItems;
}
return buttons.map((button, fallbackIndex) => {
const index = extractTOCIndexDOMGPT(button, fallbackIndex);
return buildTOCItemForIndexGPT(index, button, '', realByIndex, cfg);
});
}
function seedTOCMessageCacheGPT(visiblePrompts = null) {
if (!isTOCEnabledGPT()) return false;
resetPromptCacheIfNeeded();
resetTOCStateIfNeededGPT();
const visible = Array.isArray(visiblePrompts) ? visiblePrompts : extractVisibleMessagesDOMGPT();
const merged = buildTOCMessageItemsGPT(visible);
if (!merged.length) return false;
promptCacheItems = merged;
promptCacheMap = new Map(merged.map(item => [item._acnKey, item]));
return true;
}
function extractMessageTurnKeyDOMGPT(element, fallbackIndex) {
const turnEl = element && (element.closest('[data-turn-id], [data-turn-id-container], [data-testid^="conversation-turn"]') || element.parentElement);
if (turnEl && turnEl.getAttribute) {
const tid = turnEl.getAttribute('data-turn-id') || turnEl.getAttribute('data-turn-id-container') || turnEl.getAttribute('data-testid');
if (tid) return 'turn:' + tid;
}
const article = element && element.closest('article');
if (article) {
const allArticles = Array.from(document.querySelectorAll('article'));
const articleIndex = allArticles.indexOf(article);
if (articleIndex >= 0) return 'article:' + articleIndex;
}
const rect = element && element.getBoundingClientRect ? element.getBoundingClientRect() : null;
return 'visible:' + fallbackIndex + ':' + (rect ? Math.round(rect.top) : 0);
}
function collapseMessageVersionsGPT(prompts) {
const groups = new Map();
prompts.forEach((prompt, index) => {
const key = extractMessageTurnKeyDOMGPT(prompt.element, index);
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(prompt);
});
const collapsed = [];
groups.forEach(group => {
group.sort((a, b) => {
const ar = a.element.getBoundingClientRect();
const br = b.element.getBoundingClientRect();
return ar.top - br.top;
});
const visible = group.filter(item => isElementVisibleForNav(item.element));
const candidates = visible.length ? visible : group;
const chosen = candidates
.map(item => {
const rect = item.element.getBoundingClientRect();
const score = Math.max(0, Math.min(rect.bottom, window.innerHeight) - Math.max(rect.top, 0));
return { item, score };
})
.sort((a, b) => b.score - a.score)[0].item;
collapsed.push(chosen);
});
collapsed.sort((a, b) => {
const ar = a.element.getBoundingClientRect();
const br = b.element.getBoundingClientRect();
return ar.top - br.top;
});
return collapsed;
}
function extractVisibleMessagesDOMGPT() {
const domPrompts = [];
const selectors = ['user-query', 'div[data-message-author-role="user"]'];
const seen = new Set();
selectors.forEach(selector => {
document.querySelectorAll(selector).forEach(container => {
if (!(container instanceof HTMLElement) || seen.has(container)) return;
seen.add(container);
if (!isElementVisibleForNav(container)) return;
const msgConfig = CURRENT_SITE.userMessage;
const fileEls = msgConfig.file ? Array.from(container.querySelectorAll(msgConfig.file)).filter(isElementVisibleForNav) : [];
const imageEls = msgConfig.image ? Array.from(container.querySelectorAll(msgConfig.image)).filter(imgEl => {
if (!(imgEl instanceof HTMLImageElement)) return false;
if (imgEl.closest('#message-nav, #acn-mini-tooltip')) return false;
return true;
}) : [];
const msgEl = msgConfig.text ? container.querySelector(msgConfig.text) : null;
const files = [];
fileEls.forEach(fileEl => {
const info = extractFileInfoDOM(fileEl, msgConfig);
if (!info.name && info.ext === 'file') return;
files.push(info);
});
imageEls.forEach(imgEl => files.push({
ext: 'img',
name: imgEl.getAttribute('alt') || 'Image',
url: imgEl.currentSrc || imgEl.src || imgEl.getAttribute('src') || '',
sourceType: 'chatgpt-user-image'
}));
let text = msgEl ? extractCleanTextDOM(msgEl) : extractCleanTextDOM(container);
text = String(text || '').replace(/^\s*\d+\s*$/,'').trim();
if (!text && files.length) text = files.map(f => f.name).join(', ');
if (!text && (container.querySelector('img') || container.querySelector('canvas'))) text = 'Image';
if (!text && !files.length) return;
const promptData = { element: container, textElement: msgEl || container, text, files };
promptData.generatedFiles = [...extractFileBadgeAIMessageDOMGPT(container), ...extractImageBadgeAIMessageDOMGPT(container)];
const versionInfo = extractVersionControlDOMGPT(container);
if (versionInfo) promptData.versionInfo = versionInfo;
domPrompts.push(promptData);
});
});
domPrompts.sort((a, b) => {
const ar = a.element.getBoundingClientRect();
const br = b.element.getBoundingClientRect();
return ar.top - br.top;
});
return collapseMessageVersionsGPT(domPrompts);
}
function extractUserMessagesDOMGPT() {
const prompts = extractVisibleMessagesDOMGPT();
if (!hasUsableTOCGPT()) return rememberVisiblePromptsForCache(prompts, 'replace');
return prompts;
}
function normalizeMessageMatchTextGPT(text) {
return String(text || '')
.replace(/\.\.\.$/, '')
.replace(/\s+/g, ' ')
.trim()
.toLowerCase();
}
function isLikelySameMessageTextGPT(a, b) {
const left = normalizeMessageMatchTextGPT(a);
const right = normalizeMessageMatchTextGPT(b);
if (!left || !right) return false;
if (left === right) return true;
const minLen = Math.min(left.length, right.length);
if (minLen >= 18 && (left.includes(right.slice(0, Math.min(60, right.length))) || right.includes(left.slice(0, Math.min(60, left.length))))) return true;
const leftHead = left.slice(0, Math.min(42, left.length));
const rightHead = right.slice(0, Math.min(42, right.length));
return leftHead.length >= 12 && rightHead.length >= 12 && (leftHead === rightHead || leftHead.includes(rightHead) || rightHead.includes(leftHead));
}
function extractVisibleMessageForTOCIndexDOMGPT(index) {
const cached = chatGptTocDetailCache.get(index);
if (cached && cached.element && cached.element.isConnected) {
refreshVersionControlDataGPT(cached);
const liveGeneratedFiles = [...extractFileBadgeAIMessageDOMGPT(cached.element), ...extractImageBadgeAIMessageDOMGPT(cached.element)];
const oldSig = (cached.generatedFiles || []).map(f => `${f.name || ''}:${f.ext || ''}`).join('|');
const newSig = liveGeneratedFiles.map(f => `${f.name || ''}:${f.ext || ''}`).join('|');
if (oldSig !== newSig) return { ...cached, generatedFiles: liveGeneratedFiles };
return cached;
}
const prompts = extractVisibleMessagesDOMGPT();
if (!prompts.length) return null;
const knownText = getTOCPreviewGPT(index);
const exact = knownText ? prompts.find(prompt => isLikelySameMessageTextGPT(prompt.text, knownText)) : null;
if (exact) {
refreshVersionControlDataGPT(exact);
return exact;
}
const activeIndex = extractActiveTOCIndexDOMGPT();
const active = activeIndex === index ? extractBestVisibleMessageDOMGPT(prompts) : null;
if (active) refreshVersionControlDataGPT(active);
return active;
}
async function waitForMessageDetailGPT(index, attempts = 10, delay = 120) {
for (let i = 0; i < attempts; i++) {
const detail = extractVisibleMessageForTOCIndexDOMGPT(index);
if (detail) return detail;
await sleep(delay);
}
return null;
}
async function reconcileMountedMessageAfterNavigationGPT(index, attempts = 14, delay = 100) {
let latest = null;
for (let i = 0; i < attempts; i++) {
const buttons = extractTOCButtonsDOMGPT();
const prompts = extractVisibleMessagesDOMGPT();
const live = extractMessageDOMByIndexGPT(prompts, buttons, index) || extractVisibleMessageForTOCIndexDOMGPT(index);
if (live && live.element && live.element.isConnected) {
refreshVersionControlDataGPT(live);
latest = live;
cacheMessageDetailGPT(index, live);
if (live.versionInfo && live.versionInfo.hasMultipleVersions) break;
}
if (i + 1 < attempts) await sleep(delay);
}
if (!latest) return false;
seedTOCMessageCacheGPT(extractVisibleMessagesDOMGPT());
updateMessageList(true);
return true;
}
function cacheMessageDetailGPT(index, detail) {
if (!Number.isFinite(index) || index < 0 || !detail) return;
const previous = chatGptTocDetailCache.get(index) || null;
const text = normalizeTOCTextGPT(detail.text || '');
const incomingFiles = Array.isArray(detail.files) ? detail.files : [];
const incomingGeneratedFiles = Array.isArray(detail.generatedFiles) ? detail.generatedFiles : [];
const mergedFiles = incomingFiles.length ? incomingFiles : (previous && Array.isArray(previous.files) ? previous.files : []);
const mergedGeneratedFiles = incomingGeneratedFiles.length
? incomingGeneratedFiles
: (previous && Array.isArray(previous.generatedFiles) ? previous.generatedFiles : []);
chatGptTocDetailCache.set(index, {
text: text || (previous ? previous.text || '' : ''),
files: mergedFiles,
versionInfo: Object.prototype.hasOwnProperty.call(detail, 'versionInfo')
? (detail.versionInfo || null)
: (previous ? previous.versionInfo || null : null),
generatedFiles: mergedGeneratedFiles,
element: detail.element || (previous ? previous.element || null : null),
textElement: detail.textElement || detail.element || (previous ? previous.textElement || previous.element || null : null),
updatedAt: Date.now()
});
}
function commitMessageDetailGPT(index, detail) {
if (!Number.isFinite(index) || index < 0 || !detail) return;
chatGptTocDetailCache.set(index, {
text: normalizeTOCTextGPT(detail.text || ''),
files: Array.isArray(detail.files) ? detail.files : [],
versionInfo: detail.versionInfo || null,
generatedFiles: Array.isArray(detail.generatedFiles) ? detail.generatedFiles : [],
element: detail.element || null,
textElement: detail.textElement || detail.element || null,
updatedAt: Date.now()
});
}
function getMessageDetailSignatureGPT(detail) {
if (!detail) return '';
const text = normalizeTOCTextGPT(detail.text || '');
const files = (Array.isArray(detail.files) ? detail.files : [])
.map(file => `${file.ext || ''}:${file.name || ''}`)
.join('|');
const generatedFiles = (Array.isArray(detail.generatedFiles) ? detail.generatedFiles : [])
.map(file => `${file.ext || ''}:${file.name || ''}`)
.join('|');
const version = detail.versionInfo
? `${detail.versionInfo.currentVersion || ''}/${detail.versionInfo.totalVersions || ''}`
: '';
return `${text}::${files}::${generatedFiles}::${version}`;
}
function harvestMountedMessageDetailsGPT() {
if (!IS_CHATGPT || !hasUsableTOCGPT() || chatGptPendingConversationUrl) return false;
const buttons = extractTOCButtonsDOMGPT();
if (!buttons.length) return false;
const prompts = extractVisibleMessagesDOMGPT();
if (!prompts.length) return false;
syncActiveTOCIndexGPT(buttons);
cacheTOCTextGPT(buttons);
const mapped = mapDOMMessagesToTOCGPT(prompts, buttons);
let changed = false;
mapped.forEach((prompt, index) => {
if (!prompt) return;
refreshVersionControlDataGPT(prompt);
const previous = chatGptTocDetailCache.get(index);
const before = getMessageDetailSignatureGPT(previous);
cacheMessageDetailGPT(index, prompt);
const after = getMessageDetailSignatureGPT(chatGptTocDetailCache.get(index));
if (before !== after) changed = true;
});
if (changed) seedTOCMessageCacheGPT(prompts);
return changed;
}
function refreshActiveMessageDetailGPT() {
if (!IS_CHATGPT || !isTOCEnabledGPT() || chatGptPendingConversationUrl) return;
if (isScanningOldMessages || chatGptHighlightSuppressionDepth > 0) return;
const buttons = extractTOCButtonsDOMGPT();
if (!buttons.length) return;
const activeIndex = extractActiveTOCIndexDOMGPT(buttons);
const total = cachedPrompts.length || buttons.length;
if (activeIndex < 0 || activeIndex >= buttons.length || activeIndex + 1 > total) return;
const activeChanged = activeMessageIndex !== activeIndex + 1;
if (activeChanged) activeMessageIndex = activeIndex + 1;
const detail = extractVisibleMessageForTOCIndexDOMGPT(activeIndex);
if (detail && detail.element instanceof Element) refreshVersionControlDataGPT(detail);
if (!detail) {
if (activeChanged) scheduleTOCRefreshGPT(true);
return;
}
const existing = chatGptTocDetailCache.get(activeIndex);
const detailText = normalizeTOCTextGPT(detail.text || '');
const newSig = detailText + '::' + (detail.files || []).map(f => f.ext + ':' + (f.name || '')).join('|') + '::' + (detail.generatedFiles || []).map(f => f.ext + ':' + (f.name || '')).join('|') + '::' + (detail.versionInfo ? detail.versionInfo.currentVersion + '/' + detail.versionInfo.totalVersions : '');
const oldSig = existing ? (normalizeTOCTextGPT(existing.text || '') + '::' + (existing.files || []).map(f => f.ext + ':' + (f.name || '')).join('|') + '::' + (existing.generatedFiles || []).map(f => f.ext + ':' + (f.name || '')).join('|') + '::' + (existing.versionInfo ? existing.versionInfo.currentVersion + '/' + existing.versionInfo.totalVersions : '')) : '';
if (newSig !== oldSig || activeChanged) {
cacheMessageDetailGPT(activeIndex, detail);
scheduleTOCRefreshGPT(true);
}
}
function buildRealPromptsByMessageIdGPT(realPrompts) {
const map = new Map();
realPrompts.forEach(prompt => {
if (!prompt || !prompt.element) return;
const idEl = prompt.element.closest('[data-message-id]') || prompt.element.querySelector('[data-message-id]');
const messageId = idEl && idEl.getAttribute('data-message-id');
if (messageId && !map.has(messageId)) map.set(messageId, prompt);
});
return map;
}
function buildTOCItemForIndexGPT(index, button, apiText, realByIndex, cfg, mappedRealOverride, apiCreateTime, apiGeneratedFiles, apiUserFiles) {
const detail = chatGptTocDetailCache.get(index) || null;
const mappedReal = mappedRealOverride || realByIndex.get(index) || null;
const hasConnectedMappedReal = Boolean(mappedReal && mappedReal.element && mappedReal.element.isConnected);
if (hasConnectedMappedReal) refreshVersionControlDataGPT(mappedReal);
const metadata = hasConnectedMappedReal ? { ...(detail || {}), ...mappedReal } : (detail || mappedReal || null);
const liveElementSource = hasConnectedMappedReal ? mappedReal : metadata;
const hasConnectedRealElement = Boolean(liveElementSource && liveElementSource.element && liveElementSource.element.isConnected);
const hasConnectedButton = Boolean(button && button.isConnected);
const fullText = normalizeTOCTextGPT(metadata && metadata.text ? metadata.text : '');
const previewText = normalizeTOCTextGPT(chatGptTocTextCache.get(index) || '');
const text = previewText || fullText || normalizeTOCTextGPT(apiText || '') || `Prompt ${index + 1}`;
const domFiles = metadata && Array.isArray(metadata.files) ? metadata.files : [];
const files = Array.isArray(apiUserFiles) && apiUserFiles.length
? apiUserFiles
: domFiles;
const domVersionInfo = hasConnectedMappedReal
? (mappedReal.versionInfo || null)
: (detail && detail.versionInfo ? detail.versionInfo : null);
const versionInfo = domVersionInfo || getChatGPTApiVersionInfoForUserOrdinal(index);
if (hasConnectedMappedReal) {
const cached = chatGptTocDetailCache.get(index);
if (cached) {
cached.versionInfo = versionInfo;
cached.element = mappedReal.element;
cached.textElement = mappedReal.textElement || mappedReal.element;
cached.updatedAt = Date.now();
} else {
cacheMessageDetailGPT(index, mappedReal);
}
}
if (hasConnectedButton) {
button.__acnNativeToc = true;
button.__acnNativeIndex = index;
}
const anchorElement = hasConnectedRealElement ? liveElementSource.element : (hasConnectedButton ? button : null);
const anchorTextElement = hasConnectedRealElement ? (liveElementSource.textElement || liveElementSource.element) : (hasConnectedButton ? button : null);
const domGeneratedFiles = metadata && Array.isArray(metadata.generatedFiles) ? metadata.generatedFiles : [];
const apiImageNames = new Set(
(Array.isArray(apiGeneratedFiles) ? apiGeneratedFiles : [])
.filter(f => f && f.kind === 'chatgpt-generated-image')
.map(f => f.name)
);
const apiWritingBlockOrdinals = new Set(
(Array.isArray(apiGeneratedFiles) ? apiGeneratedFiles : [])
.filter(f => f && f.kind === 'chatgpt-writing-block')
.map(f => f.ordinal)
);
const mergedGeneratedFiles = Array.isArray(apiGeneratedFiles)
? domGeneratedFiles
.filter(f => f && f.kind !== 'chatgpt-generated-file')
.filter(f => !(f && f.kind === 'chatgpt-generated-image' && apiImageNames.has(f.name)))
.filter(f => !(f && f.kind === 'chatgpt-writing-block' && apiWritingBlockOrdinals.has(f.ordinal)))
.concat(apiGeneratedFiles)
: domGeneratedFiles;
return {
...(metadata || {}),
element: anchorElement,
textElement: anchorTextElement,
text,
files,
generatedFiles: mergedGeneratedFiles,
versionInfo,
_acnCreatedAt: parseMessageCreatedAtGPT(apiCreateTime),
_acnKey: `chatgpt-native:${index}`,
_acnNativeToc: !hasConnectedRealElement,
_acnNativeIndex: index,
_acnNativeButton: hasConnectedButton ? button : null,
_acnHideNumber: cfg.displayNumbers === false,
_acnUnmounted: !anchorElement,
_acnLastSeen: Date.now()
};
}
// ============================================================================
// 07.10 Generated-file and attachment detection
// ============================================================================
function extractAIMessageDOMGPT(userElement) {
if (!IS_CHATGPT || !(userElement instanceof HTMLElement)) return null;
const aiConfig = CURRENT_SITE.aiMessage;
if (!aiConfig || !aiConfig.userTurn || !aiConfig.container || !aiConfig.relationRoot || !aiConfig.turnWrapper) return null;
const userTurn = userElement.closest(aiConfig.userTurn) || (userElement.matches(aiConfig.userTurn) ? userElement : null);
if (!userTurn) return null;
const thread = userTurn.closest(aiConfig.relationRoot) || document.querySelector(aiConfig.relationRoot) || document;
const userWrapper = userTurn.closest(aiConfig.turnWrapper);
const chooseAssistant = wrapper => {
if (!(wrapper instanceof Element)) return null;
const candidates = [];
if (wrapper.matches(aiConfig.container)) candidates.push(wrapper);
wrapper.querySelectorAll(aiConfig.container).forEach(el => candidates.push(el));
const unique = Array.from(new Set(candidates)).filter(el => el instanceof HTMLElement && el.isConnected);
if (!unique.length) return null;
const visible = unique.filter(isElementVisibleForNav);
return (visible.length ? visible : unique)[0] || null;
};
if (userWrapper) {
const wrappers = Array.from(thread.querySelectorAll(aiConfig.turnWrapper));
const userWrapperIndex = wrappers.indexOf(userWrapper);
if (userWrapperIndex >= 0) {
for (let i = userWrapperIndex + 1; i < wrappers.length; i++) {
const wrapper = wrappers[i];
if (wrapper.querySelector(aiConfig.userTurn)) break;
const assistant = chooseAssistant(wrapper);
if (assistant) return assistant;
}
}
}
const ordered = Array.from(thread.querySelectorAll(`${aiConfig.userTurn}, ${aiConfig.container}`));
const uniqueTurns = [];
const seenWrappers = new Set();
ordered.forEach(turn => {
const wrapper = turn.closest(aiConfig.turnWrapper) || turn;
if (seenWrappers.has(wrapper)) return;
seenWrappers.add(wrapper);
uniqueTurns.push({ turn, wrapper });
});
const userIndex = uniqueTurns.findIndex(item => item.wrapper === userWrapper || item.turn === userTurn || item.wrapper.contains(userTurn));
if (userIndex >= 0) {
for (let i = userIndex + 1; i < uniqueTurns.length; i++) {
const { turn, wrapper } = uniqueTurns[i];
if ((turn.matches && turn.matches(aiConfig.userTurn)) || wrapper.querySelector(aiConfig.userTurn)) break;
const assistant = chooseAssistant(wrapper) || (turn.matches && turn.matches(aiConfig.container) ? turn : null);
if (assistant) return assistant;
}
}
return null;
}
function extractGeneratedFileNameDOMGPT(downloadBtn, aiMessageEl) {
const aiConfig = CURRENT_SITE.aiMessage;
if (!aiConfig || !aiConfig.fileName) return '';
let node = downloadBtn && downloadBtn.parentElement;
let depth = 0;
while (node && node !== aiMessageEl && depth++ < 8) {
const candidates = Array.from(node.querySelectorAll(aiConfig.fileName)).filter(btn => {
if (btn === downloadBtn) return false;
const label = (btn.getAttribute('aria-label') || '').trim();
if (!label || /^download file$/i.test(label)) return false;
if (/^(copy|share|rate|more actions|switch model|coding citation|open file)$/i.test(label)) return false;
return true;
});
const named = candidates.find(btn => {
const label = (btn.getAttribute('aria-label') || '').trim();
return /\.[a-z0-9]{1,10}$/i.test(label) || (aiConfig.fileNameVisualHint && btn.querySelector(aiConfig.fileNameVisualHint));
});
if (named) return (named.getAttribute('aria-label') || '').trim();
node = node.parentElement;
}
return '';
}
function extractGeneratedImageKeyDOMGPT(img) {
if (!(img instanceof HTMLImageElement)) return '';
const src = img.currentSrc || img.src || img.getAttribute('src') || '';
if (src) {
try {
const url = new URL(src, window.location.href);
const contentId = url.searchParams.get('id');
if (contentId) return `id:${contentId}`;
url.searchParams.delete('ts');
url.searchParams.delete('sig');
return `url:${url.href}`;
} catch (_) {
return `url:${src}`;
}
}
const alt = String(img.getAttribute('alt') || '').trim();
return alt ? `alt:${alt.toLowerCase()}` : '';
}
function extractWritingBlocksAIMessageDOMGPT(aiMessageEl, aiConfig = CURRENT_SITE.aiMessage || {}) {
if (!(aiMessageEl instanceof HTMLElement) || !aiConfig.writingBlock) return [];
const rootSelector = aiConfig.writingBlockRoot || aiConfig.writingBlock;
const ignoreSelector = aiConfig.writingBlockIgnore || '';
const writingBlocks = [];
Array.from(aiMessageEl.querySelectorAll(aiConfig.writingBlock)).forEach((node) => {
if (!(node instanceof HTMLElement)) return;
if (ignoreSelector && node.closest(ignoreSelector)) return;
const block = node.matches(rootSelector) ? node : (node.closest(rootSelector) || node);
if (!writingBlocks.includes(block)) writingBlocks.push(block);
});
return writingBlocks;
}
function extractFileBadgeAIMessageDOMGPT(userElement) {
if (!IS_CHATGPT || !(userElement instanceof HTMLElement)) return [];
const aiConfig = CURRENT_SITE.aiMessage;
if (!aiConfig || !aiConfig.file) return [];
const aiMessageEl = extractAIMessageDOMGPT(userElement);
if (!aiMessageEl) return [];
const seen = new Set();
const files = [];
extractGeneratedFilesFromTurnFiberGPT(aiMessageEl).forEach((file, ordinal) => {
const key = file.toolMessageId ? `file:${file.toolMessageId}` : `file:${file.name.toLowerCase()}`;
if (seen.has(key)) return;
seen.add(key);
files.push({ ...file, ordinal });
});
Array.from(aiMessageEl.querySelectorAll(aiConfig.file)).forEach((downloadBtn, ordinal) => {
const name = extractGeneratedFileNameDOMGPT(downloadBtn, aiMessageEl) || `file-${ordinal + 1}`;
const key = `file:${name.toLowerCase()}`;
if (seen.has(key)) return;
seen.add(key);
const extMatch = name.match(/\.([a-z0-9]{1,10})$/i);
files.push({
name,
ext: extMatch ? extMatch[1].toLowerCase() : 'file',
kind: 'chatgpt-generated-file',
aiMessageTurnId: extractTurnIdDOMGPT(aiMessageEl),
ordinal
});
});
if (aiConfig.writingBlock) {
const writingBlocks = extractWritingBlocksAIMessageDOMGPT(aiMessageEl, aiConfig);
writingBlocks.forEach((block, writingOrdinal) => {
const key = `writing-block:${writingOrdinal}`;
if (seen.has(key)) return;
seen.add(key);
const titleNode = aiConfig.writingBlockTitle ? block.querySelector(aiConfig.writingBlockTitle) : null;
const title = titleNode ? String(titleNode.textContent || '').trim() : '';
files.push({
name: title || `Writing block ${writingOrdinal + 1}`,
displayName: title ? `Writing block: ${title}` : `Writing block ${writingOrdinal + 1}`,
ext: 'writing block',
kind: 'chatgpt-writing-block',
aiMessageTurnId: extractTurnIdDOMGPT(aiMessageEl),
ordinal: writingOrdinal
});
});
}
return files;
}
function extractImageBadgeAIMessageDOMGPT(userElement) {
if (!IS_CHATGPT || !(userElement instanceof HTMLElement)) return [];
const aiConfig = CURRENT_SITE.aiMessage;
if (!aiConfig || !aiConfig.image) return [];
const aiMessageEl = extractAIMessageDOMGPT(userElement);
if (!aiMessageEl) return [];
const seen = new Set();
const images = [];
Array.from(aiMessageEl.querySelectorAll(aiConfig.image)).filter(img => {
if (!(img instanceof HTMLImageElement)) return false;
if (img.closest('#message-nav, #acn-mini-tooltip')) return false;
const alt = String(img.getAttribute('alt') || '').trim();
const src = img.currentSrc || img.src || img.getAttribute('src') || '';
return /^Generated image:/i.test(alt) || /\/backend-api\/estuary\/content\?/i.test(src) || Boolean(img.closest('.group\/imagegen-image'));
}).forEach((img, imageOrdinal) => {
const imageKey = extractGeneratedImageKeyDOMGPT(img);
if (!imageKey || seen.has(imageKey)) return;
seen.add(imageKey);
const alt = String(img.getAttribute('alt') || '').trim();
const name = alt.replace(/^Generated image:\s*/i, '').trim() || `Generated image ${imageOrdinal + 1}`;
images.push({
name,
displayName: alt || name,
ext: 'img',
kind: 'chatgpt-generated-image',
url: img.currentSrc || img.src || img.getAttribute('src') || '',
imageKey,
aiMessageTurnId: extractTurnIdDOMGPT(aiMessageEl),
ordinal: imageOrdinal
});
});
return images;
}
function findMountedUserImageGPT(index, fileIdx, file) {
const prompt = promptCacheItems[index - 1];
const roots = [];
if (prompt && prompt.element instanceof HTMLElement && prompt.element.isConnected) roots.push(prompt.element);
const nativeIndex = index - 1;
const detail = chatGptTocDetailCache.get(nativeIndex);
if (detail && detail.element instanceof HTMLElement && detail.element.isConnected && !roots.includes(detail.element)) {
roots.push(detail.element);
}
const wantedName = String(file && file.name || '').trim();
for (const root of roots) {
const images = Array.from(root.querySelectorAll('img')).filter(img => !img.closest('#message-nav, #acn-mini-tooltip'));
if (wantedName) {
const exact = images.find(img => String(img.getAttribute('alt') || '').trim() === wantedName);
if (exact) return exact;
}
const userImages = images.filter(img => {
const src = img.currentSrc || img.src || img.getAttribute('src') || '';
return /\/backend-api\/estuary\/content\?/i.test(src) || Boolean(img.closest('.group\/message-image, button[aria-label^="Open image:"]'));
});
if (userImages[fileIdx]) return userImages[fileIdx];
if (userImages.length === 1) return userImages[0];
}
return null;
}
async function downloadUserImageGPT(file, index, fileIdx) {
if (!IS_CHATGPT || !file || !(file.sourceType === 'chatgpt-user-image' || file.ext === 'img')) return false;
const filename = file.name || 'image.png';
let url = file.url || '';
if (url && await downloadBlobUrlAsFile(url, filename)) return true;
let img = findMountedUserImageGPT(index, fileIdx, file);
if (!img) {
await navigateToMessage(index);
for (let attempt = 0; attempt < 10 && !img; attempt++) {
await new Promise(resolve => window.setTimeout(resolve, attempt < 3 ? 100 : 180));
img = findMountedUserImageGPT(index, fileIdx, file);
}
}
if (!img) return false;
url = img.currentSrc || img.src || img.getAttribute('src') || '';
if (!url) return false;
file.url = url;
const nativeIndex = index - 1;
const detail = chatGptTocDetailCache.get(nativeIndex);
if (detail && Array.isArray(detail.files)) {
const matched = detail.files.find((candidate, idx) => idx === fileIdx || (candidate && candidate.name === file.name));
if (matched) matched.url = url;
}
return downloadBlobUrlAsFile(url, filename);
}
function waitForDialogMountedGPT(timeoutMs = 2000) {
return new Promise(resolve => {
const check = () => document.querySelector('[role="dialog"]');
const existing = check();
if (existing) { resolve(existing); return; }
const observer = new MutationObserver(() => {
const dialog = check();
if (dialog) {
observer.disconnect();
clearTimeout(timeoutId);
resolve(dialog);
}
});
observer.observe(document.body, { childList: true, subtree: true });
const timeoutId = setTimeout(() => { observer.disconnect(); resolve(null); }, timeoutMs);
});
}
function waitForPreviewContentFetchGPT(timeoutMs = 4000) {
return new Promise(resolve => {
const target = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
const origFetch = target.fetch;
let done = false;
const finish = () => {
if (done) return;
done = true;
target.fetch = origFetch;
clearTimeout(timeoutId);
resolve();
};
target.fetch = async function (...args) {
const req = args[0];
const url = typeof req === 'string' ? req : (req && req.url) || '';
const response = await origFetch.apply(this, args);
if (/\/backend-api\/files\/download\//.test(url)) finish();
return response;
};
const timeoutId = setTimeout(finish, timeoutMs);
});
}
async function openUserFilePreviewGPT(file, index, fileIdx) {
const debug = typeof unsafeWindow !== 'undefined' && unsafeWindow.__acnChatGPTScrollDebug;
const apiCache = chatGptApiConversationCache;
const targetMsg = apiCache && Array.isArray(apiCache.userMessages) ? apiCache.userMessages[index - 1] : null;
if (!targetMsg) {
if (debug) console.log('[ACN-GPT-USERFILE]', { stage: 'preview: no targetMsg for index', index });
return false;
}
const originalOrdinalForPreview = (() => {
const domOrdinal = extractActiveTOCIndexDOMGPT();
if (domOrdinal >= 0) return domOrdinal;
return activeMessageIndex > 0 ? activeMessageIndex - 1 : -1;
})();
if (debug) console.log('[ACN-GPT-USERFILE]', { stage: 'preview: origin ordinal captured', originalOrdinalForPreview, activeMessageIndexAtCapture: activeMessageIndex });
let previewRootCoverGPT = null;
chatGptHighlightSuppressionDepth++;
showStatusOverlayClaude('Opening preview...');
try {
const container0 = await forceRenderAndWaitForMessageIdGPT(targetMsg.id, 3000);
let container = container0;
let usedSlowCrawlForPreview = false;
if (!container) {
usedSlowCrawlForPreview = true;
previewRootCoverGPT = getConversationScrollRoot();
if (previewRootCoverGPT) showRootCoverOverlayGPT(previewRootCoverGPT);
container = await ensurePromptLoadedThenFind(index - 1, { sweepLabelPrefix: 'Opening preview and preparing quick navigation...' });
showStatusOverlayClaude('Opening preview...');
}
if (!container) {
if (debug) console.log('[ACN-GPT-USERFILE]', { stage: 'preview: message container not found/mounted', index, targetMsgId: targetMsg.id });
return false;
}
if (originalOrdinalForPreview >= 0) {
activeMessageIndex = originalOrdinalForPreview + 1;
}
const isImageFile = file && (file.sourceType === 'chatgpt-user-image' || file.ext === 'img');
let btn = null;
if (isImageFile) {
const wantedLabel = 'Open image: ' + String(file.name || '').trim();
const candidates = Array.from(container.querySelectorAll('.group\\/message-image button[aria-haspopup="dialog"]'));
btn = candidates.find(b => (b.getAttribute('aria-label') || '').trim() === wantedLabel) || null;
if (!btn && candidates.length === 1) btn = candidates[0];
}
if (!btn && file && file.name) {
btn = container.querySelector('[data-default-action="true"] button[aria-label="' + CSS.escape(file.name) + '"]');
}
if (!btn) {
if (debug) console.log('[ACN-GPT-USERFILE]', { stage: 'preview: attachment button not found', fileName: file && file.name, isImageFile });
return false;
}
btn.click();
const previewContentFetchPromise = isImageFile
? Promise.resolve()
: waitForPreviewContentFetchGPT(4000);
await waitForDialogMountedGPT(2500);
await previewContentFetchPromise;
await new Promise(resolve => setTimeout(resolve, 200));
if (usedSlowCrawlForPreview && originalOrdinalForPreview >= 0) {
await navigateToMessage(originalOrdinalForPreview + 1);
}
return true;
} finally {
hideStatusOverlayClaude();
if (previewRootCoverGPT) hideRootCoverOverlayGPT();
chatGptHighlightSuppressionDepth--;
}
}
// ============================================================================
// 07.12 No-navigation image preview (own overlay, no native dialog)
// ============================================================================
async function resolveUserImagePreviewUrlGPT(file) {
const debug = typeof unsafeWindow !== 'undefined' && unsafeWindow.__acnChatGPTScrollDebug;
if (!IS_CHATGPT || !file || !file.attachmentId) return { url: null, notFound: false };
const conversationId = getChatGPTConversationIdFromUrlAPI();
if (!conversationId) {
if (debug) console.log('[ACN-GPT-IMGPREVIEW]', { stage: 'no conversationId' });
return { url: null, notFound: false };
}
const token = await fetchChatGPTAccessTokenAPI();
const headers = token ? { Authorization: `Bearer ${token}` } : {};
const primeUrl = `/backend-api/files/${encodeURIComponent(file.attachmentId)}/simple?conversation_id=${encodeURIComponent(conversationId)}`;
try {
await fetch(primeUrl, { credentials: 'include', headers });
} catch (err) {
if (debug) console.log('[ACN-GPT-IMGPREVIEW]', { stage: 'priming fetch threw', error: String(err) });
}
const url = `/backend-api/files/download/${encodeURIComponent(file.attachmentId)}?inline=true&download_intent=false&check_context_scopes_for_conversation_id=${encodeURIComponent(conversationId)}`;
let res;
try {
res = await fetch(url, { credentials: 'include', headers });
} catch (err) {
if (debug) console.log('[ACN-GPT-IMGPREVIEW]', { stage: 'download fetch threw', error: String(err) });
return { url: null, notFound: false };
}
if (!res.ok) {
if (debug) console.log('[ACN-GPT-IMGPREVIEW]', { stage: 'download fetch not ok -- confirmed unavailable', status: res.status });
return { url: null, notFound: true };
}
const contentType = res.headers.get('content-type') || '';
if (!contentType.includes('json')) {
if (debug) console.log('[ACN-GPT-IMGPREVIEW]', { stage: 'non-JSON response -- treating as unavailable' });
return { url: null, notFound: true };
}
let data = null;
try {
data = await res.json();
} catch (err) {
if (debug) console.log('[ACN-GPT-IMGPREVIEW]', { stage: 'JSON parse threw -- treating as unavailable', error: String(err) });
return { url: null, notFound: true };
}
const realUrl = data && typeof data.download_url === 'string' ? data.download_url : null;
if (debug) console.log('[ACN-GPT-IMGPREVIEW]', { stage: 'resolved', hasUrl: Boolean(realUrl) });
if (!realUrl) return { url: null, notFound: true };
return { url: realUrl, notFound: false };
}
let acnAttachmentPreviewOverlayGPT = null;
let acnAttachmentPreviewKeydownHandlerGPT = null;
let acnAttachmentPreviewObjectUrlGPT = null;
function closeUserAttachmentPreviewOverlayGPT() {
if (acnAttachmentPreviewOverlayGPT) {
acnAttachmentPreviewOverlayGPT.remove();
acnAttachmentPreviewOverlayGPT = null;
}
if (acnAttachmentPreviewKeydownHandlerGPT) {
document.removeEventListener('keydown', acnAttachmentPreviewKeydownHandlerGPT, true);
acnAttachmentPreviewKeydownHandlerGPT = null;
}
if (acnAttachmentPreviewObjectUrlGPT) {
URL.revokeObjectURL(acnAttachmentPreviewObjectUrlGPT);
acnAttachmentPreviewObjectUrlGPT = null;
}
}
function mountAttachmentPreviewOverlayGPT(contentEl) {
closeUserAttachmentPreviewOverlayGPT();
const overlay = document.createElement('div');
overlay.id = 'acn-gpt-attachment-preview-overlay';
overlay.style.cssText = 'position: fixed; inset: 0; z-index: 10000; background-color: rgba(0,0,0,0.85); display: flex; align-items: center; justify-content: center; cursor: zoom-out;';
contentEl.addEventListener('click', (e) => e.stopPropagation());
overlay.appendChild(contentEl);
overlay.addEventListener('click', closeUserAttachmentPreviewOverlayGPT);
document.documentElement.appendChild(overlay);
acnAttachmentPreviewOverlayGPT = overlay;
acnAttachmentPreviewKeydownHandlerGPT = (e) => {
if (e.key === 'Escape') {
e.stopPropagation();
closeUserAttachmentPreviewOverlayGPT();
}
};
document.addEventListener('keydown', acnAttachmentPreviewKeydownHandlerGPT, true);
}
function openUserImagePreviewOverlayGPT(url, file) {
const img = document.createElement('img');
img.src = url;
img.alt = (file && file.name) || 'Image';
img.style.cssText = 'max-width: 90vw; max-height: 90vh; object-fit: contain; cursor: default; box-shadow: 0 4px 32px rgba(0,0,0,0.5); border-radius: 8px;';
mountAttachmentPreviewOverlayGPT(img);
}
async function openUserImagePreviewNoNavGPT(file) {
if (!file || !file.attachmentId) return { ok: false, notFound: false };
const resolved = await resolveUserImagePreviewUrlGPT(file);
if (!resolved.url) return { ok: false, notFound: resolved.notFound };
openUserImagePreviewOverlayGPT(resolved.url, file);
return { ok: true, notFound: false };
}
async function resolveUserAttachmentBlobGPT(file) {
const debug = typeof unsafeWindow !== 'undefined' && unsafeWindow.__acnChatGPTScrollDebug;
if (!IS_CHATGPT || !file || !file.attachmentId) return { blob: null, filename: null, notFound: false };
const conversationId = getChatGPTConversationIdFromUrlAPI();
if (!conversationId) {
if (debug) console.log('[ACN-GPT-ATTACHPREVIEW]', { stage: 'no conversationId' });
return { blob: null, filename: null, notFound: false };
}
const token = await fetchChatGPTAccessTokenAPI();
const headers = token ? { Authorization: `Bearer ${token}` } : {};
const primeUrl = `/backend-api/files/${encodeURIComponent(file.attachmentId)}/simple?conversation_id=${encodeURIComponent(conversationId)}`;
try {
await fetch(primeUrl, { credentials: 'include', headers });
} catch (err) {
if (debug) console.log('[ACN-GPT-ATTACHPREVIEW]', { stage: 'priming fetch threw', error: String(err) });
}
const dlUrl = `/backend-api/files/download/${encodeURIComponent(file.attachmentId)}?conversation_id=${encodeURIComponent(conversationId)}&download_intent=true`;
let res;
try {
res = await fetch(dlUrl, { credentials: 'include', headers });
} catch (err) {
if (debug) console.log('[ACN-GPT-ATTACHPREVIEW]', { stage: 'download fetch threw', error: String(err) });
return { blob: null, filename: null, notFound: false };
}
if (!res.ok) {
if (debug) console.log('[ACN-GPT-ATTACHPREVIEW]', { stage: 'download fetch not ok -- confirmed unavailable', status: res.status });
return { blob: null, filename: null, notFound: true };
}
const contentType = res.headers.get('content-type') || '';
if (contentType.includes('json')) {
let data = null;
try {
data = await res.json();
} catch (err) {
if (debug) console.log('[ACN-GPT-ATTACHPREVIEW]', { stage: 'JSON parse threw -- treating as unavailable', error: String(err) });
return { blob: null, filename: null, notFound: true };
}
const realUrl = data && typeof data.download_url === 'string' ? data.download_url : null;
if (!realUrl) {
if (debug) console.log('[ACN-GPT-ATTACHPREVIEW]', { stage: 'no download_url in envelope -- treating as unavailable' });
return { blob: null, filename: null, notFound: true };
}
let bytesRes;
try {
bytesRes = await fetch(realUrl, { credentials: 'include', headers });
} catch (err) {
if (debug) console.log('[ACN-GPT-ATTACHPREVIEW]', { stage: 'bytes fetch threw', error: String(err) });
return { blob: null, filename: null, notFound: false };
}
if (!bytesRes.ok) {
if (debug) console.log('[ACN-GPT-ATTACHPREVIEW]', { stage: 'bytes fetch not ok -- treating as unavailable', status: bytesRes.status });
return { blob: null, filename: null, notFound: true };
}
const blob = await bytesRes.blob();
if (debug) console.log('[ACN-GPT-ATTACHPREVIEW]', { stage: 'resolved via envelope', size: blob.size, type: blob.type });
return { blob, filename: file.name || (data && data.file_name) || 'file', notFound: false };
}
const blob = await res.blob();
if (debug) console.log('[ACN-GPT-ATTACHPREVIEW]', { stage: 'resolved via raw response', size: blob.size, type: blob.type });
return { blob, filename: file.name || 'file', notFound: false };
}
const PLAIN_TEXT_PREVIEW_EXTENSIONS_GPT = new Set(['txt', 'csv', 'tsv', 'json', 'xml', 'yaml', 'yml', 'md']);
function buildPlainTextPreviewContentGPT(text, file) {
const wrapper = document.createElement('div');
wrapper.style.cssText = 'background: var(--acn-nav-bg-solid, Canvas); color: var(--acn-nav-text, CanvasText); border-radius: 8px; padding: 20px; max-width: 90vw; max-height: 85vh; overflow: auto; box-sizing: border-box;';
const pre = document.createElement('pre');
pre.style.cssText = 'margin: 0; white-space: pre-wrap; word-break: break-word; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px; line-height: 1.6; max-width: 800px;';
pre.textContent = text;
wrapper.appendChild(pre);
return wrapper;
}
function isChatGPTAttachmentPreviewSupportedGPT(file) {
if (!IS_CHATGPT || !file) return false;
if (file.sourceType === 'chatgpt-user-image' || file.ext === 'img') return true;
if (file.mimeType === 'application/pdf') return true;
const ext = String(file.ext || '').toLowerCase();
return PLAIN_TEXT_PREVIEW_EXTENSIONS_GPT.has(ext);
}
function buildAttachmentPreviewContentGPT(objectUrl, blob, file) {
const panel = document.createElement('div');
panel.style.cssText = 'background: var(--acn-nav-bg-solid, Canvas); color: var(--acn-nav-text, CanvasText); border-radius: 8px; padding: 32px 40px; max-width: 80vw; text-align: center; font: 14px system-ui, sans-serif;';
const label = document.createElement('div');
label.textContent = ((file && file.name) || 'File') + ' \u2014 no inline preview for this file type.';
label.style.cssText = 'margin-bottom: 16px;';
panel.appendChild(label);
const downloadBtn = document.createElement('button');
downloadBtn.type = 'button';
downloadBtn.textContent = 'Download';
downloadBtn.style.cssText = 'padding: 8px 20px; border-radius: 6px; border: none; background: #10a37f; color: white; font-weight: 600; cursor: pointer;';
downloadBtn.addEventListener('click', () => {
const a = document.createElement('a');
a.href = objectUrl;
a.download = (file && file.name) || 'file';
a.rel = 'noopener';
document.body.appendChild(a);
a.click();
a.remove();
});
panel.appendChild(downloadBtn);
return panel;
}
async function openUserAttachmentPreviewNoNavGPT(file) {
if (!file || !file.attachmentId) return { ok: false, notFound: false };
const mime = (file && file.mimeType) || '';
if (mime === 'application/pdf') {
const resolved = await resolveUserImagePreviewUrlGPT(file);
if (!resolved.url) return { ok: false, notFound: resolved.notFound };
const iframe = document.createElement('iframe');
iframe.src = resolved.url;
iframe.title = file.name || 'PDF preview';
iframe.style.cssText = 'width: 90vw; height: 90vh; border: none; border-radius: 8px; background: white;';
mountAttachmentPreviewOverlayGPT(iframe);
return { ok: true, notFound: false };
}
const resolved = await resolveUserAttachmentBlobGPT(file);
if (!resolved.blob) return { ok: false, notFound: resolved.notFound };
const ext = String(file.ext || '').toLowerCase();
if (PLAIN_TEXT_PREVIEW_EXTENSIONS_GPT.has(ext)) {
let text;
try {
text = await resolved.blob.text();
} catch (err) {
text = '(could not read file as text)';
}
const contentEl = buildPlainTextPreviewContentGPT(text, file);
mountAttachmentPreviewOverlayGPT(contentEl);
return { ok: true, notFound: false };
}
const objectUrl = URL.createObjectURL(resolved.blob);
const contentEl = buildAttachmentPreviewContentGPT(objectUrl, resolved.blob, file);
mountAttachmentPreviewOverlayGPT(contentEl);
acnAttachmentPreviewObjectUrlGPT = objectUrl;
return { ok: true, notFound: false };
}
async function downloadUserFileGPT(file) {
const debug = typeof unsafeWindow !== 'undefined' && unsafeWindow.__acnChatGPTScrollDebug;
if (!IS_CHATGPT || !file || !file.attachmentId) {
if (debug) console.log('[ACN-GPT-USERFILE]', { stage: 'downloadUserFileGPT early return', IS_CHATGPT, hasFile: Boolean(file), attachmentId: file && file.attachmentId });
return { downloaded: false, notFound: false };
}
const conversationId = getChatGPTConversationIdFromUrlAPI();
if (!conversationId) {
if (debug) console.log('[ACN-GPT-USERFILE]', { stage: 'no conversationId' });
return { downloaded: false, notFound: false };
}
const token = await fetchChatGPTAccessTokenAPI();
const headers = token ? { Authorization: `Bearer ${token}` } : {};
if (debug) console.log('[ACN-GPT-USERFILE]', { stage: 'token fetched', hasToken: Boolean(token) });
const primeUrl = `/backend-api/files/${encodeURIComponent(file.attachmentId)}/simple?conversation_id=${encodeURIComponent(conversationId)}`;
try {
const primeRes = await fetch(primeUrl, { credentials: 'include', headers });
if (debug) console.log('[ACN-GPT-USERFILE]', { stage: 'priming fetch response', url: primeUrl, status: primeRes.status, ok: primeRes.ok });
} catch (err) {
if (debug) console.log('[ACN-GPT-USERFILE]', { stage: 'priming fetch threw', error: String(err) });
}
const url = `/backend-api/files/download/${encodeURIComponent(file.attachmentId)}?conversation_id=${encodeURIComponent(conversationId)}&download_intent=true`;
if (debug) console.log('[ACN-GPT-USERFILE]', { stage: 'fetching', url });
let res;
try {
res = await fetch(url, { credentials: 'include', headers });
} catch (err) {
if (debug) console.log('[ACN-GPT-USERFILE]', { stage: 'download fetch threw', error: String(err) });
return { downloaded: false, notFound: false };
}
if (debug) console.log('[ACN-GPT-USERFILE]', { stage: 'download fetch response', status: res.status, ok: res.ok, contentType: res.headers.get('content-type') });
if (!res.ok) return { downloaded: false, notFound: true };
const contentType = res.headers.get('content-type') || '';
if (contentType.includes('json')) {
let data = null;
try {
data = await res.json();
} catch (err) {
if (debug) console.log('[ACN-GPT-USERFILE]', { stage: 'JSON parse threw', error: String(err) });
return { downloaded: false, notFound: true };
}
if (debug) console.log('[ACN-GPT-USERFILE]', { stage: 'download response is JSON', data });
const realUrl = data && typeof data.download_url === 'string' ? data.download_url : null;
if (!realUrl) {
if (debug) console.log('[ACN-GPT-USERFILE]', { stage: 'no download_url field found in JSON envelope' });
return { downloaded: false, notFound: true };
}
if (debug) console.log('[ACN-GPT-USERFILE]', { stage: 'following download_url', realUrl });
const ok = await downloadBlobUrlAsFile(realUrl, file.name || (data && data.file_name) || 'file', headers);
return { downloaded: Boolean(ok), notFound: false };
}
const blob = await res.blob();
if (debug) console.log('[ACN-GPT-USERFILE]', { stage: 'non-JSON blob received', size: blob.size, type: blob.type });
const objectUrl = URL.createObjectURL(blob);
try {
const a = document.createElement('a');
a.href = objectUrl;
a.download = file.name || 'file';
a.rel = 'noopener';
document.body.appendChild(a);
a.click();
a.remove();
} finally {
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 1500);
}
return { downloaded: true, notFound: false };
}
async function resolveGeneratedFileBlobGPT(file) {
const debug = typeof unsafeWindow !== 'undefined' && unsafeWindow.__acnChatGPTScrollDebug;
if (!IS_CHATGPT || !file || !file.downloadMessageId || !file.sandboxPath) {
if (debug) console.log('[ACN-GPT-GENFILE]', { stage: 'resolveGeneratedFileBlobGPT early return', IS_CHATGPT, hasFile: Boolean(file), downloadMessageId: file && file.downloadMessageId, sandboxPath: file && file.sandboxPath });
return { blob: null, notFound: false };
}
const conversationId = getChatGPTConversationIdFromUrlAPI();
if (!conversationId) {
if (debug) console.log('[ACN-GPT-GENFILE]', { stage: 'no conversationId' });
return { blob: null, notFound: false };
}
const token = await fetchChatGPTAccessTokenAPI();
const headers = token ? { Authorization: `Bearer ${token}` } : {};
const url = `/backend-api/conversation/${encodeURIComponent(conversationId)}/interpreter/download?message_id=${encodeURIComponent(file.downloadMessageId)}&sandbox_path=${encodeURIComponent(file.sandboxPath)}`;
if (debug) console.log('[ACN-GPT-GENFILE]', { stage: 'fetching', url });
let res;
try {
res = await fetch(url, { credentials: 'include', headers });
} catch (err) {
if (debug) console.log('[ACN-GPT-GENFILE]', { stage: 'fetch threw', error: String(err) });
return { blob: null, notFound: false };
}
if (debug) console.log('[ACN-GPT-GENFILE]', { stage: 'fetch response', status: res.status, ok: res.ok, contentType: res.headers.get('content-type') });
if (!res.ok) return { blob: null, notFound: true };
const contentType = res.headers.get('content-type') || '';
if (contentType.includes('json')) {
let data = null;
try {
data = await res.json();
} catch (err) {
if (debug) console.log('[ACN-GPT-GENFILE]', { stage: 'JSON parse threw', error: String(err) });
return { blob: null, notFound: true };
}
if (debug) console.log('[ACN-GPT-GENFILE]', { stage: 'response is JSON', data });
const realUrl = data && typeof data.download_url === 'string' ? data.download_url : null;
if (!realUrl) {
if (debug) console.log('[ACN-GPT-GENFILE]', { stage: 'no download_url field in JSON envelope' });
return { blob: null, notFound: true };
}
let bytesRes;
try {
bytesRes = await fetch(realUrl, { credentials: 'include', headers });
} catch (err) {
if (debug) console.log('[ACN-GPT-GENFILE]', { stage: 'bytes fetch threw', error: String(err) });
return { blob: null, notFound: false };
}
if (!bytesRes.ok) return { blob: null, notFound: true };
const blob = await bytesRes.blob();
if (debug) console.log('[ACN-GPT-GENFILE]', { stage: 'resolved via envelope', size: blob.size, type: blob.type });
return { blob, notFound: false };
}
const blob = await res.blob();
if (debug) console.log('[ACN-GPT-GENFILE]', { stage: 'non-JSON blob received', size: blob.size, type: blob.type });
return { blob, notFound: false };
}
async function downloadGeneratedFileGPT(file) {
const debug = typeof unsafeWindow !== 'undefined' && unsafeWindow.__acnChatGPTScrollDebug;
const resolved = await resolveGeneratedFileBlobGPT(file);
if (!resolved.blob) return { downloaded: false, notFound: resolved.notFound };
const displayName = (file && (file.displayName || file.name)) || 'file';
const objectUrl = URL.createObjectURL(resolved.blob);
try {
const a = document.createElement('a');
a.href = objectUrl;
a.download = displayName;
a.rel = 'noopener';
document.body.appendChild(a);
a.click();
a.remove();
} finally {
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 1500);
}
if (debug) console.log('[ACN-GPT-GENFILE]', { stage: 'download triggered', displayName });
return { downloaded: true, notFound: false };
}
function extractGeneratedFilesFromTurnFiberGPT(anchorEl) {
if (!(anchorEl instanceof HTMLElement)) return [];
const fiberKey = Object.keys(anchorEl).find(k => k.startsWith('__reactFiber$'));
if (!fiberKey) return [];
let fiber = anchorEl[fiberKey];
let depth = 0;
let turn = null;
while (fiber && depth < 15 && !turn) {
const props = fiber.memoizedProps;
if (props && props.turn && typeof props.turn === 'object' && Array.isArray(props.turn.messages)) {
turn = props.turn;
}
fiber = fiber.return;
depth++;
}
if (!turn) return [];
const files = [];
const pathPattern = /\/mnt\/data\/([^\s"'\n]+\.[a-zA-Z0-9]{1,10})/g;
turn.messages.forEach(m => {
const isToolResult = m && m.author && m.author.role === 'tool'
&& m.content && m.content.content_type === 'execution_output'
&& typeof m.content.text === 'string';
if (!isToolResult) return;
let match;
pathPattern.lastIndex = 0;
while ((match = pathPattern.exec(m.content.text))) {
const name = match[1];
const extMatch = name.match(/\.([a-zA-Z0-9]{1,10})$/);
files.push({
name,
ext: extMatch ? extMatch[1].toLowerCase() : 'file',
kind: 'chatgpt-generated-file',
aiMessageTurnId: turn.id || null,
toolMessageId: m.id || null,
codeMessageId: turn.id || null
});
}
});
return files;
}
// ============================================================================
// 07.11 Click-to-download support
// ============================================================================
function findGeneratedFileTargetGPT(index, fileIdx, file) {
const debug = typeof unsafeWindow !== 'undefined' && unsafeWindow.__acnChatGPTScrollDebug;
const promptIndex = Math.max(0, index - 1);
if (debug && file && file.kind === 'chatgpt-writing-block') {
console.log('[ACN-GPT-WRITEBLOCK]', { stage: 'findGeneratedFileTargetGPT called', index, fileIdx, promptIndex, file });
}
const aiConfig = CURRENT_SITE.aiMessage || {};
if (file && file.kind === 'chatgpt-writing-block' && aiConfig.writingBlock) {
const byId = file.aiMessageTurnId ? findMountedElementByMessageIdGPT(file.aiMessageTurnId) : null;
if (debug) console.log('[ACN-GPT-WRITEBLOCK]', { stage: 'exact-id resolution', aiMessageTurnId: file.aiMessageTurnId, found: Boolean(byId) });
if (!byId) {
if (debug) console.log('[ACN-GPT-WRITEBLOCK]', { stage: 'exact-id element not mounted, giving up (no fragile fallback)' });
return null;
}
const writingBlocks = extractWritingBlocksAIMessageDOMGPT(byId, aiConfig);
const ordinal = Number.isFinite(file.ordinal) ? file.ordinal : fileIdx;
const chosen = writingBlocks[Number.isFinite(ordinal) ? ordinal : 0] || writingBlocks[0] || null;
if (debug) console.log('[ACN-GPT-WRITEBLOCK]', { stage: 'writing block lookup', ordinal, writingBlocksFound: writingBlocks.length, chosenFound: Boolean(chosen) });
if (!chosen) return null;
const target = aiConfig.writingBlockTarget
? (chosen.closest(aiConfig.writingBlockTarget) || chosen)
: chosen;
return { target, actionElement: null, isImage: false, isWritingBlock: true };
}
const detail = extractVisibleMessageForTOCIndexDOMGPT(promptIndex);
let userElement = detail && detail.element && detail.element.isConnected ? detail.element : null;
if (!userElement) {
const cached = promptCacheItems[promptIndex];
userElement = cached && cached.element && cached.element.isConnected && !cached.element.__acnNativeToc
? cached.element
: null;
}
if (!userElement) return null;
const aiMessageEl = extractAIMessageDOMGPT(userElement);
if (!aiMessageEl) return null;
if (file && file.kind === 'chatgpt-generated-image' && aiConfig.image) {
const images = Array.from(aiMessageEl.querySelectorAll(aiConfig.image)).filter(img => img instanceof HTMLImageElement);
let chosenImage = null;
const wantedKey = String(file.imageKey || '');
if (wantedKey) chosenImage = images.find(img => extractGeneratedImageKeyDOMGPT(img) === wantedKey) || null;
if (!chosenImage && file.url) {
let wantedId = '';
try { wantedId = new URL(file.url, window.location.href).searchParams.get('id') || ''; } catch (_) {}
if (wantedId) chosenImage = images.find(img => {
try {
const src = img.currentSrc || img.src || img.getAttribute('src') || '';
return new URL(src, window.location.href).searchParams.get('id') === wantedId;
} catch (_) { return false; }
}) || null;
}
if (!chosenImage) {
const unique = [];
const keys = new Set();
images.forEach(img => {
const key = extractGeneratedImageKeyDOMGPT(img);
if (!key || keys.has(key)) return;
keys.add(key);
unique.push(img);
});
chosenImage = unique[Number.isFinite(fileIdx) ? fileIdx : 0] || unique[0] || null;
}
if (!chosenImage) return null;
const target = aiConfig.imageTarget
? (chosenImage.closest(aiConfig.imageTarget) || chosenImage.parentElement || chosenImage)
: chosenImage;
return { target, actionElement: chosenImage, isImage: true };
}
if (!aiConfig.file) return null;
const elements = Array.from(aiMessageEl.querySelectorAll(aiConfig.file));
if (!elements.length) {
if (!file || !file.name) return null;
const candidates = Array.from(aiMessageEl.querySelectorAll('button, a')).filter(el => {
if (el.getAttribute('aria-label')) return false; // real file-card buttons have one; this path is for the ones that don't
const text = String(el.textContent || '').trim();
return text === file.name || text === file.displayName;
});
const inlineChosen = candidates[0] || null;
if (!inlineChosen) return null;
return { target: inlineChosen, actionElement: inlineChosen, isImage: false, isInlineFallback: true };
}
let chosen = null;
if (file) {
chosen = elements.find(el => {
const actualName = extractGeneratedFileNameDOMGPT(el, aiMessageEl);
if (!actualName) return false;
return [file.name, file.displayName].filter(Boolean).some(n => actualName.includes(n));
}) || null;
}
if (!chosen && Number.isFinite(fileIdx)) chosen = elements[fileIdx] || null;
chosen = chosen || elements[0] || null;
if (!chosen) return null;
const target = aiConfig.fileTarget
? (chosen.closest(aiConfig.fileTarget) || chosen.parentElement || chosen)
: chosen;
return { target, actionElement: chosen, isImage: false };
}
async function focusGeneratedFileGPT(index, fileIdx, file) {
const skipFreeze = typeof unsafeWindow !== 'undefined' && unsafeWindow.__acnChatGPTSkipDownloadFreeze;
const originalOrdinal = extractActiveTOCIndexDOMGPT();
const root = getConversationScrollRoot();
if (root && !skipFreeze) showRootCoverOverlayGPT(root);
if (!skipFreeze) showStatusOverlayClaude(file && file.kind === 'chatgpt-writing-block' ? 'Opening writing block...' : 'Downloading file...');
try {
if (file && file.codeMessageId && file.kind !== 'chatgpt-generated-image') {
await navigateToMessage(index);
const mounted = await forceRenderAndWaitForMessageIdGPT(file.codeMessageId, 2000);
if (mounted) {
mounted.scrollIntoView({ block: 'center', behavior: 'auto' });
await new Promise(resolve => setTimeout(resolve, 300));
} else {
await scrollToHarvestedMessageIdGPT(file.codeMessageId, 4000);
}
}
if (file && file.kind === 'chatgpt-writing-block' && file.aiMessageTurnId) {
await navigateToMessage(index);
await forceRenderAndWaitForMessageIdGPT(file.aiMessageTurnId, 2000);
}
let result = false;
const maxAttempts = 3;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
result = await activateDomGeneratedFile({
findTarget: () => findGeneratedFileTargetGPT(index, fileIdx, file),
navigate: () => navigateToMessage(index),
attempts: 12,
earlyDelay: 120,
laterDelay: 220,
performAction: async ({ actionElement, isImage, isWritingBlock, isInlineFallback }) => {
if (isWritingBlock) return;
if (!actionElement || typeof actionElement.click !== 'function') return;
if (isImage) {
const clickable = actionElement.closest('button, [role="button"], .cursor-pointer') || actionElement;
if (clickable && typeof clickable.click === 'function') clickable.click();
await new Promise(resolve => setTimeout(resolve, 400));
const dialog = document.querySelector('[role="dialog"]');
const saveBtn = dialog && dialog.querySelector('button[aria-label="Save"]');
if (saveBtn && typeof saveBtn.click === 'function') saveBtn.click();
await new Promise(resolve => setTimeout(resolve, 500));
const closeBtn = dialog && dialog.querySelector('button[aria-label="Close fullscreen view"]');
if (closeBtn && typeof closeBtn.click === 'function') closeBtn.click();
return;
}
actionElement.click();
if (isInlineFallback) {
await new Promise(resolve => setTimeout(resolve, 400));
const downloadEl = Array.from(document.querySelectorAll('button[aria-label*="download" i], a[download], a[aria-label*="download" i]'))
.find(el => el.offsetParent !== null); // visible only
if (downloadEl && typeof downloadEl.click === 'function') downloadEl.click();
} else {
await new Promise(resolve => setTimeout(resolve, 500));
}
}
});
if (!result) break; // no target found at all; retrying won't help
const failed = await waitForChatGPTUploadStatusFailureToastGPT(file && (file.name || file.displayName), 1200);
if (!failed) break;
if (attempt < maxAttempts - 1) await new Promise(resolve => setTimeout(resolve, 1200));
}
if (originalOrdinal >= 0 && (!file || file.kind !== 'chatgpt-writing-block')) {
await navigateToMessage(originalOrdinal + 1);
}
return result;
} finally {
if (!skipFreeze) hideRootCoverOverlayGPT();
if (!skipFreeze) hideStatusOverlayClaude();
}
}
function waitForChatGPTUploadStatusFailureToastGPT(fileName, timeoutMs = 1200) {
return new Promise(resolve => {
let settled = false;
const finish = (val) => {
if (settled) return;
settled = true;
observer.disconnect();
clearTimeout(timeoutId);
resolve(val);
};
const check = () => {
const toasts = document.querySelectorAll('.toast-root');
for (const toast of toasts) {
const text = toast.textContent || '';
if (/failed to get upload status/i.test(text) && (!fileName || text.includes(fileName))) {
finish(true);
return;
}
}
};
const observer = new MutationObserver(check);
observer.observe(document.body, { childList: true, subtree: true });
const timeoutId = setTimeout(() => finish(false), timeoutMs);
check();
});
}
function hideRootCoverOverlayGPT() {
chatGptRootCoverDepth = Math.max(0, chatGptRootCoverDepth - 1);
if (chatGptRootCoverDepth > 0) return;
if (chatGptRootCoverSyncTimer) {
chatGptRootCoverSyncTimer.clear();
chatGptRootCoverSyncTimer = null;
}
const overlay = document.getElementById('acn-gpt-root-cover');
if (overlay) overlay.remove();
}
function waitForBadgeChurnQuietGPT(root, quietMs = 500, maxWaitMs = 6000) {
return new Promise(resolve => {
if (!(root instanceof HTMLElement)) { resolve(); return; }
const start = performance.now();
let lastActivity = start;
const isBadge = (node) => node instanceof HTMLElement && node.classList && node.classList.contains('acn-message-inline-badge');
const observer = new MutationObserver((mutations) => {
const relevant = mutations.some(m => m.type === 'childList'
&& (Array.from(m.addedNodes).some(isBadge) || Array.from(m.removedNodes).some(isBadge)));
if (relevant) lastActivity = performance.now();
});
observer.observe(root, { childList: true, subtree: true });
const check = () => {
const now = performance.now();
if (now - lastActivity >= quietMs || now - start >= maxWaitMs) {
observer.disconnect();
resolve();
return;
}
requestAnimationFrame(check);
};
requestAnimationFrame(check);
});
}
function showRootCoverOverlayGPT(root) {
chatGptRootCoverDepth++;
if (!(root instanceof HTMLElement)) return null;
let overlay = document.getElementById('acn-gpt-root-cover');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'acn-gpt-root-cover';
overlay.style.cssText = 'position: fixed; background-color: var(--acn-nav-bg-solid, Canvas); z-index: 9998; pointer-events: none;';
document.documentElement.appendChild(overlay);
}
const sync = () => {
const liveRoot = getConversationScrollRoot() || root;
const rect = liveRoot.getBoundingClientRect();
overlay.style.top = rect.top + 'px';
overlay.style.left = rect.left + 'px';
overlay.style.width = rect.width + 'px';
overlay.style.height = rect.height + 'px';
};
sync();
if (chatGptRootCoverSyncTimer) chatGptRootCoverSyncTimer.clear();
chatGptRootCoverSyncTimer = safeSetInterval(sync, 300);
return overlay;
}
function setCssStripChatGPT(root, enabled) {
if (!root) return;
if (enabled) root.setAttribute('data-acn-gpt-strip', 'true');
else root.removeAttribute('data-acn-gpt-strip');
}
if (IS_CHATGPT) currentFileAdapter = CHATGPT_FILE_ADAPTER;
// ============================================================================
// 08. SITE-SPECIFIC FILE ADAPTERS
// ============================================================================
// 08.1 Gemini adapter: file data and file actions
// ---------------------------------------------------------------------------
const GEMINI_FILE_ADAPTER = {
async activateUserFile(file, index, fileIdx) {
return navigateToMessage(index);
},
async activateGeneratedFile(file, index, fileIdx) {
return focusGeneratedFileGemini(index, fileIdx, file);
}
};
function getBadgeUserMessageDataGemini(userElement) {
return { files: extractUserFileBadgeDOMByConfig(userElement, CURRENT_SITE.userMessage) };
}
function getFileBadgeAIMessageDataGemini(userElement) {
return { generatedFiles: extractGeneratedFilesDOMGemini(userElement).filter(file => file && file.kind !== 'gemini-generated-image') };
}
function getImageBadgeAIMessageDataGemini(userElement) {
return { generatedFiles: extractGeneratedFilesDOMGemini(userElement).filter(file => file && file.kind === 'gemini-generated-image') };
}
function extractAIMessageDOMGemini(userElement) {
if (!IS_GEMINI || !(userElement instanceof HTMLElement)) return null;
const aiConfig = CURRENT_SITE.aiMessage;
if (!aiConfig || !aiConfig.relationRoot || !aiConfig.userTurn || !aiConfig.container) return null;
const conversation = userElement.closest(aiConfig.relationRoot);
if (conversation) {
const response = conversation.querySelector(aiConfig.container);
if (response) return response;
}
let node = userElement.closest(aiConfig.userTurn) || userElement;
for (let depth = 0; node && node !== document.body && depth < 6; depth++, node = node.parentElement) {
for (let sibling = node.nextElementSibling, guard = 0; sibling && guard < 6; sibling = sibling.nextElementSibling, guard++) {
if (sibling.matches && sibling.matches(aiConfig.container)) return sibling;
const nested = sibling.querySelector ? sibling.querySelector(aiConfig.container) : null;
if (nested) return nested;
if ((sibling.matches && sibling.matches(aiConfig.userTurn)) || (sibling.querySelector && sibling.querySelector(aiConfig.userTurn))) break;
}
}
return null;
}
function extractGeneratedFilesDOMGemini(userElement) {
if (!IS_GEMINI || !(userElement instanceof HTMLElement)) return [];
const aiConfig = CURRENT_SITE.aiMessage;
if (!aiConfig) return [];
const aiMessageEl = extractAIMessageDOMGemini(userElement);
if (!aiMessageEl) return [];
const generatedFiles = [];
if (aiConfig.file) {
Array.from(aiMessageEl.querySelectorAll(aiConfig.file)).forEach((fileEl, ordinal) => {
const nameEl = aiConfig.fileName ? fileEl.querySelector(aiConfig.fileName) : null;
const typeEl = aiConfig.fileType ? fileEl.querySelector(aiConfig.fileType) : null;
const name = (nameEl && nameEl.textContent ? nameEl.textContent : '').trim() || `file-${ordinal + 1}`;
const typeText = (typeEl && typeEl.textContent ? typeEl.textContent : '').trim().toLowerCase();
const extMatch = name.match(/\.([a-z0-9]{1,10})$/i);
const ext = extMatch ? extMatch[1].toLowerCase() : (typeText || 'file');
const displayName = extMatch || !ext || ext === 'file' ? name : `${name}.${ext}`;
generatedFiles.push({ name, displayName, ext, kind: 'gemini-generated-file', ordinal });
});
}
if (aiConfig.image) {
Array.from(aiMessageEl.querySelectorAll(aiConfig.image)).forEach((imageEl, ordinal) => {
const preview = aiConfig.imagePreview ? imageEl.querySelector(aiConfig.imagePreview) : null;
const attachmentIndex = imageEl.getAttribute('data-image-attachment-index');
const imageNumber = /^\d+$/.test(String(attachmentIndex || '')) ? Number(attachmentIndex) + 1 : ordinal + 1;
const name = `image-${imageNumber}`;
generatedFiles.push({
name,
displayName: name,
ext: 'img',
kind: 'gemini-generated-image',
ordinal,
attachmentIndex: attachmentIndex || '',
previewSrc: preview && preview.getAttribute ? (preview.getAttribute('src') || '') : ''
});
});
}
if (aiConfig.markdownBlock && aiConfig.markdownLanguage) {
const wantedLanguage = String(aiConfig.markdownLanguageValue || 'markdown').trim().toLowerCase();
Array.from(aiMessageEl.querySelectorAll(aiConfig.markdownBlock)).forEach((blockEl, ordinal) => {
const languageEl = blockEl.querySelector(aiConfig.markdownLanguage);
const language = String(languageEl && languageEl.textContent || '').trim().toLowerCase();
if (language !== wantedLanguage) return;
generatedFiles.push({
name: `markdown-${ordinal + 1}.md`,
displayName: `Markdown ${ordinal + 1}`,
ext: 'md',
kind: 'gemini-markdown',
ordinal
});
});
}
return generatedFiles;
}
function extractGeneratedFileNameDOMGemini(element) {
if (!(element instanceof HTMLElement)) return '';
const aiConfig = CURRENT_SITE.aiMessage || {};
if (aiConfig.fileName) {
const nameEl = element.matches(aiConfig.fileName) ? element : element.querySelector(aiConfig.fileName);
if (nameEl && nameEl.textContent) return nameEl.textContent.trim();
}
return (element.textContent || '').trim();
}
function findGeneratedImageTargetGemini(aiMessageEl, aiConfig, file) {
if (!aiMessageEl || !aiConfig.image) return null;
const images = Array.from(aiMessageEl.querySelectorAll(aiConfig.image));
if (!images.length) return null;
let chosen = null;
if (file && file.attachmentIndex !== undefined && file.attachmentIndex !== null && file.attachmentIndex !== '') {
chosen = images.find(el => el.getAttribute('data-image-attachment-index') === String(file.attachmentIndex)) || null;
}
if (!chosen && file && Number.isFinite(file.ordinal)) chosen = images[file.ordinal] || null;
chosen = chosen || images[0] || null;
if (!chosen) return null;
const actionElement = aiConfig.imageDownload ? chosen.querySelector(aiConfig.imageDownload) : null;
const target = aiConfig.imageTarget
? (chosen.matches(aiConfig.imageTarget) ? chosen : chosen.closest(aiConfig.imageTarget) || chosen.querySelector(aiConfig.imageTarget) || chosen)
: chosen;
return { target, actionElement, isImage: true };
}
function findGeneratedFileTargetGemini(index, fileIdx, file) {
const userElement = findPromptElementByIndex(Math.max(0, index - 1));
if (!(userElement instanceof HTMLElement)) return null;
const aiMessageEl = extractAIMessageDOMGemini(userElement);
const aiConfig = CURRENT_SITE.aiMessage || {};
if (!aiMessageEl) return null;
if (file && file.kind === 'gemini-generated-image') {
return findGeneratedImageTargetGemini(aiMessageEl, aiConfig, file);
}
if (file && file.kind === 'gemini-markdown') {
if (!aiConfig.markdownBlock || !aiConfig.markdownLanguage) return null;
const wantedLanguage = String(aiConfig.markdownLanguageValue || 'markdown').trim().toLowerCase();
const blocks = Array.from(aiMessageEl.querySelectorAll(aiConfig.markdownBlock)).filter(blockEl => {
const languageEl = blockEl.querySelector(aiConfig.markdownLanguage);
return String(languageEl && languageEl.textContent || '').trim().toLowerCase() === wantedLanguage;
});
let chosen = Number.isFinite(file.ordinal) ? blocks[file.ordinal] || null : null;
if (!chosen && Number.isFinite(fileIdx)) chosen = blocks[fileIdx] || null;
chosen = chosen || blocks[0] || null;
if (!chosen) return null;
const actionElement = aiConfig.markdownDownload ? chosen.querySelector(aiConfig.markdownDownload) : null;
const target = aiConfig.markdownTarget
? (chosen.matches(aiConfig.markdownTarget) ? chosen : chosen.closest(aiConfig.markdownTarget) || chosen.querySelector(aiConfig.markdownTarget) || chosen)
: chosen;
return { target, actionElement, isImage: false };
}
if (!aiConfig.file) return null;
const elements = Array.from(aiMessageEl.querySelectorAll(aiConfig.file));
if (!elements.length) return null;
let chosen = file ? elements.find(el => {
const actualName = extractGeneratedFileNameDOMGemini(el);
return [file.name, file.displayName].filter(Boolean).includes(actualName);
}) || null : null;
if (!chosen && file && Number.isFinite(file.ordinal)) chosen = elements[file.ordinal] || null;
if (!chosen && Number.isFinite(fileIdx)) chosen = elements[fileIdx] || null;
chosen = chosen || elements[0] || null;
if (!chosen) return null;
const actionElement = aiConfig.fileDownload ? chosen.querySelector(aiConfig.fileDownload) : null;
return { target: chosen, actionElement, isImage: false };
}
async function focusGeneratedFileGemini(index, fileIdx, file) {
return activateDomGeneratedFile({
findTarget: () => findGeneratedFileTargetGemini(index, fileIdx, file),
navigate: () => navigateToMessage(index),
attempts: 10,
earlyDelay: 100,
laterDelay: 180,
performAction: ({ actionElement }) => {
if (actionElement && typeof actionElement.click === 'function') actionElement.click();
}
});
}
if (IS_GEMINI) currentFileAdapter = GEMINI_FILE_ADAPTER;
// 08.2 Kimi adapter: file data and file actions
// ---------------------------------------------------------------------------
const KIMI_FILE_ADAPTER = {
async activateUserFile(file, index, fileIdx) {
return navigateToMessage(index);
},
async activateGeneratedFile(file, index, fileIdx) {
return focusGeneratedFileKimi(index, fileIdx, file);
}
};
function getVersionControlDataKimi(container) {
if (!(container instanceof Element) || !CURRENT_SITE.versionControl) return null;
return extractVersionControlDOM(container, CURRENT_SITE.versionControl);
}
function getBadgeUserMessageDataKimi(userElement) {
return { files: extractUserFileBadgeDOMByConfig(userElement, CURRENT_SITE.userMessage) };
}
function getFileBadgeAIMessageDataKimi(userElement) {
return { generatedFiles: extractGeneratedFilesDOMKimi(userElement) };
}
function extractAIMessageDOMKimi(userElement) {
if (!IS_KIMI || !(userElement instanceof HTMLElement)) return null;
const aiConfig = CURRENT_SITE.aiMessage;
if (!aiConfig || !aiConfig.container || !aiConfig.userTurn || !aiConfig.file) return null;
let node = userElement;
for (let depth = 0; node && node !== document.body && depth < 7; depth++, node = node.parentElement) {
for (let sibling = node.nextElementSibling, guard = 0; sibling && guard < 12; sibling = sibling.nextElementSibling, guard++) {
if (sibling.matches && sibling.matches(aiConfig.userTurn)) break;
if (sibling.querySelector && sibling.querySelector(aiConfig.userTurn)) {
const directFile = sibling.querySelector(aiConfig.file);
if (!directFile) break;
}
if (sibling.matches && sibling.matches(aiConfig.container) && sibling.querySelector(aiConfig.file)) return sibling;
const nestedContainer = sibling.querySelector ? Array.from(sibling.querySelectorAll(aiConfig.container)).find(el => el.querySelector(aiConfig.file)) : null;
if (nestedContainer) return nestedContainer;
if (sibling.querySelector && sibling.querySelector(aiConfig.file)) return sibling;
}
}
return null;
}
function extractGeneratedFilesDOMKimi(userElement) {
if (!IS_KIMI || !(userElement instanceof HTMLElement)) return [];
const aiConfig = CURRENT_SITE.aiMessage;
if (!aiConfig || !aiConfig.file) return [];
const aiMessageEl = extractAIMessageDOMKimi(userElement);
if (!aiMessageEl) return [];
const seen = new Set();
const files = [];
Array.from(aiMessageEl.querySelectorAll(aiConfig.file)).forEach((link, ordinal) => {
const href = (link.getAttribute('href') || '').trim();
const textName = (link.textContent || '').trim();
let hrefName = '';
try { hrefName = decodeURIComponent(href.split('/').pop() || ''); } catch (_) { hrefName = href.split('/').pop() || ''; }
const name = textName || hrefName || `file-${ordinal + 1}`;
const key = (href || name).toLowerCase();
if (seen.has(key)) return;
seen.add(key);
const extMatch = name.match(/\.([a-z0-9]{1,10})$/i) || hrefName.match(/\.([a-z0-9]{1,10})$/i);
files.push({
name,
displayName: name,
ext: extMatch ? extMatch[1].toLowerCase() : 'file',
href,
kind: 'kimi-generated-file',
ordinal
});
});
return files;
}
function extractGeneratedFileNameDOMKimi(element) {
if (!(element instanceof HTMLElement)) return '';
const textName = (element.textContent || '').trim();
if (textName) return textName;
const href = element.getAttribute('href') || '';
if (!href) return '';
const rawName = href.split('/').pop() || '';
try { return decodeURIComponent(rawName); } catch (_) { return rawName; }
}
function findGeneratedFileTargetKimi(index, fileIdx, file) {
const userElement = findPromptElementByIndex(Math.max(0, index - 1));
if (!(userElement instanceof HTMLElement)) return null;
const aiMessageEl = extractAIMessageDOMKimi(userElement);
const aiConfig = CURRENT_SITE.aiMessage || {};
if (!aiMessageEl || !aiConfig.file) return null;
const elements = Array.from(aiMessageEl.querySelectorAll(aiConfig.file));
if (!elements.length) return null;
let chosen = file ? elements.find(el => {
if (file.href && (el.getAttribute('href') || '') === file.href) return true;
const actualName = extractGeneratedFileNameDOMKimi(el);
return [file.name, file.displayName].filter(Boolean).includes(actualName);
}) || null : null;
if (!chosen && Number.isFinite(fileIdx)) chosen = elements[fileIdx] || null;
chosen = chosen || elements[0] || null;
return chosen ? { target: chosen, actionElement: null } : null;
}
async function focusGeneratedFileKimi(index, fileIdx, file) {
return activateDomGeneratedFile({
findTarget: () => findGeneratedFileTargetKimi(index, fileIdx, file),
navigate: () => navigateToMessage(index),
attempts: 10,
earlyDelay: 100,
laterDelay: 180
});
}
if (IS_KIMI) currentFileAdapter = KIMI_FILE_ADAPTER;
// 08.3 Grok adapter: file data and file actions
// ---------------------------------------------------------------------------
const GROK_FILE_ADAPTER = {
async activateUserFile(file, index, fileIdx) {
return focusUserFileGrok(index, fileIdx, file);
},
async activateGeneratedFile(file, index, fileIdx) {
return focusGeneratedFileGrok(index, fileIdx, file);
}
};
function getBadgeUserMessageDataGrok(userElement) {
return { files: extractUserFileBadgeDOMByConfig(userElement, CURRENT_SITE.userMessage) };
}
function extractAIMessageDOMGrok(userElement) {
if (!IS_GROK || !(userElement instanceof Element)) return null;
const aiConfig = CURRENT_SITE.aiMessage || {};
const userMessage = userElement.matches(aiConfig.userTurn || '[data-testid="user-message"]')
? userElement
: userElement.querySelector(aiConfig.userTurn || '[data-testid="user-message"]');
const userTurn = (userMessage && userMessage.closest(aiConfig.relationRoot || '[data-scroll-anchor-root="true"]'))
|| userElement.closest(aiConfig.relationRoot || '[data-scroll-anchor-root="true"]');
if (!userTurn) return null;
const turns = Array.from(document.querySelectorAll(aiConfig.relationRoot || '[data-scroll-anchor-root="true"]'));
const position = turns.indexOf(userTurn);
if (position < 0) return null;
for (let i = position + 1; i < turns.length; i++) {
if (turns[i].querySelector(aiConfig.userTurn || '[data-testid="user-message"]')) break;
const aiMessage = turns[i].querySelector(aiConfig.container || '[data-testid="assistant-message"]');
if (aiMessage) return aiMessage;
}
return null;
}
function extractGeneratedFilesDOMGrok(userElement) {
const aiMessage = extractAIMessageDOMGrok(userElement);
if (!aiMessage) return [];
const aiConfig = CURRENT_SITE.aiMessage || {};
const selector = aiConfig.file || '[role="button"][aria-label^="Open "]';
const generatedFiles = [];
const seen = new Set();
Array.from(aiMessage.querySelectorAll(selector)).forEach((fileEl, ordinal) => {
const ariaLabel = (fileEl.getAttribute('aria-label') || '').trim();
const name = ariaLabel.replace(/^(Open|Download)\s+/i, '').trim();
if (!name || seen.has(name.toLowerCase())) return;
seen.add(name.toLowerCase());
generatedFiles.push({
name,
displayName: name,
ext: normalizeFileExtensionFromText(name),
kind: 'grok-generated-file',
ordinal
});
});
return generatedFiles;
}
function getFileBadgeAIMessageDataGrok(userElement) {
return { generatedFiles: extractGeneratedFilesDOMGrok(userElement) };
}
function findUserFileTargetGrok(index, fileIdx, file) {
const prompt = cachedPrompts[index - 1] || promptCacheItems[index - 1];
const userElement = prompt && prompt.element;
if (!(userElement instanceof Element)) return null;
const userConfig = CURRENT_SITE.userMessage || {};
const userTurn = userElement.closest('[data-scroll-anchor-root="true"]') || userElement;
const attachments = Array.from(userTurn.querySelectorAll(userConfig.file || 'button[aria-label="Open attachment"]'));
if (!attachments.length) return null;
const wanted = String(file && file.name || '').trim();
let target = wanted
? attachments.find(el => {
const nameEl = userConfig.fileName ? el.querySelector(userConfig.fileName) : null;
return String(nameEl && nameEl.textContent || '').trim() === wanted;
}) || null
: null;
if (!target && Number.isFinite(fileIdx)) target = attachments[fileIdx] || null;
return target || attachments[0] || null;
}
async function focusUserFileGrok(index, fileIdx, file) {
await navigateToMessage(index);
await sleep(80);
const target = findUserFileTargetGrok(index, fileIdx, file);
if (!target) return false;
await focusFileElement(target);
if (typeof target.click === 'function') target.click();
return true;
}
function findGeneratedFileTargetGrok(index, fileIdx, file) {
const prompt = cachedPrompts[index - 1] || promptCacheItems[index - 1];
const userElement = prompt && prompt.element;
if (!(userElement instanceof Element)) return null;
const aiMessage = extractAIMessageDOMGrok(userElement);
if (!aiMessage) return null;
const aiConfig = CURRENT_SITE.aiMessage || {};
const wanted = String(file && (file.name || file.displayName) || '').trim();
const openSelector = '[role="button"][aria-label^="Open "]';
const downloadSelector = aiConfig.fileDownload || 'button[aria-label^="Download "]';
const opens = Array.from(aiMessage.querySelectorAll(openSelector));
const downloads = Array.from(aiMessage.querySelectorAll(downloadSelector));
let target = wanted
? opens.find(el => (el.getAttribute('aria-label') || '').trim() === `Open ${wanted}`) || null
: null;
let actionElement = wanted
? downloads.find(el => (el.getAttribute('aria-label') || '').trim() === `Download ${wanted}`) || null
: null;
if (!target && Number.isFinite(fileIdx)) target = opens[fileIdx] || null;
target = target || opens[0] || actionElement || null;
if (!actionElement && Number.isFinite(fileIdx)) actionElement = downloads[fileIdx] || null;
return target ? { target, actionElement } : null;
}
async function focusGeneratedFileGrok(index, fileIdx, file) {
await navigateToMessage(index);
await sleep(120);
const match = findGeneratedFileTargetGrok(index, fileIdx, file);
if (!match || !match.target) return false;
await focusFileElement(match.target);
if (match.actionElement && typeof match.actionElement.click === 'function') match.actionElement.click();
else if (typeof match.target.click === 'function') match.target.click();
return true;
}
if (IS_GROK) currentFileAdapter = GROK_FILE_ADAPTER;
// 09. MESSAGE TEXT, ATTACHMENT, AND VERSION EXTRACTION
// ============================================================================
// 09.1 Text extraction and normalization
// ---------------------------------------------------------------------------
function extractCleanTextDOM(element) {
if (!element) return "";
const clone = element.cloneNode(true);
const badges = clone.querySelectorAll('.acn-message-inline-badge');
badges.forEach(b => b.remove());
return clone.textContent.trim();
}
// 09.2 Attachment extraction and normalization
// ---------------------------------------------------------------------------
function normalizeFileExtensionFromText(text, fallback = '') {
const source = String(text || '').replace(/…/g, ' ');
const fallbackText = String(fallback || '').trim().toLowerCase();
const known = ['docx','doc','odt','pdf','pptx','ppt','odp','xlsx','xls','ods','csv','txt','md','json','zip','rar','7z','png','jpg','jpeg','webp','gif','svg','heic','heif','mp4','mov','mp3','wav'];
if (known.includes(fallbackText)) return fallbackText;
const dotMatch = source.match(/\.([a-z0-9]{2,8})(?=$|[\s)\]}>.,;:!?])/i);
if (dotMatch && known.includes(dotMatch[1].toLowerCase())) return dotMatch[1].toLowerCase();
const tokenMatch = source.match(/\b(docx|doc|odt|pdf|pptx|ppt|odp|xlsx|xls|ods|csv|txt|md|json|zip|rar|7z|png|jpg|jpeg|webp|gif|svg|heic|heif|mp4|mov|mp3|wav)\b/i);
if (tokenMatch) return tokenMatch[1].toLowerCase();
if (/\bimage\b|\bphoto\b|\bpicture\b/i.test(source)) return 'img';
return 'file';
}
function extractFileTextCandidatesDOM(fileEl, msgConfig = {}) {
if (!fileEl) return [];
const candidates = [];
const push = value => {
const text = String(value || '').replace(/\s+/g, ' ').trim();
if (text && !candidates.includes(text)) candidates.push(text);
};
push(fileEl.getAttribute && fileEl.getAttribute('aria-label'));
push(fileEl.getAttribute && fileEl.getAttribute('title'));
if (msgConfig.fileName) push(fileEl.querySelector(msgConfig.fileName)?.textContent);
if (msgConfig.fileType) push(fileEl.querySelector(msgConfig.fileType)?.textContent);
push(fileEl.textContent);
let parent = fileEl.parentElement;
for (let depth = 0; parent && depth < 4; depth += 1, parent = parent.parentElement) {
push(parent.getAttribute && parent.getAttribute('aria-label'));
push(parent.getAttribute && parent.getAttribute('title'));
push(parent.textContent);
}
return candidates;
}
function extractFileInfoDOM(fileEl, msgConfig = {}) {
const candidates = extractFileTextCandidatesDOM(fileEl, msgConfig);
const explicitType = msgConfig.fileType ? extractCleanTextDOM(fileEl.querySelector(msgConfig.fileType)) : '';
const merged = candidates.join(' ');
const ext = normalizeFileExtensionFromText(merged, explicitType);
const name = candidates.find(text => /\.[a-z0-9]{2,8}\b/i.test(text)) || candidates[0] || ext;
return { ext, name };
}
// 09.3 Version-control extraction
// ---------------------------------------------------------------------------
function extractVersionControlDOM(container, vc) {
if (!container || !vc) return null;
const versionText = container.querySelector(vc.versionText);
if (!versionText) return null;
const text = versionText.textContent.trim();
let match = text.match(/(\d+)\D+(\d+)/);
let currentVersion, totalVersions;
if (match) {
currentVersion = parseInt(match[1]);
totalVersions = parseInt(match[2]);
} else if (text.length >= 2 && /^\d+$/.test(text)) {
currentVersion = parseInt(text[0]);
totalVersions = parseInt(text.slice(1));
} else {
return null;
}
const prevButton = container.querySelector(vc.prevButton);
const nextButton = container.querySelector(vc.nextButton);
return {
currentVersion,
totalVersions,
prevButton,
nextButton,
hasMultipleVersions: totalVersions > 1
};
}
function extractUserFileBadgeDOMByConfig(container, msgConfig = {}) {
if (!(container instanceof Element)) return [];
const files = [];
if (msgConfig.file) {
Array.from(container.querySelectorAll(msgConfig.file)).filter(isElementVisibleForNav).forEach(fileEl => {
const info = extractFileInfoDOM(fileEl, msgConfig);
if (info.name || info.ext !== 'file') files.push(info);
});
}
if (msgConfig.image) {
Array.from(container.querySelectorAll(msgConfig.image)).forEach(imgEl => {
if (!(imgEl instanceof HTMLImageElement) || imgEl.closest('#message-nav, #acn-mini-tooltip')) return;
files.push({ ext: 'img', name: imgEl.getAttribute('alt') || 'Image', url: imgEl.currentSrc || imgEl.src || imgEl.getAttribute('src') || '' });
});
}
return files.slice(0, 3);
}
function getBadgeUserMessageDataGeneric(container, msgConfig = {}) {
return { files: extractUserFileBadgeDOMByConfig(container, msgConfig) };
}
function getBadgeAIMessageDataGeneric() {
return { generatedFiles: [] };
}
// 09.4 Visible user-prompt collection
// ---------------------------------------------------------------------------
function isElementVisibleForNav(element) {
if (!(element instanceof HTMLElement)) return false;
if (element.closest('#message-nav, #acn-mini-tooltip')) return false;
let node = element;
while (node && node instanceof HTMLElement && node !== document.body) {
const style = window.getComputedStyle(node);
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || 1) === 0) return false;
if (node.getAttribute('aria-hidden') === 'true' || node.hidden) return false;
node = node.parentElement;
}
const rects = element.getClientRects ? Array.from(element.getClientRects()) : [];
return rects.some(rect => rect.width > 1 && rect.height > 1);
}
function extractVisibleUserPromptsDOM() {
if (IS_CHATGPT) return extractUserMessagesDOMGPT();
if (CURRENT_SITE.customFinder) return CURRENT_SITE.customFinder();
let prompts = [];
if (!CURRENT_SITE.userMessage || !CURRENT_SITE.userMessage.container) return prompts;
const userElements = Array.from(document.querySelectorAll(CURRENT_SITE.userMessage.container));
let containerElements = [];
if (CURRENT_SITE.versionControl && CURRENT_SITE.versionControl.container) {
containerElements = Array.from(document.querySelectorAll(CURRENT_SITE.versionControl.container));
}
if (CURRENT_SITE.reverse) {
userElements.reverse();
containerElements.reverse();
}
userElements.forEach((container, index) => {
let text = "";
const msgConfig = CURRENT_SITE.userMessage;
const hasChildSelectors = msgConfig.file || msgConfig.text || msgConfig.image;
let msgEl = null;
let displayFiles = [];
if (hasChildSelectors) {
msgEl = msgConfig.text ? container.querySelector(msgConfig.text) : null;
const msgText = msgEl ? extractCleanTextDOM(msgEl) : "";
if (IS_GEMINI) displayFiles = getBadgeUserMessageDataGemini(container).files;
else if (IS_KIMI) displayFiles = getBadgeUserMessageDataKimi(container).files;
else if (IS_GROK) displayFiles = getBadgeUserMessageDataGrok(container).files;
else displayFiles = getBadgeUserMessageDataGeneric(container, msgConfig).files;
if (msgText) {
text = msgText;
} else {
text = displayFiles.map(f => f.name).join(', ') || "[file]";
}
} else {
text = extractCleanTextDOM(container);
if (!text && (container.querySelector('img') || container.querySelector('canvas') || container.querySelector('svg'))) {
displayFiles.push({ext: "img", name: "Image"});
text = "Image";
}
}
if (text || displayFiles.length > 0) {
const promptData = { element: container, textElement: msgEl || container, text, files: displayFiles };
if (IS_GEMINI) promptData.generatedFiles = [
...getFileBadgeAIMessageDataGemini(container).generatedFiles,
...getImageBadgeAIMessageDataGemini(container).generatedFiles
];
else if (IS_KIMI) promptData.generatedFiles = getFileBadgeAIMessageDataKimi(container).generatedFiles;
else if (IS_GROK) promptData.generatedFiles = getFileBadgeAIMessageDataGrok(container).generatedFiles;
else promptData.generatedFiles = getBadgeAIMessageDataGeneric(container).generatedFiles;
collectPromptDateSource(promptData, container);
if (CURRENT_SITE.versionControl) {
const versionInfo = IS_CLAUDE
? getVersionControlDataClaude(container)
: (IS_KIMI ? getVersionControlDataKimi(containerElements[index]) : extractVersionControlDOM(containerElements[index], CURRENT_SITE.versionControl));
if (versionInfo) promptData.versionInfo = versionInfo;
}
prompts.push(promptData);
}
});
applySiteDateLineMetadata(prompts);
return prompts;
}
// ============================================================================
// 10. HISTORY PRELOADING AND PROMPT CACHE
// ============================================================================
// 10.1 History configuration and scroll helpers
// ---------------------------------------------------------------------------
function sleep(ms) {
return new Promise(resolve => safeSetTimeout(resolve, ms));
}
function isHistoryEnabled() {
return Boolean(CURRENT_SITE && CURRENT_SITE.historyPreload);
}
function getHistoryStorageKey() {
return HISTORY_POSITION_PREFIX + window.location.hostname + window.location.pathname;
}
function getConfigSelectorList(value) {
if (!value) return [];
if (Array.isArray(value)) return value.map(String).map(s => s.trim()).filter(Boolean);
return String(value).split(',').map(s => s.trim()).filter(Boolean);
}
function querySelectorList(selectors, push) {
getConfigSelectorList(selectors).forEach(sel => {
try { document.querySelectorAll(sel).forEach(push); } catch (_) {}
});
}
function getScrollableCandidates() {
const candidates = [];
const push = el => {
if (!el || candidates.includes(el)) return;
if (el.closest && (el.closest('#message-nav') || el.closest('#acn-mini-tooltip') || el.closest('nav'))) return;
candidates.push(el);
};
const virtualConfig = CURRENT_SITE && CURRENT_SITE.virtualScroll;
if (virtualConfig) {
querySelectorList(virtualConfig.scrollableCandidates || virtualConfig.scrollContainer, push);
}
const allowAutoDetect = !virtualConfig || virtualConfig.allowAutoScrollableDetect !== false;
if (allowAutoDetect) {
Array.from(document.querySelectorAll('main, section, div')).forEach(el => {
if (!(el instanceof HTMLElement)) return;
if (el.closest('#message-nav') || el.closest('#acn-mini-tooltip') || el.closest('nav')) return;
const cs = getComputedStyle(el);
if (!/(auto|scroll|overlay)/.test(cs.overflowY)) return;
if (el.scrollHeight <= el.clientHeight + 40) return;
push(el);
});
}
if (!virtualConfig || virtualConfig.includeDocumentScroll !== false) {
push(document.scrollingElement || document.documentElement);
push(document.body);
}
return candidates.filter(Boolean);
}
function canScrollElement(el) {
if (!el) return false;
if (el === document.body || el === document.documentElement || el === document.scrollingElement) {
return (document.scrollingElement || document.documentElement).scrollHeight > window.innerHeight + 20;
}
return el.scrollHeight > el.clientHeight + 20;
}
function getConversationScrollRoot() {
const candidates = getScrollableCandidates();
let best = null;
let bestScore = -1;
candidates.forEach(el => {
const root = (el === document.body || el === document.documentElement) ? (document.scrollingElement || document.documentElement) : el;
if (!canScrollElement(root)) return;
const hasMessages = Boolean(root.querySelector && root.querySelector(CURRENT_SITE.userMessage?.container || ''));
const rect = root.getBoundingClientRect ? root.getBoundingClientRect() : { width: window.innerWidth, height: window.innerHeight };
const overflow = Math.max(0, (root.scrollHeight || 0) - (root.clientHeight || window.innerHeight));
const score = overflow + (hasMessages ? 1000000 : 0) + Math.max(0, rect.height || 0);
if (score > bestScore) {
best = root;
bestScore = score;
}
});
return best || document.scrollingElement || document.documentElement;
}
function getScrollTop(el) {
if (!el) return 0;
if (el === document.body || el === document.documentElement || el === document.scrollingElement) return window.scrollY || (document.scrollingElement || document.documentElement).scrollTop || 0;
return el.scrollTop || 0;
}
function setScrollTop(el, top, behavior = 'auto') {
if (!el) return;
const value = Math.max(0, Number(top) || 0);
try {
if (el === document.body || el === document.documentElement || el === document.scrollingElement) {
const original = window.__acnOriginalScrollApis && window.__acnOriginalScrollApis.windowScrollTo;
if (original) original({ top: value, behavior: 'auto' });
else window.scrollTo({ top: value, behavior });
} else {
el.scrollTop = value;
if (Math.abs((el.scrollTop || 0) - value) > 2 && typeof el.scrollTo === 'function') {
el.scrollTo({ top: value, behavior });
}
}
} catch (e) {
try { el.scrollTop = value; } catch (_) {}
}
}
function getCurrentMessageCountForHistory() {
if (isTOCEnabledGPT()) {
const nativeCount = extractTOCButtonsDOMGPT().length;
if (nativeCount > 0) return nativeCount;
}
if (CURRENT_SITE.customFinder) return CURRENT_SITE.customFinder().length;
if (!CURRENT_SITE.userMessage || !CURRENT_SITE.userMessage.container) return 0;
return document.querySelectorAll(CURRENT_SITE.userMessage.container).length;
}
// 10.2 Scroll locking and reading-position persistence
// ---------------------------------------------------------------------------
function installScrollLock() {
if (scrollLockInstalled) return;
scrollLockInstalled = true;
const original = {
elementScrollIntoView: Element.prototype.scrollIntoView,
elementScrollTo: Element.prototype.scrollTo,
elementScroll: Element.prototype.scroll,
elementScrollBy: Element.prototype.scrollBy,
windowScrollTo: window.scrollTo.bind(window)
};
window.__acnOriginalScrollApis = original;
function lockedTop() {
const value = document.documentElement.dataset.acnPositionLock;
if (value === undefined) return null;
const n = Number(value);
return Number.isFinite(n) ? n : null;
}
function markBlocked() {
document.documentElement.dataset.acnPositionLockLastBlock = String(Date.now());
}
function isInsideOwnNavigator(el) {
return Boolean(el && el.closest && (el.closest('#message-nav') || el.closest('#acn-mini-tooltip')));
}
function isHorizontalOnly(args) {
const first = args && args[0];
return first && typeof first === 'object' && first.left !== undefined && first.top === undefined;
}
Element.prototype.scrollIntoView = function(options) {
const lock = lockedTop();
if (lock !== null && !isInsideOwnNavigator(this) && !(options && options.__bypassLock)) {
markBlocked();
return;
}
return original.elementScrollIntoView.apply(this, arguments);
};
window.scrollTo = function(options, y) {
const lock = lockedTop();
if (lock !== null) {
const requestedTop = (options && typeof options === 'object') ? options.top : y;
if (typeof requestedTop === 'number' && Math.abs(requestedTop - lock) > 10) {
markBlocked();
return original.windowScrollTo({ top: lock, behavior: 'auto' });
}
}
return original.windowScrollTo.apply(window, arguments);
};
function requestedTopFromArgs(args) {
const first = args && args[0];
if (first && typeof first === 'object') return first.top;
return args && args.length > 1 ? args[1] : first;
}
if (typeof original.elementScrollTo === 'function') {
Element.prototype.scrollTo = function() {
const lock = lockedTop();
if (lock !== null && !isInsideOwnNavigator(this) && !isHorizontalOnly(arguments)) {
const requestedTop = requestedTopFromArgs(arguments);
if (typeof requestedTop !== 'number' || Math.abs(requestedTop - lock) > 10) {
markBlocked();
return;
}
}
return original.elementScrollTo.apply(this, arguments);
};
}
if (typeof original.elementScroll === 'function') {
Element.prototype.scroll = function() {
const lock = lockedTop();
if (lock !== null && !isInsideOwnNavigator(this) && !isHorizontalOnly(arguments)) {
const requestedTop = requestedTopFromArgs(arguments);
if (typeof requestedTop !== 'number' || Math.abs(requestedTop - lock) > 10) {
markBlocked();
return;
}
}
return original.elementScroll.apply(this, arguments);
};
}
if (typeof original.elementScrollBy === 'function') {
Element.prototype.scrollBy = function() {
const lock = lockedTop();
if (lock !== null && !isInsideOwnNavigator(this) && !isHorizontalOnly(arguments)) {
markBlocked();
return;
}
return original.elementScrollBy.apply(this, arguments);
};
}
}
function setPositionLock(top) {
installScrollLock();
if (top === null || top === undefined) {
delete document.documentElement.dataset.acnPositionLock;
} else {
document.documentElement.dataset.acnPositionLock = String(Math.max(0, Number(top) || 0));
}
}
function scrollNavItemToTopIfOffscreen(container, item) {
if (!container || !item) return;
const cRect = container.getBoundingClientRect();
const iRect = item.getBoundingClientRect();
if (iRect.top >= cRect.top && iRect.bottom <= cRect.bottom) return;
container.scrollTop += (iRect.top - cRect.top);
}
function applyUniversalActiveMessage(index, options = {}) {
const total = cachedPrompts.length;
if (!Number.isFinite(index) || index < 1 || index > total) return false;
if (IS_CHATGPT && chatGptHighlightSuppressionDepth > 0) return activeMessageIndex !== index;
const changed = activeMessageIndex !== index;
activeMessageIndex = index;
universalScrollSyncLastIndex = index;
const list = document.getElementById('nav-list');
if (list) {
Array.from(list.children).forEach((item, idx) => item.classList.toggle('active', idx + 1 === index));
if (options.scrollBar !== false) scrollNavItemToTopIfOffscreen(list, list.children[index - 1]);
}
const miniNums = document.getElementById('nav-mini-msg-numbers');
if (miniNums) {
const wrappers = Array.from(miniNums.querySelectorAll(':scope > .nav-mini-item-wrapper'));
wrappers.forEach((wrapper, idx) => {
const num = wrapper.querySelector('.nav-mini-num');
if (num) num.classList.toggle('active', idx + 1 === index);
});
if (options.scrollBar !== false) scrollNavItemToTopIfOffscreen(miniNums, wrappers[index - 1]);
}
const superList = document.getElementById('supermini-message-list');
if (superList) {
Array.from(superList.children).forEach((item, idx) => item.classList.toggle('active', idx + 1 === index));
const superScroller = document.getElementById('supermini-scroll-inner') || superList.parentElement;
if (options.scrollBar !== false) scrollNavItemToTopIfOffscreen(superScroller, superList.children[index - 1]);
}
if (changed) {
updateMessageCounter();
updateBookmarksHeader();
}
return changed;
}
function getUniversalVisibleMessageIndex() {
if (!cachedPrompts.length) return -1;
if (IS_CHATGPT && isTOCEnabledGPT()) {
const nativeIndex = extractActiveTOCIndexDOMGPT();
if (nativeIndex >= 0 && nativeIndex < cachedPrompts.length) return nativeIndex + 1;
}
const root = getConversationScrollRoot();
const rootRect = root && root.getBoundingClientRect
? root.getBoundingClientRect()
: { top: 0, bottom: window.innerHeight, height: window.innerHeight };
const top = Math.max(0, rootRect.top || 0);
const bottom = Math.min(window.innerHeight, rootRect.bottom || window.innerHeight);
const focusY = top + Math.max(1, bottom - top) * 0.38;
let bestIndex = -1;
let bestDistance = Infinity;
cachedPrompts.forEach((prompt, idx) => {
const element = prompt && prompt.element && prompt.element.isConnected ? prompt.element : null;
if (!element || !element.getBoundingClientRect) return;
const rect = element.getBoundingClientRect();
if (rect.bottom < top || rect.top > bottom) return;
const targetY = Math.min(Math.max(focusY, rect.top), rect.bottom);
const distance = Math.abs(targetY - focusY);
if (distance < bestDistance) {
bestDistance = distance;
bestIndex = idx + 1;
}
});
if (bestIndex > 0) return bestIndex;
const selector = CURRENT_SITE.userMessage && CURRENT_SITE.userMessage.container;
if (!selector) return -1;
let elements = Array.from(document.querySelectorAll(selector));
if (CURRENT_SITE.reverse) elements.reverse();
elements.forEach((element, idx) => {
const rect = element.getBoundingClientRect();
const center = (rect.top + rect.bottom) / 2;
const distance = Math.abs(center - focusY);
if (distance < bestDistance) {
bestDistance = distance;
bestIndex = idx + 1;
}
});
return bestIndex > 0 && bestIndex <= cachedPrompts.length ? bestIndex : -1;
}
function refreshUniversalScrollSync() {
if (universalScrollSyncRafId) return;
if (IS_CHATGPT && isScanningOldMessages) return;
universalScrollSyncRafId = requestAnimationFrame(() => {
universalScrollSyncRafId = null;
if (IS_CLAUDE) {
refreshCurrentVisibleHighlightClaude();
return;
}
const index = getUniversalVisibleMessageIndex();
if (index < 1 || index === universalScrollSyncLastIndex && index === activeMessageIndex) return;
applyUniversalActiveMessage(index, { scrollBar: true });
});
}
function saveReadingPosition() {
if (!isHistoryEnabled()) return;
if (IS_CLAUDE && claudeSaveGuardActive) {
return;
}
if (IS_CLAUDE && claudeNavigationActive) {
return;
}
const now = Date.now();
if (now - historyLastSavedAt < 700) return;
historyLastSavedAt = now;
const root = getConversationScrollRoot();
const value = {
url: window.location.href,
path: window.location.pathname,
top: getScrollTop(root),
height: root.scrollHeight || 0,
count: getCurrentMessageCountForHistory(),
time: now
};
if (IS_CLAUDE) {
value.claudeTopUuid = claudeCurrentVisibleUuid;
}
try { GM_setValue(getHistoryStorageKey(), JSON.stringify(value)); } catch (e) {}
}
function readSavedReadingPosition() {
if (!isHistoryEnabled()) return null;
try {
const raw = GM_getValue(getHistoryStorageKey(), '');
if (!raw) return null;
const data = JSON.parse(raw);
if (!data || data.path !== window.location.pathname) return null;
if (Date.now() - Number(data.time || 0) > BOOKMARK_TTL_MS) return null;
return data;
} catch (e) {
return null;
}
}
function setHistoryStatus(isLoading, text) {
let status = document.getElementById('acn-history-status');
if (IS_CHATGPT) {
if (status) status.style.display = 'none';
return;
}
if (!status && document.body) {
status = document.createElement('div');
status.id = 'acn-history-status';
document.documentElement.appendChild(status);
}
if (status) {
status.textContent = text || '';
status.style.display = isLoading && text ? 'block' : 'none';
}
}
function isHistorySpinnerVisible() {
const cfg = CURRENT_SITE && CURRENT_SITE.virtualScroll;
const selectors = cfg && Array.isArray(cfg.loadingSelectors) ? cfg.loadingSelectors : [];
if (!selectors.length) return false;
return selectors.some(selector => {
try {
return Boolean(document.querySelector(selector));
} catch (e) {
return false;
}
});
}
function getHistoryLoaderOption(name, fallback) {
const cfg = CURRENT_SITE && CURRENT_SITE.virtualScroll;
return cfg && cfg[name] !== undefined ? cfg[name] : fallback;
}
// 10.3 Conversation history loading
// ---------------------------------------------------------------------------
async function loadConversationHistory(options = {}) {
if (IS_CLAUDE) {
if (!isActualChatView()) return false;
if (isHistoryLoading) {
return false;
}
isHistoryLoading = true;
try {
if (typeof options.targetIndex === 'number') {
const node = await scrollToCachedIndexClaude(options.targetIndex);
return Boolean(node);
}
if (!options.forceRebuild && promptCacheUrl === window.location.href && promptCacheItems.length > 0) {
return true;
}
const isFirstHarvestForThisUrl = promptCacheUrl !== window.location.href || promptCacheItems.length === 0;
const harvestOk = buildFastNavbarFromLiveFiberClaude();
if (harvestOk) {
scheduleHarvestStabilityCheckClaude();
const firstUuidNow = getHarvestFirstUuidClaude();
const looksContaminated = claudePreviousChatFirstUuid !== null && firstUuidNow === claudePreviousChatFirstUuid;
if (looksContaminated) {
claudeRestorePendingUrl = window.location.href;
} else if (options.restoreSaved && isFirstHarvestForThisUrl && claudeReadingPositionRestoredUrl !== window.location.href) {
claudeReadingPositionRestoredUrl = window.location.href;
const restored = restoreReadingPositionClaude();
if (restored) {
claudeRestorePendingUrl = null;
} else {
claudeRestorePendingUrl = window.location.href;
}
claudeSaveGuardActive = false;
} else if (options.restoreSaved) {
claudeSaveGuardActive = false;
}
return true;
}
return false;
} finally {
isHistoryLoading = false;
}
}
if (!isHistoryEnabled() || isHistoryLoading) return false;
if (isTOCEnabledGPT()) {
isHistoryLoading = true;
setHistoryStatus(true, 'Loading old messages');
const changed = seedTOCMessageCacheGPT();
await sleep(120);
setHistoryStatus(false, '');
isHistoryLoading = false;
return changed;
}
const root = getConversationScrollRoot();
if (!root) return false;
const currentCount = getCurrentMessageCountForHistory();
if (currentCount <= 0) return false;
rememberVisiblePromptsForCache(extractVisibleUserPromptsDOM(), 'append');
isHistoryLoading = true;
setHistoryStatus(true, 'Loading old messages');
const originalTop = getScrollTop(root);
const originalHeight = root.scrollHeight || 0;
const originalRootScrollBehavior = root && root.style ? root.style.scrollBehavior : '';
const originalDocScrollBehavior = document.documentElement.style.scrollBehavior;
const maxRounds = Number(getHistoryLoaderOption('maxRounds', HISTORY_LOAD_MAX_ROUNDS));
const stableRoundLimit = Number(getHistoryLoaderOption('stableRounds', HISTORY_LOAD_STABLE_ROUNDS));
const waitMs = Number(getHistoryLoaderOption('waitMs', HISTORY_LOAD_WAIT_MS));
const pingMs = Number(getHistoryLoaderOption('pingMs', Math.min(80, waitMs)));
const stationaryRestore = Boolean(getHistoryLoaderOption('stationaryRestore', true));
const disableSweep = Boolean(getHistoryLoaderOption('disableSweep', false));
let lastCount = Math.max(currentCount, promptCacheItems.length);
let lastHeight = originalHeight;
let stableRounds = 0;
let loadedMore = false;
const restoreReadingViewport = () => {
const heightDelta = Math.max(0, (root.scrollHeight || 0) - originalHeight);
setScrollTop(root, originalTop + heightDelta, 'auto');
};
try {
if (root && root.style) root.style.scrollBehavior = 'auto';
document.documentElement.style.scrollBehavior = 'auto';
setPositionLock(originalTop);
for (let round = 0; round < maxRounds; round++) {
const beforeCacheSize = promptCacheItems.length;
const beforeHeight = root.scrollHeight || 0;
setScrollTop(root, 0, 'auto');
await sleep(pingMs);
rememberVisiblePromptsForCache(extractVisibleUserPromptsDOM(), 'prepend');
if (stationaryRestore) {
const deltaAfterPing = Math.max(0, (root.scrollHeight || 0) - originalHeight);
setScrollTop(root, originalTop + deltaAfterPing, 'auto');
}
await sleep(Math.max(0, waitMs - pingMs));
const nextCount = Math.max(getCurrentMessageCountForHistory(), promptCacheItems.length);
const nextHeight = root.scrollHeight || 0;
const grew = nextCount > lastCount || promptCacheItems.length > beforeCacheSize || nextHeight > beforeHeight + 30 || nextHeight > lastHeight + 30;
loadedMore = loadedMore || grew;
stableRounds = grew ? 0 : stableRounds + 1;
lastCount = Math.max(lastCount, nextCount);
lastHeight = Math.max(lastHeight, nextHeight);
if (IS_CHATGPT && !disableSweep) {
const maxTop = Math.max(0, (root.scrollHeight || 0) - (root.clientHeight || window.innerHeight));
const step = Math.max(240, Math.floor((root.clientHeight || window.innerHeight) * 0.85));
for (let top = 0, guard = 0; top <= maxTop && guard < 240; top += step, guard++) {
setScrollTop(root, top, 'auto');
await sleep(35);
rememberVisiblePromptsForCache(extractVisibleUserPromptsDOM(), 'merge');
}
}
const totalDelta = Math.max(0, (root.scrollHeight || 0) - originalHeight);
setScrollTop(root, originalTop + totalDelta, 'auto');
await sleep(80);
setHistoryStatus(true, 'Loading old messages');
if (stableRounds >= stableRoundLimit && !isHistorySpinnerVisible()) break;
}
} finally {
setPositionLock(null);
if (root && root.style) root.style.scrollBehavior = originalRootScrollBehavior;
document.documentElement.style.scrollBehavior = originalDocScrollBehavior;
const saved = options.restoreSaved ? readSavedReadingPosition() : null;
if (saved) {
const savedDelta = Number(saved.height || 0) > 0 ? Math.max(0, (root.scrollHeight || 0) - Number(saved.height || 0)) : 0;
setScrollTop(root, Number(saved.top || 0) + savedDelta, 'auto');
} else {
restoreReadingViewport();
}
await sleep(80);
isHistoryLoading = false;
setHistoryStatus(false, '');
}
return loadedMore;
}
async function ensureHistoryBeforeList(forceUpdate) {
if (!isHistoryEnabled()) return false;
if (historyLoadedUrl === window.location.href && !forceUpdate) return false;
const urlBeingLoaded = window.location.href;
const chatGptTokenAtStart = IS_CHATGPT ? chatGptNavigationToken : 0;
const shouldRebuild = claudeForceRebuildPending;
claudeForceRebuildPending = false;
const loaded = await loadConversationHistory({ restoreSaved: true, forceRebuild: shouldRebuild });
const sameConversation = window.location.href === urlBeingLoaded
&& (!IS_CHATGPT || chatGptTokenAtStart === chatGptNavigationToken);
if (sameConversation && (!IS_CLAUDE || loaded)) historyLoadedUrl = urlBeingLoaded;
return loaded;
}
async function ensurePromptLoadedThenFind(index, options = {}) {
const debug = typeof unsafeWindow !== 'undefined' && unsafeWindow.__acnChatGPTScrollDebug;
const useCssStrip = typeof unsafeWindow === 'undefined' || unsafeWindow.__acnChatGPTUseCssStrip !== false;
let targetElement = findPromptElementByIndex(index);
if (targetElement) {
if (IS_CHATGPT) {
const root = getConversationScrollRoot();
if (root) {
if (useCssStrip) setCssStripChatGPT(root, true);
try {
let elementToCenter = targetElement;
let handledByForceRender = false;
if (isTOCButtonGPT(targetElement)) {
const apiCache = chatGptApiConversationCache;
const targetMsg = apiCache && Array.isArray(apiCache.userMessages) ? apiCache.userMessages[index] : null;
const realElement = targetMsg ? findMountedElementByMessageIdGPT(targetMsg.id) : null;
if (debug) {
console.log('[ACN-GPT-CENTER] toc-resolve ' + JSON.stringify({
index, isTocButton: true,
targetMsgFound: Boolean(targetMsg), targetMsgId: targetMsg ? targetMsg.id : null,
realElementFound: Boolean(realElement)
}));
}
if (realElement) {
elementToCenter = realElement;
} else if (isChatGPTApiReadyGPT()) {
const forced = await navigateViaForceRenderChatGPT(index);
if (forced) {
elementToCenter = forced;
handledByForceRender = true;
}
}
} else if (debug) {
console.log('[ACN-GPT-CENTER] toc-resolve ' + JSON.stringify({
index, isTocButton: false, elTag: targetElement.tagName,
hasTocItemIndexAttr: targetElement.hasAttribute && targetElement.hasAttribute('data-toc-item-index'),
tocEnabled: isTOCEnabledGPT()
}));
}
if (!handledByForceRender) {
await scrollElementIntoViewWithCorrectionForOffsetGPT(root, elementToCenter, 500, targetElement && targetElement.tagName === 'BUTTON' ? 'was-button' : index);
}
targetElement = elementToCenter;
} finally {
if (useCssStrip) setCssStripChatGPT(root, false);
}
}
}
return targetElement;
}
if (IS_CHATGPT && isChatGPTApiReadyGPT()) {
const sweepConversationId = getChatGPTConversationIdFromUrlAPI();
let sweepJustRanGPT = false;
if (sweepConversationId && !chatGptFullyLoadedConversationIds.has(sweepConversationId)) {
const sweepResult = await runFullHistorySweepOnceGPT(options.sweepLabelPrefix);
sweepJustRanGPT = true;
if (!sweepResult.stalledOut) {
chatGptFullyLoadedConversationIds.add(sweepConversationId);
}
}
const debug = typeof unsafeWindow !== 'undefined' && unsafeWindow.__acnChatGPTScrollDebug;
const navStartTime = performance.now();
const root = useCssStrip ? getConversationScrollRoot() : null;
if (root && useCssStrip) setCssStripChatGPT(root, true);
const postSweepRootGPT = sweepJustRanGPT ? (root || getConversationScrollRoot()) : null;
if (postSweepRootGPT) showRootCoverOverlayGPT(postSweepRootGPT);
let located;
try {
located = await navigateWithPaginationChatGPT(index);
if (!located) {
located = await navigateToHarvestedChatGPTMessageGPT(index);
if (located && debug) {
console.log('[ACN-GPT-NAV]', { index, result: 'succeeded via harvested-offset fallback' });
}
} else if (debug) {
console.log('[ACN-GPT-NAV]', { index, result: 'succeeded via force-render/pagination' });
}
} finally {
if (root && useCssStrip) setCssStripChatGPT(root, false);
if (postSweepRootGPT) {
showStatusOverlayClaude(options.sweepLabelPrefix || 'Preparing quick navigation...');
await waitForBadgeChurnQuietGPT(postSweepRootGPT);
let recorrectTarget = located && located.isConnected ? located : null;
if (!recorrectTarget) {
const apiCache = chatGptApiConversationCache;
const targetMsgForRecorrect = apiCache && Array.isArray(apiCache.userMessages) ? apiCache.userMessages[index] : null;
recorrectTarget = targetMsgForRecorrect ? findMountedElementByMessageIdGPT(targetMsgForRecorrect.id) : null;
}
if (recorrectTarget) {
await scrollElementIntoViewWithCorrectionForOffsetGPT(postSweepRootGPT, recorrectTarget, 2000);
}
hideRootCoverOverlayGPT();
hideStatusOverlayClaude();
}
if (debug) {
console.log('[ACN-GPT-NAV-TIMING]', {
index, totalMs: Math.round(performance.now() - navStartTime),
useCssStrip,
usePersistedRounds: Boolean(typeof unsafeWindow !== 'undefined' && unsafeWindow.__acnChatGPTUsePersistedRounds),
bundleFingerprint: getChatGPTBundleFingerprintGPT()
});
}
}
if (located) {
updateMessageList(true);
return located || findPromptElementByIndex(index);
}
}
if (!isHistoryEnabled()) return targetElement;
await loadConversationHistory({ restoreSaved: false, targetIndex: index });
updateMessageList(true);
return findPromptElementByIndex(index);
}
// 10.4 Stable prompt cache
// ---------------------------------------------------------------------------
function resetPromptCacheIfNeeded() {
if (!isHistoryEnabled()) return;
const cacheUrl = IS_CHATGPT ? getComparableConversationUrlGPT() : window.location.href;
if (promptCacheUrl === cacheUrl) return;
promptCacheUrl = cacheUrl;
promptCacheItems = [];
promptCacheMap = new Map();
}
function stableHashText(text) {
let h = 5381;
const value = String(text || '');
for (let i = 0; i < value.length; i++) h = ((h << 5) + h + value.charCodeAt(i)) | 0;
return Math.abs(h).toString(36) + '_' + value.length;
}
function getPromptStableKey(prompt, fallbackIndex) {
const el = prompt && prompt.element;
if (el && el.getAttribute) {
const messageEl = el.closest('[data-message-id]') || el.querySelector('[data-message-id]');
const turnEl = el.closest('[data-turn-id], [data-turn-id-container]') || el.querySelector('[data-turn-id], [data-turn-id-container]');
const mid = messageEl && messageEl.getAttribute('data-message-id');
const tid = turnEl && (turnEl.getAttribute('data-turn-id') || turnEl.getAttribute('data-turn-id-container'));
if (mid) return 'mid:' + mid;
if (tid) return 'turn:' + tid;
}
return 'txt:' + stableHashText((prompt.files || []).map(f => f.name || f.ext).join('|') + '|' + prompt.text) + ':' + fallbackIndex;
}
function rememberVisiblePromptsForCache(prompts, mode = 'merge') {
if (!isHistoryEnabled()) return prompts;
resetPromptCacheIfNeeded();
const prepared = prompts.map((p, idx) => ({ ...p, _acnKey: getPromptStableKey(p, idx), _acnLastSeen: Date.now() }));
if (mode === 'replace') {
promptCacheItems = prepared;
promptCacheMap = new Map(prepared.map(item => [item._acnKey, item]));
return promptCacheItems;
}
const newItems = [];
prepared.forEach(item => {
const old = promptCacheMap.get(item._acnKey);
if (old) {
Object.assign(old, item);
} else {
promptCacheMap.set(item._acnKey, item);
newItems.push(item);
}
});
if (newItems.length) {
if (mode === 'prepend') {
promptCacheItems = [...newItems, ...promptCacheItems.filter(item => !newItems.some(n => n._acnKey === item._acnKey))];
} else {
promptCacheItems = [...promptCacheItems, ...newItems];
}
}
const seen = new Set();
promptCacheItems = promptCacheItems.filter(item => {
if (!item || seen.has(item._acnKey)) return false;
seen.add(item._acnKey);
return true;
});
return promptCacheItems;
}
function getCachedPromptByIndex(index) {
if (!isHistoryEnabled()) return null;
resetPromptCacheIfNeeded();
return promptCacheItems[index] || null;
}
function hasUserMessages() {
if (isTOCEnabledGPT() && extractTOCButtonsDOMGPT().length > 0) return true;
if (CURRENT_SITE.customFinder) {
const prompts = CURRENT_SITE.customFinder();
return prompts && prompts.length > 0;
}
if (!CURRENT_SITE.userMessage || !CURRENT_SITE.userMessage.container) return false;
return document.querySelectorAll(CURRENT_SITE.userMessage.container).length > 0;
}
// ============================================================================
// 11. MESSAGE INDEXING AND NAVIGATION
// ============================================================================
// 11.1 Prompt discovery and cached lookup
// ---------------------------------------------------------------------------
function findUserPrompts() {
if (IS_CHATGPT && CURRENT_SITE.customFinder) {
return CURRENT_SITE.customFinder();
}
if (IS_CLAUDE) {
if (promptCacheItems.length) tagCurrentlyMountedMessagesClaude();
return promptCacheItems.length ? promptCacheItems : [];
}
const visiblePrompts = extractVisibleUserPromptsDOM();
if (isTOCEnabledGPT()) {
if (hasUsableTOCGPT()) {
rememberVisiblePromptsForCache(visiblePrompts, 'merge');
seedTOCMessageCacheGPT();
return promptCacheItems.length ? promptCacheItems : buildTOCMessageItemsGPT(visiblePrompts);
}
return rememberVisiblePromptsForCache(visiblePrompts, 'replace');
}
if (!isHistoryEnabled()) return visiblePrompts;
return rememberVisiblePromptsForCache(visiblePrompts, 'merge');
}
function findPromptElementByIndex(targetIndex) {
if (isTOCEnabledGPT()) {
seedTOCMessageCacheGPT();
const buttons = extractTOCButtonsDOMGPT();
const button = buttons.find((candidate, fallbackIndex) =>
extractTOCIndexDOMGPT(candidate, fallbackIndex) === targetIndex
);
if (button) return button;
const cached = getCachedPromptByIndex(targetIndex);
if (cached && cached.element && cached.element.isConnected) {
if (!isChatGPTApiReadyGPT() || verifyChatGPTElementMatchesOrdinalGPT(cached.element, targetIndex)) {
return cached.element;
}
}
}
if (isHistoryEnabled()) {
const cached = getCachedPromptByIndex(targetIndex);
if (cached && cached.element && cached.element.isConnected) {
if (!IS_CHATGPT || !isChatGPTApiReadyGPT() || verifyChatGPTElementMatchesOrdinalGPT(cached.element, targetIndex)) {
return cached.element;
}
}
}
if (IS_CLAUDE) {
return null;
}
if (CURRENT_SITE.customFinder) {
const prompts = CURRENT_SITE.customFinder();
return prompts[targetIndex] ? prompts[targetIndex].element : null;
}
if (!CURRENT_SITE.userMessage || !CURRENT_SITE.userMessage.container) return null;
const elements = Array.from(document.querySelectorAll(CURRENT_SITE.userMessage.container));
if (CURRENT_SITE.reverse) elements.reverse();
return elements[targetIndex] || null;
}
// 11.2 Navigator item construction and refresh
// ---------------------------------------------------------------------------
function createListItem(prompt, index) {
const listItem = document.createElement('li');
listItem.className = 'nav-max-item';
if (siteUsesDateLine()) {
const dateTooltipText = buildMessageDateTooltipText(prompt);
if (dateTooltipText) listItem.title = dateTooltipText;
}
if (index === activeMessageIndex) listItem.classList.add('active');
const preview = prompt.text.length > 80 ? prompt.text.slice(0, 80) + '...' : prompt.text;
if (prompt._acnHideNumber) {
listItem.classList.add('acn-chatgpt-toc-item');
} else {
const numberContainer = document.createElement('span');
numberContainer.className = 'nav-max-number';
numberContainer.title = 'Double-click to (un)bookmark this prompt.';
if (bookmarkedMessages.has(index)) {
numberContainer.textContent = `${index}`;
numberContainer.classList.add('bookmarked');
} else {
numberContainer.textContent = `${index}.`;
}
let maxClickTimer = null;
numberContainer.addEventListener('click', (e) => {
e.stopPropagation();
if (maxClickTimer) clearTimeout(maxClickTimer);
maxClickTimer = setTimeout(() => {
maxClickTimer = null;
navigateToMessage(index);
}, 220);
});
numberContainer.addEventListener('dblclick', (e) => {
e.preventDefault();
e.stopPropagation();
if (maxClickTimer) clearTimeout(maxClickTimer);
maxClickTimer = null;
toggleBookmark(index);
});
listItem.appendChild(numberContainer);
}
const contentWrapper = document.createElement('span');
contentWrapper.className = 'nav-max-content';
if (prompt.files && prompt.files.length > 0) {
prompt.files.forEach((f, fileIdx) => {
contentWrapper.appendChild(createUserFileBadge(f, index, fileIdx, 'max', prompt));
});
}
const textSpan = document.createElement('span');
textSpan.className = 'nav-max-preview';
textSpan.textContent = (prompt.files && prompt.files.length > 0) ? ` ${preview}` : preview;
contentWrapper.appendChild(textSpan);
if (prompt.versionInfo && prompt.versionInfo.hasMultipleVersions) {
const versionControls = document.createElement('span');
versionControls.className = 'nav-max-version-controls';
const prevVersionBtn = document.createElement('button');
prevVersionBtn.className = 'nav-max-version-btn';
prevVersionBtn.textContent = '◀';
prevVersionBtn.disabled = IS_CLAUDE ? isVersionButtonDisabledClaude(prompt.versionInfo, 'prevButton') : (!prompt.versionInfo.prevButton || prompt.versionInfo.prevButton.disabled);
prevVersionBtn.addEventListener('click', async (e) => {
e.stopPropagation();
if (IS_CLAUDE) {
prevVersionBtn.disabled = true;
nextVersionBtn.disabled = true;
await clickVersionButtonClaude(index, 'prevButton', prompt._acnUuid);
safeSetTimeout(() => updateMessageList(true), 300);
} else if (IS_CHATGPT) {
prevVersionBtn.disabled = true;
nextVersionBtn.disabled = true;
await switchVersionGPT(index, 'prevButton');
} else if (prompt.versionInfo.prevButton && !prompt.versionInfo.prevButton.disabled) {
prompt.versionInfo.prevButton.click();
safeSetTimeout(() => updateMessageList(true), 300);
}
});
const versionText = document.createElement('span');
versionText.className = 'nav-max-version-text';
versionText.textContent = `${prompt.versionInfo.currentVersion}/${prompt.versionInfo.totalVersions}`;
const nextVersionBtn = document.createElement('button');
nextVersionBtn.className = 'nav-max-version-btn';
nextVersionBtn.textContent = '▶';
nextVersionBtn.disabled = IS_CLAUDE ? isVersionButtonDisabledClaude(prompt.versionInfo, 'nextButton') : (!prompt.versionInfo.nextButton || prompt.versionInfo.nextButton.disabled);
nextVersionBtn.addEventListener('click', async (e) => {
e.stopPropagation();
if (IS_CLAUDE) {
prevVersionBtn.disabled = true;
nextVersionBtn.disabled = true;
await clickVersionButtonClaude(index, 'nextButton', prompt._acnUuid);
safeSetTimeout(() => updateMessageList(true), 300);
} else if (IS_CHATGPT) {
prevVersionBtn.disabled = true;
nextVersionBtn.disabled = true;
await switchVersionGPT(index, 'nextButton');
} else if (prompt.versionInfo.nextButton && !prompt.versionInfo.nextButton.disabled) {
prompt.versionInfo.nextButton.click();
safeSetTimeout(() => updateMessageList(true), 300);
}
});
versionControls.appendChild(prevVersionBtn);
versionControls.appendChild(versionText);
versionControls.appendChild(nextVersionBtn);
contentWrapper.appendChild(versionControls);
}
listItem.appendChild(contentWrapper);
if ((IS_CLAUDE || IS_CHATGPT || IS_GEMINI || IS_KIMI || IS_GROK) && prompt.generatedFiles && prompt.generatedFiles.length > 0) {
listItem.appendChild(createGeneratedFilesRow(prompt.generatedFiles, index));
}
textSpan.addEventListener('click', () => navigateToMessage(index));
return listItem;
}
async function updateMessageList(forceUpdate = false) {
if (IS_CHATGPT) {
const now = Date.now();
if (now - chatGptLastPassiveHarvestAt > 3000) {
chatGptLastPassiveHarvestAt = now;
harvestChatGPTDocumentOffsetsTick();
}
}
let container = document.getElementById('message-nav');
if (IS_CLAUDE && !isActualChatView()) {
if (container) container.style.display = 'none';
document.body.classList.remove('navigator-expanded');
updateBodyClassForLayout();
return;
}
const currentUrl = IS_CLAUDE
? getComparableUrlClaude()
: (IS_CHATGPT ? getComparableConversationUrlGPT() : window.location.href);
let chatGptUpdateToken = IS_CHATGPT ? chatGptNavigationToken : 0;
const isChatGptNewChatUrlPromotion = IS_CHATGPT
&& chatGptSawTransientWebUrl
&& currentUrl !== lastUrl
&& isNewChatRootUrlGPT(lastUrl)
&& !isTransientConversationUrlGPT(currentUrl);
if (isChatGptNewChatUrlPromotion) {
lastUrl = currentUrl;
chatGptSawTransientWebUrl = false;
if (promptCacheItems.length) promptCacheUrl = currentUrl;
if (chatGptTocCacheUrl) chatGptTocCacheUrl = currentUrl;
if (historyLoadedUrl) historyLoadedUrl = currentUrl;
bookmarkedMessages.clear();
loadBookmarks();
forceUpdate = true;
} else if (currentUrl !== lastUrl) {
if (IS_CLAUDE) claudePreviousChatFirstUuid = getHarvestFirstUuidClaude();
if (IS_CHATGPT) {
chatGptSawTransientWebUrl = false;
chatGptUpdateToken = beginConversationTransitionGPT(currentUrl);
}
lastUrl = currentUrl;
lastPromptsContent = '';
activeMessageIndex = -1;
cachedPrompts = [];
promptCacheUrl = '';
promptCacheItems = [];
promptCacheMap = new Map();
historyLoadedUrl = '';
isScanningOldMessages = false;
if (!IS_CLAUDE) setScanButtonState(false, 'Scan older messages');
bookmarkedMessages.clear();
loadBookmarks();
isClosedByUser = false;
claudeLastKnownChainLength = 0;
claudeReadingPositionRestoredUrl = null;
claudeRestorePendingUrl = null;
claudeSaveGuardActive = true;
forceUpdate = true;
}
if (IS_CHATGPT && chatGptPendingConversationUrl === currentUrl) {
chatGptUpdateToken = chatGptNavigationToken;
const ready = await waitForConversationReadyGPT(chatGptUpdateToken, currentUrl);
if (!ready || chatGptUpdateToken !== chatGptNavigationToken || window.location.href !== currentUrl) return;
forceUpdate = true;
}
const hasMessages = hasUserMessages();
let claudeGrowthDetectedThisTick = false;
let claudeChainDataThisTick = null;
if (IS_CLAUDE && isClaudeSharePage()) {
if (promptCacheUrl !== window.location.href || promptCacheItems.length === 0) {
if (buildShareNavbarFromDOMClaude()) forceUpdate = true;
}
} else if (IS_CLAUDE) {
claudeChainDataThisTick = extractMessageChainDataDOMClaude(document);
if (claudeChainDataThisTick) {
const chainLength = claudeChainDataThisTick.length;
if (chainLength > claudeLastKnownChainLength) {
claudeForceRebuildPending = true;
forceUpdate = true;
claudeGrowthDetectedThisTick = true;
}
claudeLastKnownChainLength = chainLength;
if (!claudeGrowthDetectedThisTick) {
refreshLastAssistantGeneratedFilesClaude(claudeChainDataThisTick);
}
}
}
const claudeHasRealData = IS_CLAUDE && (!!claudeChainDataThisTick || (isClaudeSharePage() && promptCacheItems.length > 0));
if ((hasMessages || claudeHasRealData) && isHistoryEnabled() && !isHistoryLoading && (forceUpdate || historyLoadedUrl !== window.location.href)) {
await ensureHistoryBeforeList(forceUpdate);
if (IS_CHATGPT && (chatGptUpdateToken !== chatGptNavigationToken || window.location.href !== currentUrl || chatGptPendingConversationUrl)) return;
}
const shouldShow = IS_CLAUDE ? (promptCacheItems.length > 0) : hasMessages;
updateShiftStyles(shouldShow && !isSuperMini);
if (!shouldShow || isClosedByUser) {
if (container) container.style.display = 'none';
document.body.classList.remove('navigator-expanded');
updateBodyClassForLayout();
if (IS_CHATGPT) commitConversationSnapshotGPT();
return;
}
const activeContainer = createContainer();
activeContainer.style.display = 'block';
applyNavTheme();
applyHeaderSolidBackgroundClaude();
updateBodyClassForLayout();
const content = document.getElementById('message-nav-content');
if (!content) return; let list = document.getElementById('nav-list');
if (!list) {
list = document.createElement('ul');
list.id = 'nav-list';
content.appendChild(list);
}
const prompts = findUserPrompts();
if (IS_CHATGPT && (chatGptUpdateToken !== chatGptNavigationToken || window.location.href !== currentUrl || chatGptPendingConversationUrl)) return;
updateMessageBadges(prompts);
const currentPromptsContent = prompts.map(p => (p._acnKey || '') + ':' + (p.files ? p.files.map(f => (f.name || '') + '.' + (f.ext || '')).join(',') : '') + ':' + (p.generatedFiles ? p.generatedFiles.map(f => (f.name || '') + '.' + (f.ext || '') + ':' + (f.kind || '')).join(',') : '') + ':' + (p.versionInfo ? (p.versionInfo.currentVersion + '/' + p.versionInfo.totalVersions) : '') + ':' + (p._acnCreatedAt ? p._acnCreatedAt.getTime() : '') + ':' + p.text).join('|');
if (!forceUpdate && currentPromptsContent === lastPromptsContent) {
if (IS_CHATGPT) commitConversationSnapshotGPT();
return;
}
lastPromptsContent = currentPromptsContent;
cachedPrompts = prompts;
if (IS_CHATGPT) {
const chatGptGrowthDetectedThisTick = prompts.length > chatGptLastKnownPromptCount;
chatGptLastKnownPromptCount = prompts.length;
if (chatGptGrowthDetectedThisTick && prompts.length > 0) {
activeMessageIndex = prompts.length;
chatGptStickyActiveOrdinal = prompts.length - 1;
if (typeof unsafeWindow !== 'undefined' && unsafeWindow.__acnChatGPTActiveDebug) {
console.log('[ACN-GPT-ACTIVE]', {
tier: 'growth-jump',
promptsLength: prompts.length,
activeMessageIndex,
chatGptStickyActiveOrdinal
});
}
} else {
if (chatGptHighlightSuppressionDepth === 0) {
const buttons = extractTOCButtonsDOMGPT();
const nativeActiveIndex = extractActiveTOCIndexDOMGPT(buttons);
const nativeCurrent = nativeActiveIndex >= 0 && nativeActiveIndex < prompts.length
? nativeActiveIndex + 1
: -1;
if (nativeCurrent > 0) {
activeMessageIndex = nativeCurrent;
} else if (activeMessageIndex < 1 || activeMessageIndex > prompts.length) {
activeMessageIndex = prompts.length > 0 ? 1 : -1;
}
}
}
}
while (list.firstChild) list.removeChild(list.firstChild);
prompts.forEach((prompt, index) => list.appendChild(createListItem(prompt, index + 1)));
if (IS_CLAUDE) {
if (claudeGrowthDetectedThisTick && prompts.length > 0) {
const lastPrompt = prompts[prompts.length - 1];
claudeCurrentVisibleIndex = prompts.length;
claudeCurrentVisibleUuid = lastPrompt._acnUuid || null;
}
applyNavHighlightClassesClaude();
if (claudeGrowthDetectedThisTick && prompts.length > 0) {
const newItem = list.children[prompts.length - 1];
scrollNavItemToTopIfOffscreen(list, newItem);
}
}
if (siteUsesDateLine()) renderNavDateDecorations(list);
updateMessageCounter();
if (IS_CLAUDE && claudeGrowthDetectedThisTick && prompts.length > 0) {
const miniNums = document.getElementById('nav-mini-msg-numbers');
const miniWrappers = miniNums ? miniNums.querySelectorAll(':scope > .nav-mini-item-wrapper') : null;
const miniWrapper = miniWrappers && miniWrappers[prompts.length - 1];
scrollNavItemToTopIfOffscreen(miniNums, miniWrapper);
}
updateBookmarksHeader();
if (IS_CHATGPT) commitConversationSnapshotGPT();
}
// ============================================================================
// 11.3 Message navigation
// ============================================================================
async function navigateToMessage(messageIndex, options = {}) {
const isRestore = Boolean(options.isRestore);
let locatingToastTimer = null;
if (IS_CLAUDE) {
claudeNavigationActive = true;
if (!isRestore) locatingToastTimer = realSetTimeoutClaude(() => showStatusOverlayClaude('Locating requested message...'), CLAUDE_LOCATING_TOAST_DELAY_MS);
showNavFreezeClaude();
}
try {
await navigateToMessageInner(messageIndex);
if (IS_CLAUDE) await sleepClaude(180);
} finally {
if (IS_CLAUDE) {
if (locatingToastTimer) locatingToastTimer.clear();
claudeNavigationActive = false;
claudeCurrentVisibleIndex = messageIndex;
const clickedItem = promptCacheItems[messageIndex - 1];
claudeCurrentVisibleUuid = clickedItem ? clickedItem._acnUuid : null;
if (!isRestore) claudeLastClickedIndex = messageIndex;
applyNavHighlightClassesClaude();
updateSuperminiInputNav();
if (isRestore) {
const list = document.getElementById('nav-list');
const item = list && list.children[messageIndex - 1];
if (item) item.scrollIntoView({ block: 'center', behavior: 'auto' });
const miniNums = document.getElementById('nav-mini-msg-numbers');
const miniWrapper = miniNums && miniNums.children[messageIndex - 1];
if (miniWrapper) miniWrapper.scrollIntoView({ block: 'center', behavior: 'auto' });
} else {
const list = document.getElementById('nav-list');
const item = list && list.children[messageIndex - 1];
scrollNavItemToTopIfOffscreen(list, item);
const miniNums = document.getElementById('nav-mini-msg-numbers');
const miniWrapper = miniNums && miniNums.children[messageIndex - 1];
scrollNavItemToTopIfOffscreen(miniNums, miniWrapper);
}
historyLastSavedAt = 0;
saveReadingPosition();
hideNavLoadingOverlayClaude();
hideStatusOverlayClaude();
realSetTimeoutClaude(refreshOffsetsFromNativeCacheClaude, 300);
}
}
}
async function navigateToMessageInner(messageIndex) {
let targetElement = await ensurePromptLoadedThenFind(messageIndex - 1);
if (!targetElement) return;
const list = document.getElementById('nav-list');
if (list) {
list.querySelectorAll('.nav-max-item').forEach(li => li.classList.remove('active'));
const targetItem = list.children[messageIndex - 1];
if (targetItem) {
targetItem.classList.add('active');
targetItem.scrollIntoView({ behavior: 'smooth', block: 'nearest', __bypassLock: true });
}
}
activeMessageIndex = messageIndex;
updateMessageCounter();
updateBookmarksHeader();
if (targetElement.__acnNativeToc) {
const nativeDetailIndex = messageIndex - 1;
targetElement.scrollIntoView({ behavior: 'auto', block: 'nearest', inline: 'nearest', __bypassLock: true });
targetElement.click();
const detail = isTOCEnabledGPT()
? await waitForMessageDetailGPT(
nativeDetailIndex,
isScanningOldMessages ? 3 : 10,
isScanningOldMessages ? 35 : 120
)
: null;
if (detail) cacheMessageDetailGPT(nativeDetailIndex, detail);
rememberVisiblePromptsForCache(extractVisibleUserPromptsDOM(), 'merge');
seedTOCMessageCacheGPT();
if (!isScanningOldMessages) {
await reconcileMountedMessageAfterNavigationGPT(nativeDetailIndex);
const mountedButtons = extractTOCButtonsDOMGPT();
const mountedPrompts = extractVisibleMessagesDOMGPT();
updateAdditionalDataGPT(ADDITIONAL_DATA_REASON_GPT.MESSAGE_MOUNT, {
targetIndex: nativeDetailIndex,
buttons: mountedButtons,
realPrompts: mountedPrompts
});
seedTOCMessageCacheGPT(mountedPrompts);
updateMessageList(true);
}
const remounted = findPromptElementByIndex(nativeDetailIndex);
if (remounted && !remounted.__acnNativeToc) targetElement = remounted;
} else if (CURRENT_SITE.useClick) {
targetElement.click();
} else if (IS_CHATGPT) {
} else {
const block = IS_CLAUDE ? 'center' : 'start';
try {
targetElement.scrollIntoView({ behavior: 'instant', block, __bypassLock: true });
} catch (e) {
targetElement.scrollIntoView({ block, __bypassLock: true });
}
}
saveReadingPosition();
}
// ============================================================================
// 12. NAVIGATOR LAYOUT AND BAR MODES
// ============================================================================
// 12.1 Navigator container and controls
// ---------------------------------------------------------------------------
function getEffectiveCurrentMessageIndex() {
if (IS_CLAUDE && claudeCurrentVisibleIndex > 0) return claudeCurrentVisibleIndex;
return activeMessageIndex;
}
function getMessageNavigationTooltip(direction) {
return direction < 0
? 'Previous message\nClick: Scroll the navbar upward/backward\nShift + click: Move backward 10 messages\nCtrl + Alt + click: Go to first message'
: 'Next message\nClick: Scroll the navbar downward/forward\nShift + click: Move forward 10 messages\nCtrl + Alt + click: Go to last message';
}
function handleMessageNavigationClick(e, direction) {
e.preventDefault();
e.stopPropagation();
const total = cachedPrompts.length;
if (!total) return;
if (e.altKey && (e.ctrlKey || e.metaKey)) {
navigateToMessage(direction < 0 ? 1 : total);
return;
}
const current = getEffectiveCurrentMessageIndex();
if (current < 1) return;
const step = e.shiftKey ? 10 : 1;
const target = Math.min(total, Math.max(1, current + (direction * step)));
if (target !== current) navigateToMessage(target);
}
function createContainer() {
let container = document.getElementById('message-nav');
let isNew = false;
if (!container) {
isNew = true;
isNavOnLeftSide = Boolean(GM_getValue(NAV_SIDE_STORAGE_KEY, isNavOnLeftSide));
container = document.createElement('div');
container.id = 'message-nav';
if (isSuperMini) {
container.classList.add('supermini-bar');
container.style.width = NAV_SUPERMINI_WIDTH + 'px';
container.style.minWidth = NAV_SUPERMINI_WIDTH + 'px';
container.style.maxWidth = NAV_SUPERMINI_WIDTH + 'px';
container.style.height = 'auto';
container.style.left = '';
container.style.top = '100px';
container.style.right = '10px';
container.style.bottom = 'auto';
} else {
container.classList.toggle('nav-left', isNavOnLeftSide);
container.style.left = isNavOnLeftSide ? '0' : '';
container.style.right = isNavOnLeftSide ? 'auto' : '0';
container.style.top = '0';
container.style.bottom = '0';
container.classList.add(isMiniBar ? 'mini-bar' : 'max-bar');
}
const header = document.createElement('div');
header.id = 'nav-header';
const navButtonsContainer = document.createElement('div');
navButtonsContainer.id = 'nav-buttons-group';
const scanBtn = document.createElement('button');
scanBtn.id = 'nav-btn-scan';
scanBtn.className = 'nav-control-btn';
if (IS_CLAUDE) {
scanBtn.textContent = SYMBOL_HARVEST_MOUNT_POSITION;
scanBtn.title = 'Harvest mount positions\nScrolls the whole conversation to record accurate positions for every message';
scanBtn.addEventListener('click', (e) => {
e.stopPropagation();
scanOlderMessages();
});
} else {
scanBtn.textContent = IS_CHATGPT ? SYMBOL_HARVEST_MOUNT_POSITION : SYMBOL_SCAN;
scanBtn.title = IS_CHATGPT
? 'Prepare and enable instant navigation\nSweeps the whole conversation once so every later jump is instant'
: 'Scan messages';
scanBtn.addEventListener('click', (e) => {
e.stopPropagation();
scanOlderMessages();
});
}
const backBtn = document.createElement('button');
backBtn.id = 'nav-btn-back';
backBtn.className = 'nav-control-btn';
backBtn.textContent = '◀';
backBtn.title = getMessageNavigationTooltip(-1);
backBtn.addEventListener('click', (e) => {
if (claudeHoldScrollWasActive) {
claudeHoldScrollWasActive = false;
e.preventDefault();
e.stopPropagation();
return;
}
handleMessageNavigationClick(e, -1);
});
backBtn.addEventListener('mousedown', (e) => { if (e.button === 0) startHoldScrollClaude(-1); });
backBtn.addEventListener('mouseup', stopHoldScrollClaude);
backBtn.addEventListener('mouseleave', stopHoldScrollClaude);
const messageCounter = document.createElement('span');
messageCounter.id = 'nav-message-counter';
const forwardBtn = document.createElement('button');
forwardBtn.id = 'nav-btn-forward';
forwardBtn.className = 'nav-control-btn';
forwardBtn.textContent = '▶';
forwardBtn.title = getMessageNavigationTooltip(1);
forwardBtn.addEventListener('click', (e) => {
if (claudeHoldScrollWasActive) {
claudeHoldScrollWasActive = false;
e.preventDefault();
e.stopPropagation();
return;
}
handleMessageNavigationClick(e, 1);
});
forwardBtn.addEventListener('mousedown', (e) => { if (e.button === 0) startHoldScrollClaude(1); });
forwardBtn.addEventListener('mouseup', stopHoldScrollClaude);
forwardBtn.addEventListener('mouseleave', stopHoldScrollClaude);
navButtonsContainer.appendChild(backBtn);
navButtonsContainer.appendChild(messageCounter);
navButtonsContainer.appendChild(forwardBtn);
updateNavButtonIconsClaude();
const miniBtn = document.createElement('button');
miniBtn.id = 'nav-btn-mini';
miniBtn.textContent = isMiniBar ? '▢' : '◧';
miniBtn.title = (isMiniBar ? "Click: switch to full bar\nCtrl+click: supermini bar\nEsc+click: close" : "Click: switch to mini bar\nCtrl+click: supermini bar") + "\nMiddle-click: switch side";
miniBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (e.detail > 1) return;
if (isMiniBar && claudeNavModifierEscape) {
closeNavigator();
return;
}
if (e.ctrlKey) {
applyBarMode('supermini');
return;
}
applyBarMode(isMiniBar ? 'max' : 'mini');
});
miniBtn.addEventListener('mousedown', (e) => {
if (e.button === 1) e.preventDefault();
});
miniBtn.addEventListener('auxclick', (e) => {
if (e.button !== 1) return;
e.preventDefault();
e.stopPropagation();
toggleNavSide();
});
const closeBtn = document.createElement('button');
closeBtn.id = 'nav-btn-close';
closeBtn.textContent = SYMBOL_CLOSE;
closeBtn.title = "Close Navigator";
header.appendChild(closeBtn);
header.appendChild(miniBtn);
header.appendChild(navButtonsContainer);
const miniBookmarks = document.createElement('div');
miniBookmarks.id = 'nav-mini-bookmarks';
miniBookmarks.textContent = SYMBOL_BOOKMARK;
miniBookmarks.title = 'Bookmarked messages';
const miniBookmarksPanel = document.createElement('div');
miniBookmarksPanel.id = 'nav-mini-bookmarks-panel';
miniBookmarks.appendChild(miniBookmarksPanel);
miniBookmarks.addEventListener('mouseenter', positionMiniBookmarksPanel);
header.appendChild(miniBookmarks);
{
const themeBtn = document.createElement('button');
themeBtn.id = 'nav-theme-btn';
themeBtn.type = 'button';
themeBtn.textContent = SYMBOL_SETTINGS;
themeBtn.title = 'Settings';
themeBtn.addEventListener('mousedown', (e) => e.preventDefault());
const themePanel = createSettingsPanelClaude();
document.documentElement.appendChild(themePanel);
themeBtn.addEventListener('pointerup', (e) => {
e.stopPropagation();
const isOpen = themePanel.style.display === 'flex';
themePanel.style.display = isOpen ? 'none' : 'flex';
themeBtn.classList.toggle('acn-panel-open', !isOpen);
container.classList.toggle('acn-panel-open', !isOpen);
if (!isOpen) {
positionThemePanelClaude();
themePanel.style.setProperty('background-color', extractProtectedNavBgDOMClaude(), 'important');
}
});
const miniTools = document.createElement('div');
miniTools.id = 'nav-mini-tools';
miniTools.appendChild(scanBtn);
miniTools.appendChild(themeBtn);
header.appendChild(miniTools);
}
const miniMsgWrapper = document.createElement('div');
miniMsgWrapper.id = 'nav-mini-msg-wrapper';
const miniMsgNumbers = document.createElement('div');
miniMsgNumbers.id = 'nav-mini-msg-numbers';
miniMsgWrapper.appendChild(miniMsgNumbers);
header.appendChild(miniMsgWrapper);
const content = document.createElement('div');
content.id = 'message-nav-content';
container.appendChild(header);
container.appendChild(content);
document.documentElement.appendChild(container);
let tooltipEl = document.getElementById('acn-mini-tooltip');
if (!tooltipEl) {
tooltipEl = document.createElement('div');
tooltipEl.id = 'acn-mini-tooltip';
document.documentElement.appendChild(tooltipEl);
}
closeBtn.addEventListener('click', (e) => {
e.stopPropagation();
closeNavigator();
});
updateBodyClassForLayout();
}
if (!container.querySelector('#nav-supermini-header')) {
const superminiHeader = document.createElement('div');
superminiHeader.id = 'nav-supermini-header';
const smMaxBtn = document.createElement('button');
smMaxBtn.className = 'nav-supermini-btn';
smMaxBtn.textContent = SYMBOL_MAXBAR;
smMaxBtn.title = "Click: Maxbar\nCtrl+click: Minibar";
smMaxBtn.addEventListener('click', (e) => {
e.stopPropagation();
applyBarMode(e.ctrlKey || e.metaKey ? 'mini' : 'max');
});
const smCloseBtn = document.createElement('button');
smCloseBtn.className = 'nav-supermini-btn';
smCloseBtn.textContent = SYMBOL_CLOSE;
smCloseBtn.title = "Close";
smCloseBtn.addEventListener('click', (e) => {
e.stopPropagation();
container.style.display = 'none';
isClosedByUser = true;
updateBodyClassForLayout();
updateSuperminiInputNav();
});
const smDragBtn = document.createElement('button');
smDragBtn.id = 'nav-btn-drag';
smDragBtn.className = 'nav-supermini-btn';
smDragBtn.textContent = SYMBOL_DRAG;
smDragBtn.title = "Drag";
superminiHeader.appendChild(smMaxBtn);
superminiHeader.appendChild(smCloseBtn);
superminiHeader.appendChild(smDragBtn);
const superminiPanel = document.createElement('div');
superminiPanel.id = 'supermini-message-panel';
container.appendChild(superminiHeader);
container.appendChild(superminiPanel);
}
if (isNew) {
applyBarMode(isSuperMini ? 'supermini' : (isMiniBar ? 'mini' : 'max'), { persist: false, source: 'restore' });
if (isSuperMini) {
requestAnimationFrame(() => {
updateSuperminiPanel();
const list = document.querySelector('#supermini-message-panel ul');
if (siteUsesDateLine() && list) renderNavDateDecorations(list);
});
}
}
initSuperminiDrag();
relocateSettingsButtonClaude(isSuperMini ? 'supermini' : (isMiniBar ? 'mini' : 'max'));
return container;
}
// 12.2 Page shifting and navigator width
// ---------------------------------------------------------------------------
function getEffectiveNavWidth() {
if (isSuperMini) return 0;
return isMiniBar ? (IS_CLAUDE ? 52 : NAV_MINI_WIDTH) : (CURRENT_SITE.width || NAV_WIDTH);
}
const getShiftStyle = (width, selector = '', onLeft = false) => {
if (!selector) return '';
const nearProp = onLeft ? 'margin-left' : 'margin-right';
const farProp = onLeft ? 'margin-right' : 'margin-left';
if (selector === 'body') {
return `
body { transition: margin-right 0.3s ease, max-width 0.3s ease, margin-left 0.3s ease; }
body.navigator-expanded { ${farProp}: 0 !important; ${nearProp}: ${width}px !important; max-width: calc(100% - ${width}px) !important; transform: translateZ(0); }
`;
}
const selectors = selector.split(',');
const prefixedSelector = selectors.map(s => `body.navigator-expanded ${s.trim()}`).join(', ');
return `
${selector} { transition: margin-right 0.3s ease, max-width 0.3s ease, margin-left 0.3s ease; }
${prefixedSelector} { ${farProp}: 0 !important; ${nearProp}: ${width}px !important; max-width: calc(100% - ${width}px) !important; }
`;
};
function updateShiftStyles(shouldInject) {
if (isSuperMini) {
let existingStyle = document.getElementById(injectedStyleId);
if (existingStyle) existingStyle.remove();
return;
}
let existingStyle = document.getElementById(injectedStyleId);
if (shouldInject) {
const currentWidth = getEffectiveNavWidth();
const cssContent = CURRENT_SITE.shiftTarget ? getShiftStyle(currentWidth, CURRENT_SITE.shiftTarget, isNavOnLeftSide) : '';
if (cssContent) {
if (!existingStyle) {
existingStyle = document.createElement('style');
existingStyle.id = injectedStyleId;
document.head.appendChild(existingStyle);
}
if (existingStyle.textContent !== cssContent) {
existingStyle.textContent = cssContent;
}
} else if (existingStyle) {
existingStyle.remove();
}
} else if (!shouldInject && existingStyle) {
existingStyle.remove();
}
}
function updateBodyClassForLayout() {
const container = document.getElementById('message-nav');
if (!container || container.style.display === 'none' || isSuperMini) {
document.body.classList.remove('navigator-expanded');
return;
}
document.body.classList.add('navigator-expanded');
}
function checkMiniScrollVisibility() {
safeSetTimeout(() => {
const numContainer = document.getElementById('nav-mini-msg-numbers');
const scrollUpBtn = document.getElementById('nav-mini-scroll-up');
const scrollDownBtn = document.getElementById('nav-mini-scroll-down');
const nav = document.getElementById('message-nav');
if (!numContainer || !scrollUpBtn || !scrollDownBtn || !nav) return;
if (nav.classList.contains('mini-bar') && nav.style.display !== 'none') {
const hasOverflow = numContainer.scrollHeight > numContainer.clientHeight;
scrollUpBtn.style.display = hasOverflow ? 'block' : 'none';
scrollDownBtn.style.display = hasOverflow ? 'block' : 'none';
}
}, 250);
}
// 12.3 Maxbar mode
// ---------------------------------------------------------------------------
function getOurNavLeftWidth() {
if (isMiniBar) return IS_CLAUDE ? 52 : NAV_MINI_WIDTH;
return CURRENT_SITE.width || NAV_WIDTH;
}
function updateSidebarPushForNavLeftClaude() {
if (!IS_CLAUDE) return;
const sidebar = document.querySelector('nav.fixed.left-0');
if (!sidebar) return;
if (isNavOnLeftSide && !isSuperMini && !isClosedByUser) {
sidebar.style.left = getOurNavLeftWidth() + 'px';
} else {
sidebar.style.left = '';
}
}
function closeNavigator() {
const container = document.getElementById('message-nav');
if (container) container.style.display = 'none';
isClosedByUser = true;
updateShiftStyles(false);
document.body.classList.remove('navigator-expanded');
const tooltipEl = document.getElementById('acn-mini-tooltip');
if (tooltipEl) tooltipEl.style.display = 'none';
if (IS_CLAUDE) {
const sidebar = document.querySelector('nav.fixed.left-0');
if (sidebar) sidebar.style.left = '';
}
updateBodyClassForLayout();
updateSuperminiInputNav();
}
function initSidebarWatcherClaude() {
if (!IS_CLAUDE || claudeSidebarResizeObserver) return;
const sidebar = document.querySelector('nav.fixed.left-0');
if (!sidebar) return;
claudeSidebarResizeObserver = new ResizeObserver(() => updateSidebarPushForNavLeftClaude());
claudeSidebarResizeObserver.observe(sidebar);
}
function relocateSettingsButtonClaude(mode) {
const settingsBtn = document.getElementById('nav-theme-btn');
const scanBtn = document.getElementById('nav-btn-scan');
const miniTools = document.getElementById('nav-mini-tools');
if (!settingsBtn || !scanBtn || !miniTools) return;
if (mode === 'supermini') {
const superminiHeader = document.getElementById('nav-supermini-header');
const dragBtn = document.getElementById('nav-btn-drag');
if (superminiHeader && dragBtn) {
if (scanBtn.parentElement !== superminiHeader) superminiHeader.insertBefore(scanBtn, dragBtn);
if (settingsBtn.parentElement !== superminiHeader) superminiHeader.insertBefore(settingsBtn, dragBtn);
}
return;
}
const header = document.getElementById('nav-header');
if (!header) return;
if (mode === 'mini') {
if (scanBtn.parentElement !== miniTools) miniTools.appendChild(scanBtn);
if (settingsBtn.parentElement !== miniTools) miniTools.appendChild(settingsBtn);
if (miniTools.parentElement !== header) header.appendChild(miniTools);
return;
}
if (scanBtn.parentElement !== header) header.appendChild(scanBtn);
if (settingsBtn.parentElement !== header) header.appendChild(settingsBtn);
}
function applyBarMode(mode, options = {}) {
const persist = options.persist !== false;
const source = options.source || 'manual';
if (source !== 'responsive') isResponsiveMiniActive = false;
isMiniBar = (mode === 'mini');
isSuperMini = (mode === 'supermini');
if (persist) {
if (isSuperMini) {
GM_setValue(SUPERMINI_BAR_STORAGE_KEY, true);
GM_deleteValue(MINI_BAR_STORAGE_KEY);
} else if (isMiniBar) {
GM_setValue(MINI_BAR_STORAGE_KEY, true);
GM_deleteValue(SUPERMINI_BAR_STORAGE_KEY);
} else {
GM_deleteValue(MINI_BAR_STORAGE_KEY);
GM_deleteValue(SUPERMINI_BAR_STORAGE_KEY);
}
}
const container = document.getElementById('message-nav');
if (!container) return;
container.classList.remove('mini-bar', 'max-bar', 'supermini-bar');
if (mode === 'supermini') {
container.classList.remove('nav-left');
container.classList.add('supermini-bar');
container.style.width = NAV_SUPERMINI_WIDTH + 'px';
container.style.minWidth = NAV_SUPERMINI_WIDTH + 'px';
container.style.maxWidth = NAV_SUPERMINI_WIDTH + 'px';
container.style.height = 'auto';
container.style.bottom = 'auto';
const savedPos = GM_getValue('acn_supermini_pos_' + window.location.hostname, '');
if (savedPos) {
try {
const pos = JSON.parse(savedPos);
container.style.left = pos.x + 'px';
container.style.top = pos.y + 'px';
container.style.right = 'auto';
container.style.bottom = 'auto';
} catch(e) {}
} else {
container.style.left = '';
container.style.top = '100px';
container.style.right = '10px';
container.style.bottom = 'auto';
}
updateShiftStyles(false);
} else if (mode === 'mini') {
container.style.width = '';
container.style.minWidth = '';
container.style.maxWidth = '';
container.style.height = '';
container.classList.toggle('nav-left', isNavOnLeftSide);
container.classList.add('mini-bar');
container.style.left = isNavOnLeftSide ? '0' : '';
container.style.top = '0';
container.style.right = isNavOnLeftSide ? 'auto' : '0';
container.style.bottom = '0';
updateShiftStyles(true);
} else {
container.style.width = '';
container.style.minWidth = '';
container.style.maxWidth = '';
container.style.height = '';
container.classList.toggle('nav-left', isNavOnLeftSide);
container.classList.add('max-bar');
container.style.left = isNavOnLeftSide ? '0' : '';
container.style.top = '0';
container.style.right = isNavOnLeftSide ? 'auto' : '0';
container.style.bottom = '0';
updateShiftStyles(true);
}
const tooltipEl = document.getElementById('acn-mini-tooltip');
if(tooltipEl) tooltipEl.style.display = 'none';
checkMiniScrollVisibility();
updateSuperminiPanel();
updateBookmarksHeader();
updateBodyClassForLayout();
updateMiniBtnAppearance();
initSuperminiDrag();
updateSuperminiInputNav();
relocateSettingsButtonClaude(mode);
applyNavTheme();
applyHeaderSolidBackgroundClaude();
initThemeAutoWatcherClaude();
if (siteUsesDateLine()) scheduleNavDateDecorRerender();
if (IS_CLAUDE) initSidebarWatcherClaude();
if (IS_CLAUDE) updateSidebarPushForNavLeftClaude();
}
window.addEventListener('resize', () => { if (isMiniBar) positionMiniBookmarksPanel(); }, { passive: true });
// 12.4 Minibar mode
// ---------------------------------------------------------------------------
function toggleNavSide() {
isNavOnLeftSide = !isNavOnLeftSide;
GM_setValue(NAV_SIDE_STORAGE_KEY, isNavOnLeftSide);
applyBarMode(isSuperMini ? 'supermini' : (isMiniBar ? 'mini' : 'max'), { persist: false });
}
function updateMiniBtnAppearance() {
const miniBtn = document.getElementById('nav-btn-mini');
if (!miniBtn) return;
if (isMiniBar) {
miniBtn.textContent = SYMBOL_MAXBAR;
miniBtn.title = "Click: switch to full bar\nCtrl+click: supermini bar\nEsc+click: close\nMiddle-click: switch side";
} else {
miniBtn.textContent = SYMBOL_MINIBAR;
miniBtn.title = "Click: switch to mini bar\nCtrl+click: supermini bar\nMiddle-click: switch side";
}
}
// 12.5 Superminibar mode and positioning
// ---------------------------------------------------------------------------
function getSuperminiInputPlacement() {
const inputAndSend = CURRENT_SITE.inputAndSend;
if (!inputAndSend?.composerInput) return null;
const input = document.querySelector(inputAndSend.composerInput);
if (!input) return null;
let container = null;
if (inputAndSend.composerContainer) {
container = input.closest(inputAndSend.composerContainer);
}
const sendButton = inputAndSend.composerSendButton
? document.querySelector(inputAndSend.composerSendButton)
: null;
if (!container && sendButton) {
const inputAncestors = new Set();
let node = input;
while (node && node !== document.documentElement) {
inputAncestors.add(node);
node = node.parentElement;
}
node = sendButton;
while (node && node !== document.documentElement) {
if (inputAncestors.has(node)) {
container = node;
break;
}
node = node.parentElement;
}
}
if (!container) container = input.closest('form, fieldset') || input.parentElement;
if (!container) return null;
let anchor = container;
if (inputAndSend.inputNavAnchor) {
anchor = input.closest(inputAndSend.inputNavAnchor)
|| container.querySelector(inputAndSend.inputNavAnchor)
|| container;
}
return { container, anchor };
}
function positionSuperminiInputNav(nav, container, anchor) {
if (!nav || !container || !anchor) return;
const containerRect = container.getBoundingClientRect();
const anchorRect = anchor.getBoundingClientRect();
if (!containerRect.width || !anchorRect.width) return;
nav.style.left = `${anchorRect.left - containerRect.left + (anchorRect.width / 2)}px`;
nav.style.top = `${anchorRect.top - containerRect.top}px`;
nav.style.right = 'auto';
nav.style.bottom = 'auto';
nav.style.transform = 'translate(-50%, calc(-100% - 3px))';
}
function getSuperminiInputCurrentIndex(total) {
if (!total) return 0;
const current = getEffectiveCurrentMessageIndex();
return Math.min(Math.max(current > 0 ? current : 1, 1), total);
}
function removeSuperminiInputNav(nav = document.getElementById('acn-supermini-input-nav')) {
if (!nav) return;
const host = nav.parentElement;
nav.remove();
if (host) host.classList.remove('acn-supermini-input-host');
}
function updateSuperminiInputNav() {
const supportsInputNav = Boolean(CURRENT_SITE.inputAndSend?.composerInput);
const existing = document.getElementById('acn-supermini-input-nav');
if (!supportsInputNav) {
removeSuperminiInputNav(existing);
return;
}
const mainNav = document.getElementById('message-nav');
const shouldShow = Boolean(isSuperMini && !isClosedByUser && mainNav && mainNav.style.display !== 'none');
const placement = getSuperminiInputPlacement();
const host = placement?.container || null;
const anchor = placement?.anchor || null;
if (!shouldShow || !host) {
if (!shouldShow) removeSuperminiInputNav(existing);
else if (existing) existing.classList.remove('acn-visible');
return;
}
let nav = existing;
if (!nav || nav.parentElement !== host) {
removeSuperminiInputNav(nav);
host.classList.add('acn-supermini-input-host');
nav = document.createElement('div');
nav.id = 'acn-supermini-input-nav';
const previousBtn = document.createElement('button');
previousBtn.type = 'button';
previousBtn.className = 'acn-supermini-input-btn';
previousBtn.dataset.acnDirection = 'previous';
previousBtn.textContent = SYMBOL_PREVIOUS;
previousBtn.title = getMessageNavigationTooltip(-1);
previousBtn.addEventListener('click', (e) => handleMessageNavigationClick(e, -1));
const counter = document.createElement('span');
counter.id = 'acn-supermini-input-counter';
counter.textContent = '0 / 0';
const nextBtn = document.createElement('button');
nextBtn.type = 'button';
nextBtn.className = 'acn-supermini-input-btn';
nextBtn.dataset.acnDirection = 'next';
nextBtn.textContent = SYMBOL_NEXT;
nextBtn.title = getMessageNavigationTooltip(1);
nextBtn.addEventListener('click', (e) => handleMessageNavigationClick(e, 1));
nav.appendChild(previousBtn);
nav.appendChild(counter);
nav.appendChild(nextBtn);
host.appendChild(nav);
} else {
host.classList.add('acn-supermini-input-host');
}
const total = cachedPrompts.length;
const current = getSuperminiInputCurrentIndex(total);
const counter = nav.querySelector('#acn-supermini-input-counter');
const previousBtn = nav.querySelector('[data-acn-direction="previous"]');
const nextBtn = nav.querySelector('[data-acn-direction="next"]');
if (counter) counter.textContent = `${current} / ${total}`;
if (previousBtn) previousBtn.disabled = current <= 1;
if (nextBtn) nextBtn.disabled = total === 0 || current >= total;
nav.classList.add('acn-visible');
positionSuperminiInputNav(nav, host, anchor);
}
function initSuperminiDrag() {
if (superminiDragInitialized) return;
superminiDragInitialized = true;
let isDragging = false;
let dragOffset = { x: 0, y: 0 };
let activeContainer = null;
let activeDragBtn = null;
document.addEventListener('mousedown', (e) => {
if (!isSuperMini) return;
const dragBtn = e.target.closest('#nav-btn-drag');
if (!dragBtn) return;
const container = document.getElementById('message-nav');
if (!container) return;
isDragging = true;
activeContainer = container;
activeDragBtn = dragBtn;
const rect = container.getBoundingClientRect();
dragOffset.x = e.clientX - rect.left;
dragOffset.y = e.clientY - rect.top;
container.style.transition = 'none';
dragBtn.style.cursor = 'grabbing';
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isDragging || !activeContainer) return;
e.preventDefault();
let x = e.clientX - dragOffset.x;
let y = e.clientY - dragOffset.y;
const maxX = window.innerWidth - activeContainer.offsetWidth;
const maxY = window.innerHeight - activeContainer.offsetHeight;
x = Math.max(0, Math.min(x, maxX));
y = Math.max(0, Math.min(y, maxY));
activeContainer.style.left = x + 'px';
activeContainer.style.top = y + 'px';
activeContainer.style.right = 'auto';
activeContainer.style.bottom = 'auto';
});
document.addEventListener('mouseup', () => {
if (!isDragging) return;
isDragging = false;
if (activeContainer) activeContainer.style.transition = '';
if (activeDragBtn) activeDragBtn.style.cursor = 'grab';
if (activeContainer) {
const left = parseInt(activeContainer.style.left) || 0;
const top = parseInt(activeContainer.style.top) || 0;
GM_setValue('acn_supermini_pos_' + window.location.hostname, JSON.stringify({ x: left, y: top }));
}
activeContainer = null;
activeDragBtn = null;
});
}
// ============================================================================
// 12.6 Responsive Mini mode
// ---------------------------------------------------------------------------
function shouldUseResponsiveMiniBar() {
return window.innerWidth > 0 && window.innerWidth <= RESPONSIVE_MINI_BREAKPOINT;
}
function applyResponsiveBarMode() {
if (isSuperMini || isClosedByUser) return;
if (shouldUseResponsiveMiniBar()) {
if (!isMiniBar) {
isResponsiveMiniActive = true;
applyBarMode('mini', { persist: false, source: 'responsive' });
}
return;
}
if (isResponsiveMiniActive && isMiniBar) {
isResponsiveMiniActive = false;
applyBarMode('max', { persist: false, source: 'responsive' });
} else {
isResponsiveMiniActive = false;
}
}
function scheduleResponsiveBarMode() {
if (responsiveResizeTimer) responsiveResizeTimer.clear();
responsiveResizeTimer = safeSetTimeout(() => {
responsiveResizeTimer = null;
applyResponsiveBarMode();
}, RESPONSIVE_RESIZE_DEBOUNCE_MS);
}
// 12.7 Navigator content rendering and interaction
// ---------------------------------------------------------------------------
// 12.8 Message badges and tooltips
// ---------------------------------------------------------------------------
function insertOrUpdateBadge(targetElRaw, number, isBookmarked, preferredInsertEl) {
let badge = targetElRaw.querySelector('.acn-message-inline-badge');
if (!badge) {
badge = document.createElement('span');
badge.className = 'acn-message-inline-badge';
badge.contentEditable = 'false';
let insertTarget = (preferredInsertEl && targetElRaw.contains(preferredInsertEl)) ? preferredInsertEl : targetElRaw;
let cornerOverlayParent = null;
let cornerOverlayClass = 'acn-message-inline-badge-corner';
let directInsertParent = null;
let directInsertBeforeSibling = null;
while (true) {
let firstMeaningful = null;
for (let i = 0; i < insertTarget.childNodes.length; i++) {
const node = insertTarget.childNodes[i];
if (node.nodeType === 3 && node.textContent.trim() === '') continue;
firstMeaningful = node;
break;
}
if (
firstMeaningful &&
firstMeaningful.nodeType === 1 &&
/\bsr-only\b/.test(firstMeaningful.className || '')
) {
firstMeaningful = firstMeaningful.nextElementSibling;
}
if (
IS_CHATGPT &&
firstMeaningful &&
firstMeaningful.nodeType === 1 &&
firstMeaningful.getAttribute &&
firstMeaningful.getAttribute('data-default-action') === 'true'
) {
cornerOverlayParent = insertTarget;
break;
}
if (
IS_CHATGPT &&
firstMeaningful &&
firstMeaningful.nodeType === 1 &&
firstMeaningful.tagName === 'BUTTON' &&
insertTarget.classList &&
insertTarget.classList.contains('group/message-image')
) {
const clippingWrapper = insertTarget.parentElement;
const row = clippingWrapper && clippingWrapper.parentElement;
if (row && clippingWrapper) {
directInsertParent = row;
directInsertBeforeSibling = clippingWrapper;
} else {
cornerOverlayParent = insertTarget;
cornerOverlayClass = 'acn-message-inline-badge-corner-inset';
}
break;
}
if (
firstMeaningful &&
firstMeaningful.nodeType === 1 &&
['P', 'DIV', 'SPAN', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(firstMeaningful.tagName)
) {
insertTarget = firstMeaningful;
} else {
break;
}
}
if (directInsertParent) {
directInsertParent.insertBefore(badge, directInsertBeforeSibling);
} else if (cornerOverlayParent) {
badge.classList.add(cornerOverlayClass);
cornerOverlayParent.insertBefore(badge, cornerOverlayParent.firstChild);
} else {
insertTarget.insertBefore(badge, insertTarget.firstChild);
}
}
if (badge.textContent !== String(number)) badge.textContent = number;
if (badge.classList.contains('bookmarked') !== !!isBookmarked) badge.classList.toggle('bookmarked', isBookmarked);
}
function updateMessageBadges(prompts) {
if (!IS_CLAUDE) {
const currentTargets = prompts.map(p => p.textElement || p.element).filter(Boolean);
document.querySelectorAll('.acn-message-inline-badge').forEach(badge => {
if (badge.closest('#acn-mini-tooltip')) return;
const stillValid = currentTargets.some(t => t.contains(badge));
if (!stillValid) badge.remove();
});
}
if (IS_CHATGPT) {
cleanupTOCBadgesGPT();
}
if (IS_CLAUDE) {
if (isClaudeSharePage()) {
promptCacheItems.forEach((item, index) => {
if (!item || !item.element || !item.element.isConnected) return;
insertOrUpdateBadge(item.element, index + 1, bookmarkedMessages.has(index + 1));
});
return;
}
if (!CURRENT_SITE.userMessage || !CURRENT_SITE.userMessage.container) return;
const liveNodes = Array.from(document.querySelectorAll(CURRENT_SITE.userMessage.container));
liveNodes.forEach(node => {
const textEl = node.querySelector(CURRENT_SITE.userMessage.text);
const text = textEl ? extractMessageTextDOMClaude(textEl) : extractMessageTextDOMClaude(node);
const cacheIndex = resolveCacheIndexClaude(node, text);
if (cacheIndex === -1) return;
insertOrUpdateBadge(node, cacheIndex + 1, bookmarkedMessages.has(cacheIndex + 1), textEl);
});
return;
}
prompts.forEach((prompt, index) => {
const targetEl = prompt.textElement || prompt.element;
if (!targetEl) return;
insertOrUpdateBadge(targetEl, index + 1, bookmarkedMessages.has(index + 1));
});
cleanupTOCBadgesGPT();
}
// ============================================================================
// 13. BOOKMARKS AND NAVIGATOR CONTENT
// ============================================================================
// 13.1 Bookmark storage and persistence
// ---------------------------------------------------------------------------
function getStorageKey() {
return BOOKMARK_PREFIX + window.location.hostname + window.location.pathname;
}
function saveBookmarks() {
const key = getStorageKey();
if (IS_CLAUDE) {
if (claudeBookmarkedUuids.size === 0 && claudeBookmarkedLegacyIndices.size === 0) {
GM_deleteValue(key);
return;
}
const parts = Array.from(claudeBookmarkedUuids);
claudeBookmarkedLegacyIndices.forEach(idx => parts.push('#' + idx));
const value = Date.now() + '|u:' + parts.join(',');
GM_setValue(key, value);
return;
}
const indices = Array.from(bookmarkedMessages);
if (indices.length === 0) {
GM_deleteValue(key);
return;
}
const value = Date.now() + '|' + indices.sort((a, b) => a - b).join(',');
GM_setValue(key, value);
}
function loadBookmarks() {
const key = getStorageKey();
const raw = GM_getValue(key, '');
claudeBookmarkedUuids = new Set();
claudeBookmarkedLegacyIndices = new Set();
if (!raw) {
bookmarkedMessages = new Set();
return;
}
const sep = raw.indexOf('|');
if (sep === -1) {
GM_deleteValue(key);
bookmarkedMessages = new Set();
return;
}
const savedAt = parseInt(raw.slice(0, sep));
const age = Date.now() - savedAt;
if (age > BOOKMARK_TTL_MS) {
GM_deleteValue(key);
bookmarkedMessages = new Set();
return;
}
const payload = raw.slice(sep + 1);
if (IS_CLAUDE && payload.startsWith('u:')) {
payload.slice(2).split(',').filter(Boolean).forEach(p => {
if (p.startsWith('#')) {
const n = Number(p.slice(1));
if (!isNaN(n) && n > 0) claudeBookmarkedLegacyIndices.add(n);
} else {
claudeBookmarkedUuids.add(p);
}
});
bookmarkedMessages = new Set();
rebuildBookmarkedIndicesFromUuidsClaude();
} else {
bookmarkedMessages = new Set(payload.split(',').map(Number).filter(n => !isNaN(n) && n > 0));
}
}
function rebuildBookmarkedIndicesFromUuidsClaude() {
if (!IS_CLAUDE) return false;
const next = new Set();
claudeBookmarkedUuids.forEach(uuid => {
if (claudeUuidToIndex.has(uuid)) next.add(claudeUuidToIndex.get(uuid) + 1);
});
claudeBookmarkedLegacyIndices.forEach(idx => {
if (idx > 0 && idx <= promptCacheItems.length) next.add(idx);
});
let changed = next.size !== bookmarkedMessages.size;
if (!changed) {
for (const v of next) { if (!bookmarkedMessages.has(v)) { changed = true; break; } }
}
bookmarkedMessages = next;
if (changed) {
updateBookmarkVisuals();
updateBookmarksHeader();
}
return changed;
}
function cleanupExpiredBookmarks() {
try {
GM_listValues().forEach(key => {
if (!key.startsWith(BOOKMARK_PREFIX)) return;
const raw = GM_getValue(key, '');
if (!raw) {
GM_deleteValue(key);
return;
}
const sep = raw.indexOf('|');
if (sep === -1) {
GM_deleteValue(key);
return;
}
const age = Date.now() - parseInt(raw.slice(0, sep));
if (age > BOOKMARK_TTL_MS) GM_deleteValue(key);
});
} catch (e) {}
}
function toggleBookmark(index) {
if (IS_CLAUDE) {
const item = promptCacheItems[index - 1];
const uuid = item && item._acnUuid;
if (uuid) {
if (claudeBookmarkedUuids.has(uuid)) claudeBookmarkedUuids.delete(uuid);
else claudeBookmarkedUuids.add(uuid);
} else {
if (claudeBookmarkedLegacyIndices.has(index)) claudeBookmarkedLegacyIndices.delete(index);
else claudeBookmarkedLegacyIndices.add(index);
}
rebuildBookmarkedIndicesFromUuidsClaude();
} else {
if (bookmarkedMessages.has(index)) {
bookmarkedMessages.delete(index);
} else {
bookmarkedMessages.add(index);
}
}
saveBookmarks();
updateBookmarkVisuals();
updateBookmarksHeader();
}
// 13.2 Bookmark visual state and interaction
// ---------------------------------------------------------------------------
function updateBookmarkVisuals() {
const content = document.getElementById('message-nav-content');
if (content) {
const list = content.querySelector('#nav-list');
if (list) {
list.querySelectorAll('.nav-max-item').forEach((item, idx) => {
const numberContainer = item.querySelector('.nav-max-number');
if (numberContainer) {
const index = idx + 1;
if (bookmarkedMessages.has(index)) {
numberContainer.textContent = `${index}`;
numberContainer.classList.add('bookmarked');
} else {
numberContainer.textContent = `${index}.`;
numberContainer.classList.remove('bookmarked');
}
}
});
}
}
const miniNums = document.getElementById('nav-mini-msg-numbers');
if (miniNums) {
miniNums.querySelectorAll('.nav-mini-num').forEach((numEl, idx) => {
const index = idx + 1;
if (bookmarkedMessages.has(index)) {
numEl.classList.add('bookmarked');
} else {
numEl.classList.remove('bookmarked');
}
});
}
document.querySelectorAll('.acn-message-inline-badge').forEach(badge => {
if (!badge.closest('#acn-mini-tooltip')) badge.remove();
});
updateSuperminiPanel();
}
function createCompactBookmarkEntry(index) {
const item = document.createElement('div');
item.className = 'acn-compact-bookmark-item';
if (index === activeMessageIndex) item.classList.add('active');
const num = document.createElement('span');
num.className = 'acn-compact-bookmark-num';
num.textContent = index;
const text = document.createElement('span');
text.className = 'acn-compact-bookmark-text';
const prompt = cachedPrompts[index - 1];
const fullText = prompt && prompt.text ? prompt.text : `Message ${index}`;
text.textContent = fullText.length > 55 ? fullText.slice(0, 55) + '...' : fullText;
item.title = fullText;
item.appendChild(num);
item.appendChild(text);
item.addEventListener('click', (e) => { e.stopPropagation(); navigateToMessage(index); });
return item;
}
function positionMiniBookmarksPanel() {
const trigger = document.getElementById('nav-mini-bookmarks');
const panel = document.getElementById('nav-mini-bookmarks-panel');
if (!trigger || !panel || !isMiniBar) return;
panel.style.setProperty('background-color', extractProtectedNavBgDOMClaude(), 'important');
const rect = trigger.getBoundingClientRect();
const panelWidth = 220;
const left = isNavOnLeftSide ? rect.right + 6 : rect.left - panelWidth - 6;
panel.style.left = Math.max(6, Math.min(window.innerWidth - panelWidth - 6, left)) + 'px';
panel.style.top = Math.max(6, Math.min(window.innerHeight - 286, rect.top)) + 'px';
}
function updateBookmarksHeader() {
const sortedBookmarks = Array.from(bookmarkedMessages).filter(index => index >= 1 && index <= cachedPrompts.length).sort((a, b) => a - b);
let bookmarksContainer = document.getElementById('nav-bookmarks-header');
const header = document.getElementById('nav-header');
if (header) {
if (sortedBookmarks.length === 0 || cachedPrompts.length < 6 || isMiniBar || isSuperMini) {
if (bookmarksContainer) bookmarksContainer.remove();
} else {
if (!bookmarksContainer) {
bookmarksContainer = document.createElement('div');
bookmarksContainer.id = 'nav-bookmarks-header';
bookmarksContainer.className = 'nav-bookmarks-container';
bookmarksContainer.style.top = header.offsetHeight + 'px';
header.parentNode.insertBefore(bookmarksContainer, header.nextSibling);
applyHeaderSolidBackgroundClaude();
}
bookmarksContainer.textContent = '';
sortedBookmarks.forEach(index => {
const bookmarkItem = document.createElement('span');
bookmarkItem.className = 'nav-bookmark-item';
bookmarkItem.textContent = index;
if (index === activeMessageIndex) bookmarkItem.classList.add('active');
bookmarkItem.addEventListener('click', () => navigateToMessage(index));
bookmarksContainer.appendChild(bookmarkItem);
});
}
}
const miniTrigger = document.getElementById('nav-mini-bookmarks');
const miniPanel = document.getElementById('nav-mini-bookmarks-panel');
if (miniTrigger && miniPanel) {
miniTrigger.classList.toggle('has-bookmarks', isMiniBar && sortedBookmarks.length > 0);
miniPanel.textContent = '';
sortedBookmarks.forEach(index => miniPanel.appendChild(createCompactBookmarkEntry(index)));
positionMiniBookmarksPanel();
}
const superRow = document.getElementById('supermini-bookmarks-row');
if (superRow) {
superRow.classList.toggle('has-bookmarks', isSuperMini && sortedBookmarks.length > 0);
superRow.textContent = '';
sortedBookmarks.forEach(index => {
const bookmarkItem = document.createElement('span');
bookmarkItem.className = 'nav-bookmark-item';
bookmarkItem.textContent = index;
if (index === activeMessageIndex) bookmarkItem.classList.add('active');
bookmarkItem.title = cachedPrompts[index - 1]?.text || `Message ${index}`;
bookmarkItem.addEventListener('click', (e) => { e.stopPropagation(); navigateToMessage(index); });
superRow.appendChild(bookmarkItem);
});
}
}
// 13.3 Message counters
// ---------------------------------------------------------------------------
function updateMessageCounter() {
const counterSpan = document.getElementById('nav-message-counter');
if (counterSpan) {
const total = cachedPrompts.length;
const current = total > 0
? Math.min(Math.max(activeMessageIndex > 0 ? activeMessageIndex : 1, 1), total)
: 0;
if (activeMessageIndex !== current) activeMessageIndex = current > 0 ? current : -1;
counterSpan.textContent = '';
const numSpan = document.createElement('span');
numSpan.className = 'counter-num';
numSpan.textContent = current;
const divSpan = document.createElement('span');
divSpan.className = 'counter-divider';
divSpan.textContent = '/';
const totalSpan = document.createElement('span');
totalSpan.className = 'counter-total';
totalSpan.textContent = total;
counterSpan.appendChild(numSpan);
counterSpan.appendChild(divSpan);
counterSpan.appendChild(totalSpan);
}
updateSuperminiInputNav();
const miniNums = document.getElementById('nav-mini-msg-numbers');
const miniWrapperCount = miniNums ? miniNums.querySelectorAll(':scope > .nav-mini-item-wrapper').length : 0;
if (miniNums && miniWrapperCount === cachedPrompts.length) {
Array.from(miniNums.querySelectorAll(':scope > .nav-mini-item-wrapper')).forEach((wrapper, idx) => {
const numEl = wrapper.querySelector('.nav-mini-num');
if (!numEl) return;
const i = idx + 1;
numEl.classList.toggle('active', i === (IS_CLAUDE ? claudeCurrentVisibleIndex : activeMessageIndex));
if (IS_CLAUDE || IS_CHATGPT || IS_GEMINI || IS_KIMI || IS_GROK) {
if (IS_CLAUDE) numEl.classList.toggle('acn-clicked', i === claudeLastClickedIndex);
const prompt = cachedPrompts[idx];
const hasFiles = prompt && prompt.generatedFiles && prompt.generatedFiles.length > 0;
const existingFav = wrapper.querySelector('.acn-genfile-indicator');
if (hasFiles && !existingFav) {
numEl.classList.add('has-genfiles');
const favEl = document.createElement('img');
favEl.className = 'acn-genfile-indicator';
favEl.src = getSiteFaviconUrl();
favEl.alt = '';
wrapper.appendChild(favEl);
} else if (!hasFiles && existingFav) {
existingFav.remove();
numEl.classList.remove('has-genfiles');
}
}
});
} else if (miniNums) {
const tooltipEl = document.getElementById('acn-mini-tooltip');
if (tooltipEl) tooltipEl.style.display = 'none';
while (miniNums.firstChild) miniNums.removeChild(miniNums.firstChild);
for (let i = 1; i <= cachedPrompts.length; i++) {
const prompt = cachedPrompts[i - 1];
const wrapper = document.createElement('div');
wrapper.className = 'nav-mini-item-wrapper';
const row = document.createElement('div');
row.className = 'nav-mini-row';
const numEl = document.createElement('span');
numEl.className = 'nav-mini-num';
numEl.textContent = i;
if ((IS_CLAUDE || IS_CHATGPT || IS_GEMINI || IS_KIMI || IS_GROK) && prompt.generatedFiles && prompt.generatedFiles.length > 0) {
numEl.classList.add('has-genfiles');
const favEl = document.createElement('img');
favEl.className = 'acn-genfile-indicator';
favEl.src = getSiteFaviconUrl();
favEl.alt = '';
wrapper.appendChild(favEl);
}
if (i === (IS_CLAUDE ? claudeCurrentVisibleIndex : activeMessageIndex)) {
numEl.classList.add('active');
if (isMiniBar) {
safeSetTimeout(() => {
const parent = wrapper.parentNode;
if (parent) {
parent.scrollTo({ top: wrapper.offsetTop - parent.offsetTop - (parent.clientHeight / 2) + (wrapper.clientHeight / 2), behavior: 'smooth' });
}
}, 50);
}
}
if (IS_CLAUDE && i === claudeLastClickedIndex) numEl.classList.add('acn-clicked');
if (bookmarkedMessages.has(i)) numEl.classList.add('bookmarked');
let miniClickTimer = null;
numEl.addEventListener('click', (e) => {
e.stopPropagation();
if (miniClickTimer) clearTimeout(miniClickTimer);
miniClickTimer = setTimeout(() => {
miniClickTimer = null;
navigateToMessage(i);
}, 220);
});
numEl.addEventListener('dblclick', (e) => {
e.preventDefault();
e.stopPropagation();
if (miniClickTimer) clearTimeout(miniClickTimer);
miniClickTimer = null;
toggleBookmark(i);
});
wrapper.addEventListener('mouseenter', (e) => showMiniTooltip(e, i));
wrapper.addEventListener('mouseleave', (e) => scheduleHideMiniTooltip());
if (prompt.versionInfo && prompt.versionInfo.hasMultipleVersions) {
const sup = document.createElement('span');
sup.className = 'nav-mini-version-label';
sup.textContent = `${prompt.versionInfo.currentVersion}/${prompt.versionInfo.totalVersions}`;
const prevBtn = document.createElement('button');
prevBtn.className = 'nav-mini-vbtn';
prevBtn.innerHTML = SYMBOL_PREVIOUS;
prevBtn.disabled = IS_CLAUDE ? isVersionButtonDisabledClaude(prompt.versionInfo, 'prevButton') : (!prompt.versionInfo.prevButton || prompt.versionInfo.prevButton.disabled);
prevBtn.addEventListener('click', async (e) => {
e.stopPropagation();
if (IS_CLAUDE) {
prevBtn.disabled = true;
nextBtn.disabled = true;
await clickVersionButtonClaude(i, 'prevButton', prompt._acnUuid);
safeSetTimeout(() => updateMessageList(true), 300);
} else if (IS_CHATGPT) {
prevBtn.disabled = true;
nextBtn.disabled = true;
await switchVersionGPT(i, 'prevButton');
} else if (prompt.versionInfo.prevButton && !prompt.versionInfo.prevButton.disabled) {
prompt.versionInfo.prevButton.click();
safeSetTimeout(() => updateMessageList(true), 300);
}
});
const nextBtn = document.createElement('button');
nextBtn.className = 'nav-mini-vbtn';
nextBtn.innerHTML = SYMBOL_NEXT;
nextBtn.disabled = IS_CLAUDE ? isVersionButtonDisabledClaude(prompt.versionInfo, 'nextButton') : (!prompt.versionInfo.nextButton || prompt.versionInfo.nextButton.disabled);
nextBtn.addEventListener('click', async (e) => {
e.stopPropagation();
if (IS_CLAUDE) {
prevBtn.disabled = true;
nextBtn.disabled = true;
await clickVersionButtonClaude(i, 'nextButton', prompt._acnUuid);
safeSetTimeout(() => updateMessageList(true), 300);
} else if (IS_CHATGPT) {
prevBtn.disabled = true;
nextBtn.disabled = true;
await switchVersionGPT(i, 'nextButton');
} else if (prompt.versionInfo.nextButton && !prompt.versionInfo.nextButton.disabled) {
prompt.versionInfo.nextButton.click();
safeSetTimeout(() => updateMessageList(true), 300);
}
});
row.appendChild(numEl);
const branchRow = document.createElement('div');
branchRow.className = 'nav-mini-version-row';
branchRow.appendChild(prevBtn);
branchRow.appendChild(sup);
branchRow.appendChild(nextBtn);
wrapper.appendChild(branchRow);
wrapper.appendChild(row);
} else {
row.appendChild(numEl);
wrapper.appendChild(row);
}
miniNums.appendChild(wrapper);
}
if (siteUsesDateLine()) renderNavDateDecorations(miniNums);
}
checkMiniScrollVisibility();
updateSuperminiPanel();
}
function applySuperminiScrollHeightClaude() {
const scrollInner = document.getElementById('supermini-scroll-inner');
const list = document.getElementById('supermini-message-list');
if (!scrollInner || !list) return;
const items = Array.from(list.children);
const visibleCount = Math.min(items.length, 10);
let visibleHeight = 0;
for (let i = 0; i < visibleCount; i++) {
const item = items[i];
const style = getComputedStyle(item);
visibleHeight += item.offsetHeight
+ parseFloat(style.marginTop || 0)
+ parseFloat(style.marginBottom || 0);
}
scrollInner.style.maxHeight = visibleHeight > 0 ? `${visibleHeight}px` : 'none';
scrollInner.style.overflowY = items.length > 10 ? 'auto' : 'hidden';
}
// 13.4 Compact and Supermini content
// ---------------------------------------------------------------------------
function updateSuperminiPanel() {
const panel = document.getElementById('supermini-message-panel');
if (!panel) return;
if (!isSuperMini || cachedPrompts.length === 0) return;
if (!panel.dataset.acnHoverRerenderBound) {
panel.dataset.acnHoverRerenderBound = '1';
const rerenderSupermini = () => {
realSetTimeoutClaude(() => {
applySuperminiScrollHeightClaude();
if (siteUsesDateLine()) {
const list = document.getElementById('supermini-message-list');
if (list) renderNavDateDecorations(list);
}
}, 20);
};
panel.addEventListener('mouseenter', rerenderSupermini);
const messageNav = document.getElementById('message-nav');
if (messageNav) messageNav.addEventListener('mouseenter', rerenderSupermini);
}
let bookmarksRow = document.getElementById('supermini-bookmarks-row');
if (!bookmarksRow) {
bookmarksRow = document.createElement('div');
bookmarksRow.id = 'supermini-bookmarks-row';
panel.insertBefore(bookmarksRow, panel.firstChild);
}
let scrollInner = document.getElementById('supermini-scroll-inner');
if (!scrollInner) {
scrollInner = document.createElement('div');
scrollInner.id = 'supermini-scroll-inner';
panel.appendChild(scrollInner);
}
let list = document.getElementById('supermini-message-list');
if (!list) {
list = document.createElement('ul');
list.id = 'supermini-message-list';
scrollInner.appendChild(list);
}
const signature = cachedPrompts.length + '|' + activeMessageIndex + '|' + cachedPrompts.map((p, idx) => {
const bookmark = bookmarkedMessages.has(idx + 1) ? 'b' : '';
const files = p.generatedFiles ? p.generatedFiles.length : 0;
const version = (p.versionInfo && p.versionInfo.hasMultipleVersions) ? p.versionInfo.currentVersion + '/' + p.versionInfo.totalVersions : '';
return p.text.slice(0, 45) + '~' + bookmark + '~' + files + '~' + version;
}).join('|');
if (list.dataset.acnSignature !== signature) {
list.dataset.acnSignature = signature;
while (list.firstChild) list.removeChild(list.firstChild);
for (let i = 1; i <= cachedPrompts.length; i++) {
const li = document.createElement('li');
const prompt = cachedPrompts[i - 1];
const preview = prompt.text.length > 45 ? prompt.text.slice(0, 45) + '...' : prompt.text;
if (i === activeMessageIndex) li.classList.add('active');
const numSpan = document.createElement('span');
numSpan.className = 'nav-superm-num';
numSpan.textContent = i + '.';
if (bookmarkedMessages.has(i)) numSpan.classList.add('bookmarked');
if (prompt.versionInfo && prompt.versionInfo.hasMultipleVersions) {
const wrapper = document.createElement('div');
wrapper.className = 'nav-superm-number-wrapper';
const sup = document.createElement('span');
sup.className = 'nav-superm-version-label';
sup.textContent = `${prompt.versionInfo.currentVersion}/${prompt.versionInfo.totalVersions}`;
const prevBtn = document.createElement('button');
prevBtn.className = 'nav-superm-version-btn';
prevBtn.innerHTML = SYMBOL_PREVIOUS;
prevBtn.disabled = IS_CLAUDE ? isVersionButtonDisabledClaude(prompt.versionInfo, 'prevButton') : (!prompt.versionInfo.prevButton || prompt.versionInfo.prevButton.disabled);
prevBtn.addEventListener('click', async (e) => {
e.stopPropagation();
if (IS_CLAUDE) {
prevBtn.disabled = true;
nextBtn.disabled = true;
await clickVersionButtonClaude(i, 'prevButton', prompt._acnUuid);
safeSetTimeout(() => updateMessageList(true), 300);
} else if (IS_CHATGPT) {
prevBtn.disabled = true;
nextBtn.disabled = true;
await switchVersionGPT(i, 'prevButton');
} else if (prompt.versionInfo.prevButton && !prompt.versionInfo.prevButton.disabled) {
prompt.versionInfo.prevButton.click();
safeSetTimeout(() => updateMessageList(true), 300);
}
});
const nextBtn = document.createElement('button');
nextBtn.className = 'nav-superm-version-btn';
nextBtn.innerHTML = SYMBOL_NEXT;
nextBtn.disabled = IS_CLAUDE ? isVersionButtonDisabledClaude(prompt.versionInfo, 'nextButton') : (!prompt.versionInfo.nextButton || prompt.versionInfo.nextButton.disabled);
nextBtn.addEventListener('click', async (e) => {
e.stopPropagation();
if (IS_CLAUDE) {
prevBtn.disabled = true;
nextBtn.disabled = true;
await clickVersionButtonClaude(i, 'nextButton', prompt._acnUuid);
safeSetTimeout(() => updateMessageList(true), 300);
} else if (IS_CHATGPT) {
prevBtn.disabled = true;
nextBtn.disabled = true;
await switchVersionGPT(i, 'nextButton');
} else if (prompt.versionInfo.nextButton && !prompt.versionInfo.nextButton.disabled) {
prompt.versionInfo.nextButton.click();
safeSetTimeout(() => updateMessageList(true), 300);
}
});
const arrowsGroup = document.createElement('span');
arrowsGroup.className = 'nav-superm-version-arrows';
arrowsGroup.appendChild(prevBtn);
arrowsGroup.appendChild(nextBtn);
arrowsGroup.appendChild(sup);
wrapper.appendChild(numSpan);
wrapper.appendChild(arrowsGroup);
li.appendChild(wrapper);
} else {
li.appendChild(numSpan);
}
if (prompt.files && prompt.files.length > 0) {
const userFiles = document.createElement('span');
userFiles.className = 'nav-superm-userfiles';
prompt.files.forEach((f, fileIdx) => {
userFiles.appendChild(createUserFileBadge(f, i, fileIdx, 'supermini', prompt));
});
li.appendChild(userFiles);
}
const textSpan = document.createElement('span');
textSpan.className = 'nav-superm-text';
textSpan.textContent = preview;
const dateTooltipText = siteUsesDateLine() ? buildMessageDateTooltipText(prompt) : '';
li.title = dateTooltipText ? `${prompt.text}\n\n${dateTooltipText}` : prompt.text;
li.appendChild(textSpan);
if ((IS_CLAUDE || IS_CHATGPT || IS_GEMINI || IS_KIMI || IS_GROK) && prompt.generatedFiles && prompt.generatedFiles.length > 0) {
li.appendChild(createGeneratedFilesRow(prompt.generatedFiles, i, 'supermini'));
}
let superminiClickTimer = null;
li.addEventListener('click', (e) => {
if (e.target.closest('.nav-superm-version-btn')) return;
if (e.target.closest('.nav-superm-num')) {
e.stopPropagation();
if (superminiClickTimer) clearTimeout(superminiClickTimer);
superminiClickTimer = setTimeout(() => {
superminiClickTimer = null;
navigateToMessage(i);
}, 220);
return;
}
navigateToMessage(i);
});
numSpan.addEventListener('dblclick', (e) => {
e.preventDefault();
e.stopPropagation();
if (superminiClickTimer) clearTimeout(superminiClickTimer);
superminiClickTimer = null;
toggleBookmark(i);
});
list.appendChild(li);
}
requestAnimationFrame(() => {
applySuperminiScrollHeightClaude();
if (siteUsesDateLine()) renderNavDateDecorations(list);
});
}
updateBookmarksHeader();
}
function scheduleHideMiniTooltip() {
clearTimeout(tooltipTimeout);
tooltipTimeout = setTimeout(() => {
const tooltipEl = document.getElementById('acn-mini-tooltip');
if (tooltipEl) tooltipEl.style.display = 'none';
}, 200);
}
function showMiniTooltip(event, index) {
clearTimeout(tooltipTimeout);
const tooltipEl = document.getElementById('acn-mini-tooltip');
if (!tooltipEl || !cachedPrompts[index - 1]) return;
tooltipEl.style.setProperty('background-color', extractProtectedNavBgDOMClaude(), 'important');
tooltipEl.textContent = '';
const prompt = cachedPrompts[index - 1];
const preview = prompt.text.length > 80 ? prompt.text.slice(0, 80) + '...' : prompt.text;
const badgeEl = document.createElement('span');
badgeEl.className = 'acn-message-inline-badge';
badgeEl.textContent = String(index);
if (bookmarkedMessages.has(index)) badgeEl.classList.add('bookmarked');
badgeEl.addEventListener('click', (e) => {
e.stopPropagation();
toggleBookmark(index);
badgeEl.classList.toggle('bookmarked', bookmarkedMessages.has(index));
});
tooltipEl.appendChild(badgeEl);
if (prompt.files && prompt.files.length > 0) {
prompt.files.forEach((f, fileIdx) => {
tooltipEl.appendChild(createUserFileBadge(f, index, fileIdx, 'max', prompt));
});
}
const textSpan = document.createElement('span');
textSpan.className = 'nav-max-preview';
textSpan.textContent = (prompt.files && prompt.files.length > 0) ? ` ${preview}` : preview;
tooltipEl.appendChild(textSpan);
if ((IS_CLAUDE || IS_CHATGPT || IS_GEMINI || IS_KIMI || IS_GROK) && prompt.generatedFiles && prompt.generatedFiles.length > 0) {
tooltipEl.appendChild(createGeneratedFilesRow(prompt.generatedFiles, index));
}
const dateTooltipText = buildMessageDateTooltipText(prompt);
if (dateTooltipText) {
const dateLine = document.createElement('div');
dateLine.className = 'nav-item-date-line';
dateLine.textContent = dateTooltipText;
tooltipEl.appendChild(dateLine);
}
if (prompt.versionInfo && prompt.versionInfo.hasMultipleVersions) {
const versionControls = document.createElement('span');
versionControls.className = 'nav-max-version-controls';
const prevVersionBtn = document.createElement('button');
prevVersionBtn.className = 'nav-max-version-btn';
prevVersionBtn.textContent = '◀';
prevVersionBtn.disabled = IS_CLAUDE ? isVersionButtonDisabledClaude(prompt.versionInfo, 'prevButton') : (!prompt.versionInfo.prevButton || prompt.versionInfo.prevButton.disabled);
prevVersionBtn.addEventListener('click', async (e) => {
e.stopPropagation();
if (IS_CLAUDE) {
prevVersionBtn.disabled = true;
nextVersionBtn.disabled = true;
await clickVersionButtonClaude(index, 'prevButton', prompt._acnUuid);
safeSetTimeout(() => updateMessageList(true), 300);
} else if (IS_CHATGPT) {
prevVersionBtn.disabled = true;
nextVersionBtn.disabled = true;
await switchVersionGPT(index, 'prevButton');
} else if (prompt.versionInfo.prevButton && !prompt.versionInfo.prevButton.disabled) {
prompt.versionInfo.prevButton.click();
safeSetTimeout(() => updateMessageList(true), 300);
}
});
const versionText = document.createElement('span');
versionText.className = 'nav-max-version-text';
versionText.textContent = `${prompt.versionInfo.currentVersion}/${prompt.versionInfo.totalVersions}`;
const nextVersionBtn = document.createElement('button');
nextVersionBtn.className = 'nav-max-version-btn';
nextVersionBtn.textContent = '▶';
nextVersionBtn.disabled = IS_CLAUDE ? isVersionButtonDisabledClaude(prompt.versionInfo, 'nextButton') : (!prompt.versionInfo.nextButton || prompt.versionInfo.nextButton.disabled);
nextVersionBtn.addEventListener('click', async (e) => {
e.stopPropagation();
if (IS_CLAUDE) {
prevVersionBtn.disabled = true;
nextVersionBtn.disabled = true;
await clickVersionButtonClaude(index, 'nextButton', prompt._acnUuid);
safeSetTimeout(() => updateMessageList(true), 300);
} else if (IS_CHATGPT) {
prevVersionBtn.disabled = true;
nextVersionBtn.disabled = true;
await switchVersionGPT(index, 'nextButton');
} else if (prompt.versionInfo.nextButton && !prompt.versionInfo.nextButton.disabled) {
prompt.versionInfo.nextButton.click();
safeSetTimeout(() => updateMessageList(true), 300);
}
});
versionControls.appendChild(prevVersionBtn);
versionControls.appendChild(versionText);
versionControls.appendChild(nextVersionBtn);
tooltipEl.appendChild(versionControls);
}
if (!tooltipEl.dataset.acnHoverBound) {
tooltipEl.dataset.acnHoverBound = '1';
tooltipEl.addEventListener('mouseenter', () => clearTimeout(tooltipTimeout));
tooltipEl.addEventListener('mouseleave', () => scheduleHideMiniTooltip());
}
tooltipEl.style.visibility = 'hidden';
tooltipEl.style.display = 'block';
tooltipEl.style.left = '0px';
tooltipEl.style.top = '0px';
const tWidth = tooltipEl.offsetWidth;
const tHeight = tooltipEl.offsetHeight;
const viewportWidth = document.documentElement.clientWidth;
const viewportHeight = document.documentElement.clientHeight;
const gap = 12;
const triggerRect = event.currentTarget && event.currentTarget.getBoundingClientRect
? event.currentTarget.getBoundingClientRect()
: null;
let left;
if (isNavOnLeftSide) {
const navWidth = IS_CLAUDE ? 52 : NAV_MINI_WIDTH;
const anchorRight = triggerRect ? triggerRect.right : navWidth;
left = Math.max(navWidth, anchorRight) + gap;
if (left + tWidth > viewportWidth - 5) {
left = viewportWidth - tWidth - 5;
}
} else {
const anchorLeft = triggerRect ? triggerRect.left : event.clientX;
left = anchorLeft - tWidth - gap;
const maxLeft = viewportWidth - (IS_CLAUDE ? 52 : NAV_MINI_WIDTH) - tWidth - gap;
if (left > maxLeft) left = maxLeft;
}
if (left < 5) left = 5;
let top = event.clientY - (tHeight / 2);
if (top + tHeight > viewportHeight) {
top = viewportHeight - tHeight - 5;
}
if (top < 5) top = 5;
tooltipEl.style.left = `${left}px`;
tooltipEl.style.top = `${top}px`;
tooltipEl.style.visibility = 'visible';
}
// ============================================================================
// 14. MESSAGE SCANNING AND METADATA HARVESTING
// ============================================================================
// 14.1 Scan state and shared helpers
// ---------------------------------------------------------------------------
function getPromptListScanSignature() {
return cachedPrompts.map((prompt, index) => {
if (prompt && prompt._acnKey) return prompt._acnKey;
return getPromptStableKey(prompt || { text: '' }, index);
}).join('||');
}
function setScanButtonState(isScanning, title) {
const scanBtn = document.getElementById('nav-btn-scan');
if (scanBtn) {
scanBtn.classList.toggle('scanning', Boolean(isScanning));
scanBtn.textContent = isScanning
? SYMBOL_SCANNING
: ((IS_CLAUDE || IS_CHATGPT) ? SYMBOL_HARVEST_MOUNT_POSITION : SYMBOL_SCAN);
scanBtn.title = title || (isScanning ? 'Scanning...' : 'Scan older messages');
}
if (isScanning) {
showStatusOverlayClaude((title || 'Scanning...') + '\nKeep this tab focused for best performance');
} else {
hideStatusOverlayClaude();
}
}
// 14.2 Slow scan: Claude and ChatGPT metadata harvesting
// ---------------------------------------------------------------------------
async function runFullHistorySweepOnceGPT(sweepLabelPrefix = 'Preparing quick navigation...') {
if (!IS_CHATGPT || !isTOCEnabledGPT()) return { sentinelFullyExhausted: false, stalledOut: false };
if (isScanningOldMessages) return { sentinelFullyExhausted: false, stalledOut: false };
const root = getConversationScrollRoot();
if (!root) return { sentinelFullyExhausted: false, stalledOut: false };
const sweepButtons = extractTOCButtonsDOMGPT();
const originalIndex = extractActiveTOCIndexDOMGPT(sweepButtons);
isScanningOldMessages = true;
chatGptHighlightSuppressionDepth++;
showRootCoverOverlayGPT(root);
let sentinelFullyExhausted = false;
let stalledOut = false;
try {
await ensureChatGPTApiConversationCache();
const apiCache = chatGptApiConversationCache;
const knownTotal = apiCache && Array.isArray(apiCache.userMessages) ? apiCache.userMessages.length : null;
let maxPctSeen = 0;
const describeProgress = () => {
if (!knownTotal) return '';
const ordinal = findActiveOrdinalFromVisibleDOMGPT();
if (ordinal < 0) return '';
const pct = Math.round(((knownTotal - ordinal) / knownTotal) * 100);
const clamped = Math.min(99, Math.max(0, pct));
maxPctSeen = Math.max(maxPctSeen, clamped);
return ` (${maxPctSeen}%)`;
};
const showToast = () => showStatusOverlayClaude(`${sweepLabelPrefix}${describeProgress()}\nThis will happen only once per conversation and future jumps will be instant\nKeep this tab focused for best performance`);
showToast();
let sentinelRounds = 0;
const maxSentinelRounds = 500;
while (isScanningOldMessages && sentinelRounds < maxSentinelRounds) {
const stillHasSentinel = await loadOlderChatGPTMessagesViaSentinel(1);
sentinelRounds++;
if (stillHasSentinel === null) { stalledOut = true; break; }
showToast();
if (stillHasSentinel === false) { sentinelFullyExhausted = true; break; }
}
const oldestMessageId = knownTotal ? apiCache.userMessages[0].id : null;
if (sentinelFullyExhausted && oldestMessageId) {
recordChatGPTPaginationRounds(oldestMessageId, sentinelRounds);
}
if (originalIndex >= 0) {
await navigateToMessage(originalIndex + 1);
}
await updateMessageList(true);
} finally {
hideRootCoverOverlayGPT();
hideStatusOverlayClaude();
isScanningOldMessages = false;
chatGptHighlightSuppressionDepth--;
applyUniversalActiveMessage(activeMessageIndex);
if (stalledOut) {
showStatusOverlayClaude('Quick navigation setup stalled. Please retry.', { showSpinner: false });
setTimeout(hideStatusOverlayClaude, 2500);
}
}
return { sentinelFullyExhausted, stalledOut };
}
async function slowScan() {
if (IS_CLAUDE) {
await harvestMountPositionsClaude();
return true;
}
if (!IS_CHATGPT || !isTOCEnabledGPT()) return false;
return false;
}
// 14.3 Fast scan: history loading to the latest message
// ---------------------------------------------------------------------------
async function fastScan() {
const maxRounds = 80;
const stableRoundLimit = 3;
const waitMs = 850;
let stableRounds = 0;
try {
await updateMessageList(true);
for (let round = 0; round < maxRounds && isScanningOldMessages; round++) {
if (!cachedPrompts.length) break;
const beforeCount = cachedPrompts.length;
const beforeSignature = getPromptListScanSignature();
navigateToMessage(1);
const root = getConversationScrollRoot();
if (root) setScrollTop(root, 0, 'auto');
await sleep(waitMs);
rememberVisiblePromptsForCache(extractVisibleUserPromptsDOM(), 'prepend');
await updateMessageList(true);
const afterCount = cachedPrompts.length;
const afterSignature = getPromptListScanSignature();
const foundOlder = afterCount > beforeCount || afterSignature !== beforeSignature;
if (foundOlder) stableRounds = 0;
else stableRounds += 1;
setScanButtonState(true, 'Fast scanning...');
if (stableRounds >= stableRoundLimit) break;
}
if (isScanningOldMessages) {
await updateMessageList(true);
if (cachedPrompts.length) {
await navigateToMessage(cachedPrompts.length);
await sleep(120);
}
}
} finally {
isScanningOldMessages = false;
setScanButtonState(false, 'Scan messages');
await updateMessageList(true);
}
}
// 14.4 Site-specific scan dispatch
// ---------------------------------------------------------------------------
async function scanOlderMessages() {
if (isScanningOldMessages) {
isScanningOldMessages = false;
setScanButtonState(false, IS_CHATGPT ? 'Prepare and enable instant navigation' : 'Scan messages');
return;
}
if (IS_CLAUDE) {
await slowScan();
return;
}
if (IS_CHATGPT && isTOCEnabledGPT()) {
const sweepConversationId = getChatGPTConversationIdFromUrlAPI();
if (sweepConversationId && chatGptFullyLoadedConversationIds.has(sweepConversationId)) {
showStatusOverlayClaude('Quick navigation already active for this conversation', { showSpinner: false });
setTimeout(hideStatusOverlayClaude, 1800);
return;
}
const sweepResult = await runFullHistorySweepOnceGPT();
if (sweepConversationId && !sweepResult.stalledOut) {
chatGptFullyLoadedConversationIds.add(sweepConversationId);
}
return;
}
isScanningOldMessages = true;
setScanButtonState(true, (IS_CHATGPT && isTOCEnabledGPT()) ? 'Slow scanning...' : 'Fast scanning...');
if (IS_CHATGPT && isTOCEnabledGPT()) {
await slowScan();
} else {
await fastScan();
}
}
// ============================================================================
// 15. SHARED FILE BADGES AND FILE ACTIONS
// ============================================================================
// 15.1 Download, focus, and adapter dispatch
// ---------------------------------------------------------------------------
async function downloadBlobUrlAsFile(url, filename, headers) {
if (!url) return false;
const debug = typeof unsafeWindow !== 'undefined' && unsafeWindow.__acnChatGPTScrollDebug;
try {
const response = await fetch(url, { credentials: 'include', headers: headers || {} });
if (debug) console.log('[ACN-GPT-USERFILE]', { stage: 'downloadBlobUrlAsFile fetch response', url, status: response.status, ok: response.ok, contentType: response.headers.get('content-type') });
if (!response.ok) return false;
const blob = await response.blob();
if (debug) console.log('[ACN-GPT-USERFILE]', { stage: 'blob received', size: blob.size, type: blob.type });
const objectUrl = URL.createObjectURL(blob);
try {
const a = document.createElement('a');
a.href = objectUrl;
a.download = filename || 'image';
a.rel = 'noopener';
document.body.appendChild(a);
a.click();
a.remove();
} finally {
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 1500);
}
return true;
} catch (err) {
if (debug) console.log('[ACN-GPT-USERFILE]', { stage: 'downloadBlobUrlAsFile threw', error: String(err) });
return false;
}
}
async function focusFileElement(target, options = {}) {
if (!(target instanceof HTMLElement)) return false;
const { wait = sleep, actionDelay = 180, highlightMs = 1800, behavior = 'smooth', block = 'center' } = options;
target.scrollIntoView({ behavior, block, inline: 'nearest' });
target.classList.remove('acn-generated-file-target');
void target.offsetWidth;
target.classList.add('acn-generated-file-target');
safeSetTimeout(() => {
if (target.isConnected) target.classList.remove('acn-generated-file-target');
}, highlightMs);
if (actionDelay > 0) await wait(actionDelay);
return true;
}
async function activateDomGeneratedFile({ findTarget, navigate, performAction, attempts = 10, earlyDelay = 120, laterDelay = 200, earlyAttempts = 3, wait = sleep }) {
let result = findTarget();
if (!result && navigate) {
await navigate();
for (let attempt = 0; attempt < attempts; attempt++) {
await wait(attempt < earlyAttempts ? earlyDelay : laterDelay);
result = findTarget();
if (result) break;
}
}
if (!result) return false;
const target = result instanceof HTMLElement ? result : (result.target instanceof HTMLElement ? result.target : result.actionElement);
if (!(target instanceof HTMLElement)) return false;
await focusFileElement(target, { wait });
if (performAction) await performAction(result);
return true;
}
async function activateUserFile(file, index, fileIdx, mode = 'default') {
if (currentFileAdapter && typeof currentFileAdapter.activateUserFile === 'function') {
return currentFileAdapter.activateUserFile(file, index, fileIdx, mode);
}
return navigateToMessage(index);
}
async function activateFile(file, index, fileIdx, mode = 'default') {
if (typeof unsafeWindow !== 'undefined' && unsafeWindow.__acnChatGPTScrollDebug && file && file.kind === 'chatgpt-writing-block') {
console.log('[ACN-GPT-WRITEBLOCK]', { stage: 'activateFile called (badge click entry point)', index, fileIdx, file, mode });
}
if (currentFileAdapter && typeof currentFileAdapter.activateGeneratedFile === 'function') {
return currentFileAdapter.activateGeneratedFile(file, index, fileIdx, mode);
}
return false;
}
// 15.2 File badge rendering
// ---------------------------------------------------------------------------
function createUserFileBadge(file, index, fileIdx, variant = 'max', prompt = null) {
const badge = document.createElement('span');
badge.className = variant === 'supermini' ? 'nav-superm-userfile-badge' : 'nav-file-tag';
const labelExt = file && (file.displayExt || file.ext);
if (variant === 'supermini') {
const shortLabel = String(labelExt || (file && file.name) || 'f').trim().charAt(0);
badge.textContent = shortLabel ? shortLabel.toLowerCase() : 'f';
} else {
badge.textContent = labelExt || 'file';
}
const titleParts = [];
if (file && file.name) titleParts.push(file.name);
if (file && file.size) titleParts.push('(' + file.size + ')');
if (variant !== 'supermini' && prompt) {
const fileDateText = siteUsesDateLine() ? buildMessageDateTooltipText(prompt) : '';
if (fileDateText) titleParts.push(String.fromCharCode(10) + fileDateText);
if (file && file.preview) titleParts.push(String.fromCharCode(10) + file.preview);
}
if (IS_CHATGPT && variant !== 'supermini') {
titleParts.push(String.fromCharCode(10) + 'Click to download');
if (isChatGPTAttachmentPreviewSupportedGPT(file)) {
titleParts.push(String.fromCharCode(10) + 'Ctrl+click to preview');
}
}
badge.title = titleParts.join(' ') || 'File';
badge.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
const modifierPreview = IS_CHATGPT && (e.ctrlKey || e.metaKey);
const mode = variant === 'supermini' ? 'attachment-preview' : (modifierPreview ? 'attachment-preview' : 'default');
await activateUserFile(file, index, fileIdx, mode);
});
return badge;
}
function getGeneratedFileBadgeLabel(file) {
if (!file) return 'file';
if (file.kind === 'artifact') return 'artifact';
if (file.kind === 'chatgpt-writing-block') return 'writing block';
return file.ext ? String(file.ext).toLowerCase() : 'file';
}
function createGeneratedFilesRow(files, index, variant = 'max') {
const row = document.createElement(variant === 'supermini' ? 'span' : 'div');
row.className = variant === 'supermini' ? 'acn-genfiles-row-sm' : 'acn-genfiles-row';
const favicon = document.createElement('img');
favicon.className = 'acn-genfiles-row-favicon';
favicon.src = getSiteFaviconUrl();
favicon.alt = '';
favicon.title = 'AI-generated files';
row.appendChild(favicon);
files.forEach((f, fileIdx) => row.appendChild(createGeneratedFileBadge(f, index, fileIdx, variant)));
return row;
}
function createGeneratedFileBadge(file, index, fileIdx, variant = 'max') {
const badge = document.createElement('span');
badge.className = variant === 'supermini'
? 'nav-superm-userfile-badge acn-genfile-badge'
: 'nav-file-tag acn-genfile-badge';
const extEl = document.createElement('span');
extEl.className = 'acn-genfile-badge-ext';
const fullLabel = getGeneratedFileBadgeLabel(file);
extEl.textContent = variant === 'supermini'
? (String(fullLabel || 'f').trim().charAt(0).toLowerCase() || 'f')
: fullLabel;
badge.appendChild(extEl);
const titleParts = [file && (file.displayName || file.name) ? (file.displayName || file.name) : 'AI generated file'];
if (file && file.mimeType) titleParts.push('(' + file.mimeType + ')');
if (IS_CHATGPT && variant !== 'supermini') {
titleParts.push(String.fromCharCode(10) + 'Click to download');
if (isChatGPTAttachmentPreviewSupportedGPT(file)) {
titleParts.push(String.fromCharCode(10) + 'Ctrl+click to preview');
}
}
badge.title = titleParts.join(' ');
badge.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
const modifierPreview = IS_CHATGPT && (e.ctrlKey || e.metaKey);
const mode = modifierPreview ? 'attachment-preview' : 'default';
await activateFile(file, index, fileIdx, mode);
});
return badge;
}
// ============================================================================
// 16. MESSAGE DATELINE SYSTEM
// ============================================================================
// 16.1 Date/time data acquisition and normalization
// ---------------------------------------------------------------------------
function siteUsesDateLine() {
return !!(CURRENT_SITE && CURRENT_SITE.dateLine && CURRENT_SITE.dateLine.enabled);
}
function normalizeDateLineDate(value) {
if (!value) return null;
const date = value instanceof Date ? value : new Date(value);
return Number.isNaN(date.getTime()) ? null : date;
}
function collectPromptDateSource(promptData, container) {
if (!promptData || !container || !siteUsesDateLine()) return promptData;
if (promptData._acnCreatedAt) {
promptData._acnCreatedAt = normalizeDateLineDate(promptData._acnCreatedAt);
}
if (promptData._acnUpdatedAt) {
promptData._acnUpdatedAt = normalizeDateLineDate(promptData._acnUpdatedAt);
}
return promptData;
}
function applySiteDateLineMetadata(prompts) {
if (!siteUsesDateLine() || !Array.isArray(prompts) || prompts.length === 0) return prompts;
prompts.forEach((prompt) => {
if (prompt && prompt._acnCreatedAt) prompt._acnCreatedAt = normalizeDateLineDate(prompt._acnCreatedAt);
if (prompt && prompt._acnUpdatedAt) prompt._acnUpdatedAt = normalizeDateLineDate(prompt._acnUpdatedAt);
});
return prompts;
}
// 16.2 Dateline presentation and viewport positioning
// ---------------------------------------------------------------------------
function buildSolidDateLineSegments(items, groupStart, groupEndExclusive, container, children, top, bottom, color) {
const segments = [];
let segStartTop = top;
let prevBucket = null;
for (let k = groupStart; k < groupEndExclusive; k++) {
const d = items[k] && items[k]._acnCreatedAt ? normalizeDateLineDate(items[k]._acnCreatedAt) : null;
if (!d) continue;
const bucket = Math.floor(d.getHours() / 6);
if (prevBucket !== null && bucket !== prevBucket) {
const el = children[k];
const bounds = el ? getDateLineElementBounds(container, el) : null;
const splitTop = bounds ? bounds.top : null;
if (splitTop !== null && splitTop > segStartTop + DATELINE_HOUR_GAP_PX) {
segments.push({
top: segStartTop,
height: Math.max(1, (splitTop - DATELINE_HOUR_GAP_PX / 2) - segStartTop),
color,
});
segStartTop = splitTop + DATELINE_HOUR_GAP_PX / 2;
}
}
prevBucket = bucket;
}
segments.push({ top: segStartTop, height: Math.max(1, bottom - segStartTop), color });
return segments;
}
function dateLineLocalDateKey(date) {
const d = normalizeDateLineDate(date);
if (!d) return '';
return [
d.getFullYear(),
String(d.getMonth() + 1).padStart(2, '0'),
String(d.getDate()).padStart(2, '0')
].join('-');
}
function formatNavDateLabel(date) {
const d = normalizeDateLineDate(date);
return d ? d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) : '';
}
function formatMessageDateTimeForTooltip(date) {
const d = normalizeDateLineDate(date);
return d ? d.toLocaleString(undefined, {
month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit'
}) : '';
}
function buildMessageDateTooltipText(item) {
if (!item || !item._acnCreatedAt) return '';
const created = normalizeDateLineDate(item._acnCreatedAt);
if (!created) return '';
const createdStr = formatMessageDateTimeForTooltip(created);
const inferredSuffix = item._acnDateInferred ? ' (date inferred from message order)' : '';
const updated = normalizeDateLineDate(item._acnUpdatedAt);
if (!updated || updated.getTime() === created.getTime()) {
return `Created at ${createdStr}${inferredSuffix}`;
}
return `Created at ${createdStr}. Edited at ${formatMessageDateTimeForTooltip(updated)}${inferredSuffix}`;
}
function getDateLineTarget(container) {
if (!container) return null;
return container.tagName === 'UL' ? container.parentElement : container;
}
function getDateLineElementBounds(container, element) {
const target = getDateLineTarget(container);
if (!target || !element) return null;
const targetRect = target.getBoundingClientRect();
const rect = element.getBoundingClientRect();
const targetScrollTop = target === container ? target.scrollTop : 0;
return {
top: rect.top - targetRect.top + targetScrollTop,
bottom: rect.bottom - targetRect.top + targetScrollTop,
};
}
function getDateLineViewport(container) {
const target = getDateLineTarget(container);
if (!target) return null;
const isUl = container.tagName === 'UL';
const scroller = isUl ? (container.closest('#message-nav.max-bar') || target) : container;
const targetRect = target.getBoundingClientRect();
const scrollerRect = scroller.getBoundingClientRect();
const top = scrollerRect.top - targetRect.top + (target === scroller ? target.scrollTop : 0);
return { top, bottom: top + scroller.clientHeight };
}
function scheduleNavDateDecorRerender() {
if (!siteUsesDateLine()) return;
if (navDateDecorRerenderCleanup) navDateDecorRerenderCleanup();
const run = () => {
const navList = document.getElementById('nav-list');
if (navList) renderNavDateDecorations(navList);
const miniNums = document.getElementById('nav-mini-msg-numbers');
if (miniNums) renderNavDateDecorations(miniNums);
const superminiList = document.getElementById('supermini-message-list');
if (superminiList) renderNavDateDecorations(superminiList);
};
['nav-list', 'nav-mini-msg-numbers', 'supermini-message-list'].forEach((id) => {
const el = document.getElementById(id);
const target = getDateLineTarget(el);
if (!target) return;
const stale = target.querySelector(':scope > .acn-date-decor-layer');
if (stale) stale.remove();
});
const messageNav = document.getElementById('message-nav');
if (!messageNav) {
navDateDecorRerenderCleanup = null;
run();
return;
}
let fired = false;
const onEnd = (event) => {
if (event.propertyName !== 'width' || fired) return;
fired = true;
cleanup();
run();
};
const safetyTimer = safeSetTimeout(() => {
if (fired) return;
fired = true;
cleanup();
run();
}, 300);
function cleanup() {
messageNav.removeEventListener('transitionend', onEnd);
safetyTimer.clear();
navDateDecorRerenderCleanup = null;
}
messageNav.addEventListener('transitionend', onEnd);
navDateDecorRerenderCleanup = cleanup;
}
function scaleNavDateDecorationsLive(containerId, ratio) {
const container = document.getElementById(containerId);
if (!container || !Number.isFinite(ratio) || ratio <= 0) return;
const target = getDateLineTarget(container);
if (!target) return;
const layer = target.querySelector(':scope > .acn-date-decor-layer');
if (!layer) return;
layer.querySelectorAll(':scope > .acn-date-line').forEach((line) => {
const top = parseFloat(line.style.top) || 0;
const height = parseFloat(line.style.height) || 0;
line.style.top = (top * ratio) + 'px';
line.style.height = (height * ratio) + 'px';
});
layer.querySelectorAll(':scope > .acn-date-label').forEach((label) => {
const groupTop = (Number(label.dataset.groupTop) || 0) * ratio;
const groupBottom = (Number(label.dataset.groupBottom) || 0) * ratio;
label.dataset.groupTop = String(groupTop);
label.dataset.groupBottom = String(groupBottom);
label.style.top = ((groupTop + groupBottom) / 2) + 'px';
});
const layerHeight = parseFloat(layer.style.height) || 0;
if (layerHeight) layer.style.height = (layerHeight * ratio) + 'px';
}
function renderNavDateDecorations(container) {
if (!siteUsesDateLine() || !container) return;
const target = getDateLineTarget(container);
if (!target) return;
const items = cachedPrompts;
if (!items || items.length === 0) return;
const children = Array.from(container.children).filter((el) =>
!el.classList.contains('acn-date-decor-layer') &&
!el.classList.contains('acn-date-label-fixed')
);
if (children.length !== items.length) return;
const lineSpecs = [];
const labelSpecs = [];
let groupStart = 0;
for (let i = 1; i <= items.length; i++) {
const prevKey = items[i - 1] && items[i - 1]._acnCreatedAt
? dateLineLocalDateKey(items[i - 1]._acnCreatedAt)
: null;
const curKey = i < items.length && items[i] && items[i]._acnCreatedAt
? dateLineLocalDateKey(items[i]._acnCreatedAt)
: null;
if (i === items.length || prevKey !== curKey) {
if (prevKey) {
const firstEl = children[groupStart];
const lastEl = children[i - 1];
const firstBounds = getDateLineElementBounds(container, firstEl);
const lastBounds = getDateLineElementBounds(container, lastEl);
if (firstBounds && lastBounds) {
const top = firstBounds.top;
const height = Math.max(1, lastBounds.bottom - top);
const date = normalizeDateLineDate(items[groupStart]._acnCreatedAt);
const color = DATELINE_GROUP_COLORS[date ? date.getDay() : 0];
if (dateLineStyle === 'solid') {
lineSpecs.push(...buildSolidDateLineSegments(items, groupStart, i, container, children, top, top + height, color));
} else {
lineSpecs.push({ top, height, color });
}
labelSpecs.push({
groupTop: top,
groupBottom: top + height,
centerTop: top + height / 2,
text: formatNavDateLabel(date),
});
}
}
groupStart = i;
}
}
const isSupermini = container.id === 'supermini-message-list';
if (isSupermini) {
const staleFixedLabel = target.parentElement && target.parentElement.querySelector(':scope > .acn-date-label-fixed');
if (staleFixedLabel) staleFixedLabel.remove();
}
const existing = target.querySelector(':scope > .acn-date-decor-layer');
const existingLines = existing ? existing.querySelectorAll(':scope > .acn-date-line') : [];
const existingLabels = existing ? existing.querySelectorAll(':scope > .acn-date-label') : [];
const canReuse = existing && existingLines.length === lineSpecs.length && existingLabels.length === labelSpecs.length;
let layer = existing;
if (!canReuse) {
if (layer) layer.remove();
layer = document.createElement('div');
layer.className = 'acn-date-decor-layer';
target.appendChild(layer);
}
layer.style.top = '0px';
layer.style.height = Math.max(target.scrollHeight, container.scrollHeight) + 'px';
if (canReuse) {
lineSpecs.forEach((spec, index) => {
const seg = existingLines[index];
seg.style.top = spec.top + 'px';
seg.style.height = spec.height + 'px';
seg.style.borderLeftColor = spec.color;
});
labelSpecs.forEach((spec, index) => {
const label = existingLabels[index];
label.dataset.groupTop = String(spec.groupTop);
label.dataset.groupBottom = String(spec.groupBottom);
label.textContent = spec.text;
label.style.top = spec.centerTop + 'px';
});
} else {
lineSpecs.forEach((spec) => {
const seg = document.createElement('div');
seg.className = 'acn-date-line';
seg.style.top = spec.top + 'px';
seg.style.height = spec.height + 'px';
seg.style.borderLeftColor = spec.color;
layer.appendChild(seg);
});
labelSpecs.forEach((spec) => {
const label = document.createElement('span');
label.className = 'acn-date-label';
label.dataset.groupTop = String(spec.groupTop);
label.dataset.groupBottom = String(spec.groupBottom);
label.style.top = spec.centerTop + 'px';
label.textContent = spec.text;
layer.appendChild(label);
});
}
repositionNavDateLabels(container);
const isUl = container.tagName === 'UL';
const scrollBindTarget = isUl ? (container.closest('#message-nav.max-bar') || target) : container;
if (scrollBindTarget && !scrollBindTarget.dataset.acnDateScrollBound) {
scrollBindTarget.dataset.acnDateScrollBound = '1';
let rafId = null;
scrollBindTarget.addEventListener('scroll', () => {
if (rafId) return;
rafId = requestAnimationFrame(() => {
rafId = null;
repositionNavDateLabels(container);
});
}, { passive: true });
}
}
function repositionNavDateLabels(container) {
if (!siteUsesDateLine() || !container) return;
const target = getDateLineTarget(container);
if (!target) return;
const layer = target.querySelector(':scope > .acn-date-decor-layer');
if (!layer) return;
const viewport = getDateLineViewport(container);
if (!viewport) return;
layer.querySelectorAll('.acn-date-label').forEach((label) => {
const groupTop = Number(label.dataset.groupTop);
const groupBottom = Number(label.dataset.groupBottom);
const visTop = Math.max(groupTop, viewport.top);
const visBottom = Math.min(groupBottom, viewport.bottom);
const center = visBottom > visTop
? (visTop + visBottom) / 2
: (groupTop + groupBottom) / 2;
label.style.top = center + 'px';
});
}
// ============================================================================
// 17. OBSERVERS AND URL TRACKING
// ============================================================================
// 17.1 Fast URL watcher
// ---------------------------------------------------------------------------
function startUrlWatcher() {
if (!CURRENT_SITE.fastUpdate) return;
if (urlCheckInterval) urlCheckInterval.clear();
if (IS_CLAUDE) {
if (urlCheckIntervalId) clearInterval(urlCheckIntervalId);
urlCheckIntervalId = setInterval(() => {
const current = getComparableUrlClaude();
if (current !== lastUrl) {
updateMessageList(true);
}
}, 300);
urlCheckInterval = { clear: () => clearInterval(urlCheckIntervalId) };
return;
}
urlCheckInterval = safeSetInterval(() => {
const current = IS_CHATGPT ? getComparableConversationUrlGPT() : window.location.href;
if (current !== lastUrl) updateMessageList(true);
}, 300);
}
// 17.2 Conversation mutation observer and history hooks
// ---------------------------------------------------------------------------
let chatGptMutationHarvestTimer = null;
function observeConversation() {
if (conversationObserver) conversationObserver.disconnect();
const debounceTime = CURRENT_SITE.debounceTime || DEBOUNCE_TIME;
conversationObserver = new MutationObserver(() => {
if (IS_CHATGPT && !isScanningOldMessages) {
if (chatGptMutationHarvestTimer) chatGptMutationHarvestTimer.clear();
chatGptMutationHarvestTimer = safeSetTimeout(() => {
chatGptMutationHarvestTimer = null;
if (!isScanningOldMessages) harvestMountedMessageDetailsGPT();
}, 220);
}
if (IS_CHATGPT && isScanningOldMessages) return;
if (window.navigatorUpdateTimeout) window.navigatorUpdateTimeout.clear();
window.navigatorUpdateTimeout = safeSetTimeout(() => updateMessageList(), debounceTime);
});
conversationObserver.observe(document.body, { childList: true, subtree: true });
if (IS_CLAUDE) {
if (claudeStreamingUpdateInterval) claudeStreamingUpdateInterval.clear();
claudeStreamingUpdateInterval = safeSetInterval(() => updateMessageList(), 400);
}
window.addEventListener('popstate', () => {
if (IS_CLAUDE) return;
if (IS_CHATGPT) queuePreviousConversationSnapshotGPT(false);
lastPromptsContent = '';
cachedPrompts = [];
updateMessageList(true);
});
const originalPushState = history.pushState;
history.pushState = function () {
const previousUrl = window.location.href;
if (IS_CHATGPT) queuePreviousConversationSnapshotGPT();
originalPushState.apply(this, arguments);
if (IS_CLAUDE) return;
if (IS_CHATGPT && isTransientConversationUrlGPT(window.location.href)) {
chatGptSawTransientWebUrl = true;
chatGptQueuedPreviousSnapshot = null;
updateMessageList(true);
return;
}
if (window.location.href === previousUrl) {
if (IS_CHATGPT) chatGptQueuedPreviousSnapshot = null;
return;
}
lastPromptsContent = '';
cachedPrompts = [];
updateMessageList(true);
};
const originalReplaceState = history.replaceState;
history.replaceState = function () {
const previousUrl = window.location.href;
if (IS_CHATGPT) queuePreviousConversationSnapshotGPT();
originalReplaceState.apply(this, arguments);
if (IS_CLAUDE) return;
if (IS_CHATGPT && isTransientConversationUrlGPT(window.location.href)) {
chatGptSawTransientWebUrl = true;
chatGptQueuedPreviousSnapshot = null;
updateMessageList(true);
return;
}
if (window.location.href === previousUrl) {
if (IS_CHATGPT) chatGptQueuedPreviousSnapshot = null;
return;
}
lastPromptsContent = '';
cachedPrompts = [];
updateMessageList(true);
};
}
// 17.3 Shared view validation
// ---------------------------------------------------------------------------
function isActualChatView() {
if (!IS_CLAUDE) return true;
return window.location.hash === '';
}
// ============================================================================
// 18. SCRIPT INITIALIZATION
// ============================================================================
window.addEventListener('resize', () => {
scheduleResponsiveBarMode();
if (isMiniBar) checkMiniScrollVisibility();
});
if (isHistoryEnabled()) {
installScrollLock();
window.addEventListener('scroll', saveReadingPosition, { passive: true, capture: true });
document.addEventListener('scroll', saveReadingPosition, { passive: true, capture: true });
if (IS_CLAUDE) {
document.addEventListener('keydown', handleNavModifierKeydownClaude, { passive: true, capture: true });
document.addEventListener('keyup', handleNavModifierKeyupClaude, { passive: true, capture: true });
window.addEventListener('blur', resetNavModifiersClaude, { passive: true });
window.addEventListener('blur', stopHoldScrollClaude, { passive: true });
}
if (IS_CHATGPT) {
document.addEventListener('keydown', handleComposerKeydownGPT, { capture: true });
document.addEventListener('click', handleSendClickGPT, { capture: true });
document.addEventListener('keydown', handleNavModifierKeydownGPT, { passive: true, capture: true });
document.addEventListener('keyup', handleNavModifierKeyupGPT, { passive: true, capture: true });
window.addEventListener('blur', resetNavModifiersGPT, { passive: true });
let tocDetailTimer = null;
const scheduleTocDetailRefresh = () => {
if (tocDetailTimer) tocDetailTimer.clear();
tocDetailTimer = safeSetTimeout(() => {
tocDetailTimer = null;
const harvested = harvestMountedMessageDetailsGPT();
refreshActiveMessageDetailGPT();
applyChatGPTNativeTocHidingGPT();
if (harvested) scheduleTOCRefreshGPT(true);
}, 220);
};
window.addEventListener('scroll', scheduleTocDetailRefresh, { passive: true, capture: true });
document.addEventListener('scroll', scheduleTocDetailRefresh, { passive: true, capture: true });
applyChatGPTNativeTocHidingGPT();
installChatGPTNativeTocHideObserver();
}
}
window.addEventListener('scroll', refreshUniversalScrollSync, { passive: true, capture: true });
document.addEventListener('scroll', refreshUniversalScrollSync, { passive: true, capture: true });
injectStyles();
initThemeAutoWatcherClaude();
initSuperminiDrag();
safeSetTimeout(async () => {
cleanupExpiredBookmarks();
loadBookmarks();
await updateMessageList(true);
if (IS_CLAUDE) {
startPassiveTaggingClaude();
}
if (isSuperMini) {
applyBarMode('supermini');
} else if (isMiniBar) {
applyBarMode('mini');
} else {
applyBarMode('max');
applyResponsiveBarMode();
}
startUrlWatcher();
}, 1000);
observeConversation();
})();