From 0ec14d53a47e74efdd48f62087db46d57f5c1949 Mon Sep 17 00:00:00 2001 From: kdazhy <3095454350@qq.com> Date: Sat, 8 Aug 2026 11:15:06 +0800 Subject: [PATCH 1/7] Add ChatGPT long conversation navigator --- index.json | 22 +- .../chatgpt-long-conversation-navigator.js | 1177 +++++++++++++++++ 2 files changed, 1198 insertions(+), 1 deletion(-) create mode 100644 scripts/chatgpt-long-conversation-navigator.js diff --git a/index.json b/index.json index 4eac122..de882ee 100644 --- a/index.json +++ b/index.json @@ -1,6 +1,6 @@ { "version": 1, - "updated_at": "2026-07-28T03:17:39Z", + "updated_at": "2026-08-08T03:11:36Z", "scripts": [ { "id": "codex-context-used-meter", @@ -241,6 +241,26 @@ "homepage": "", "script_url": "https://raw.githubusercontent.com/BigPizzaV3/CodexPlusPlusScriptMarket/main/scripts/Codex%20Model%20Matrix.js", "sha256": "0a25838a887699f8c8b72efc67f64d9df045697f6e1555b856fb10a9baa8f075" + }, + { + "id": "chatgpt-long-conversation-navigator", + "name": "ChatGPT 长对话导航与预览", + "description": "为 ChatGPT/Codex Windows 桌面端长对话提供右侧提问索引、悬浮标题预览、精确跳转、当前阅读位置高亮和 Alt+↑/↓ 快捷导航;支持浅深主题与 Codex++ 热重载。", + "version": "6.1.0", + "author": "kdazhy", + "tags": [ + "chatgpt", + "codex", + "conversation", + "navigation", + "preview", + "productivity", + "ui", + "windows" + ], + "homepage": "https://github.com/kdazhy/CodexPlusPlusScriptMarket", + "script_url": "https://raw.githubusercontent.com/BigPizzaV3/CodexPlusPlusScriptMarket/main/scripts/chatgpt-long-conversation-navigator.js", + "sha256": "e8b7bbb15f22cc53c6e1710621c111731ca2bbb4273f57b507cb8ad164e07e0d" } ] } diff --git a/scripts/chatgpt-long-conversation-navigator.js b/scripts/chatgpt-long-conversation-navigator.js new file mode 100644 index 0000000..6e02aa9 --- /dev/null +++ b/scripts/chatgpt-long-conversation-navigator.js @@ -0,0 +1,1177 @@ +// ==UserScript== +// @name ChatGPT 长对话导航与预览(Codex++) +// @namespace https://github.com/kdazhy +// @version 6.1.0 +// @description 为 ChatGPT Windows 桌面端长对话提供提问索引、悬浮预览、精确跳转和快捷键导航。 +// @author kdazhy +// @match https://chatgpt.com/* +// @match https://chat.openai.com/* +// @run-at document-idle +// @grant none +// ==/UserScript== + +(() => { + 'use strict'; + + const VERSION = '6.1.0'; + const INSTALL_KEY = '__codexPlusChatConversationNavigator'; + const LOCK_ID = 'cgpt-codex-navigator-lock'; + const HOST_ID = 'cgpt-codex-navigator-v6-host'; + const LEGACY_STYLE_ID = 'cgpt-codex-navigator-v6-legacy-shield'; + + const CONFIG = { + right: 10, + collapsedWidth: 54, + expandedWidth: 430, + rowHeight: 38, + maxShellHeightRatio: 0.72, + + normalWidth: 8, + near2Width: 11, + near1Width: 17, + activeWidth: 25, + hoverWidth: 38, + + animationMs: 190, + smoothScroll: true, + + titleMaxChars: 72, + scanDebounceMs: 180, + routePollMs: 900, + jumpCorrectionMs: 650, + }; + + // ========================================================= + // 1. Codex++ 热重载与 DOM 单例 + // ========================================================= + // Codex++ 的“重新加载用户脚本”会再次 evaluate 当前文件,因此先销毁旧实例。 + const previous = window[INSTALL_KEY]; + if (previous && typeof previous.destroy === 'function') { + try { + previous.destroy(); + } catch (error) { + console.warn('[ChatGPT Navigator v6] 清理旧实例失败,将强制接管。', error); + } + } + + function installDomLock() { + document.getElementById(LOCK_ID)?.remove(); + const lock = document.createElement('meta'); + lock.id = LOCK_ID; + lock.dataset.version = VERSION; + lock.dataset.hostId = HOST_ID; + document.documentElement.appendChild(lock); + } + + // ========================================================= + // 2. 清理 / 屏蔽已知旧版本 + // ========================================================= + + function installLegacyShield() { + let style = document.getElementById(LEGACY_STYLE_ID); + if (!style) { + style = document.createElement('style'); + style.id = LEGACY_STYLE_ID; + document.documentElement.appendChild(style); + } + + style.textContent = ` + #cgpt-nav-root, + #cgpt-codex-nav-root-v2, + #cgpt-codex-nav-tooltip-v2, + #cgpt-codex-nav-root-v3, + #cgpt-codex-navigator-v4-host, + #cgpt-codex-navigator-v5-host { + display: none !important; + visibility: hidden !important; + opacity: 0 !important; + pointer-events: none !important; + } + `; + } + + function removeStaleHosts() { + document + .querySelectorAll([ + '#cgpt-codex-navigator-v4-host', + '#cgpt-codex-navigator-v5-host', + `#${HOST_ID}`, + ].join(',')) + .forEach(el => el.remove()); + } + + removeStaleHosts(); + installDomLock(); + installLegacyShield(); + + // ========================================================= + // 3. 状态 + // ========================================================= + + const state = { + host: null, + shadow: null, + shell: null, + body: null, + count: null, + + items: [], + activeIndex: -1, + hoverIndex: -1, + conversationRoot: null, + scrollRoot: null, + + currentRouteKey: '', + destroyed: false, + scanTimer: null, + lateScanTimer: null, + jumpCorrectionTimer: null, + scrollRAF: null, + mutationObserver: null, + routeTimer: null, + domReadyHandler: null, + initialized: false, + }; + + // ========================================================= + // 4. ChatGPT 消息提取 + // ========================================================= + + function normalizeText(text) { + return (text || '') + .replace(/\u00A0/g, ' ') + .replace(/[ \t]+/g, ' ') + .replace(/\n{2,}/g, '\n') + .trim(); + } + + function oneLine(text) { + return normalizeText(text).replace(/\s*\n\s*/g, ' '); + } + + function truncate(text, maxChars = CONFIG.titleMaxChars) { + const chars = Array.from(text); + return chars.length > maxChars + ? chars.slice(0, maxChars).join('') + '…' + : text; + } + + function getConversationRoot() { + const candidates = [ + ...document.querySelectorAll([ + '[data-thread-find-target="conversation"]', + '[data-testid="conversation-turn-list"]', + 'main', + ].join(',')), + ]; + + let best = null; + let bestCount = -1; + + for (const candidate of candidates) { + const count = candidate.querySelectorAll([ + '[data-user-message-bubble]', + '[data-message-author-role="user"]', + '[data-testid="user-message"]', + '[data-message-role="user"]', + ].join(',')).length; + + if (count > bestCount) { + best = candidate; + bestCount = count; + } + } + + return best || document.body || document.documentElement; + } + + function getUserMessageNodes(root = getConversationRoot()) { + const selector = [ + '[data-user-message-bubble]', + '[data-message-author-role="user"]', + '[data-testid="user-message"]', + '[data-message-role="user"]', + ].join(','); + + return [...root.querySelectorAll(selector)].filter(node => { + const parentUserNode = node.parentElement?.closest(selector); + return !parentUserNode || !root.contains(parentUserNode); + }); + } + + function getScrollTarget(messageNode) { + return ( + messageNode.closest('[data-local-conversation-user-anchor]') || + messageNode.closest('[data-turn-key]') || + messageNode.closest('[data-content-search-turn-key]') || + messageNode.closest('[data-testid^="conversation-turn-"]') || + messageNode.closest('[data-thread-find-target^="message"]') || + messageNode.closest('[data-message-id]') || + messageNode.closest('article') || + messageNode + ); + } + + function extractMessageText(node) { + const preferred = + node.querySelector('.whitespace-pre-wrap') || + node.querySelector('[class*="whitespace-pre-wrap"]') || + node.querySelector('[data-message-content]') || + node.querySelector('[data-testid="user-message"]') || + node.querySelector('[class*="markdown"]') || + node; + + const text = oneLine(preferred.innerText || preferred.textContent || ''); + if (text) return text; + + const attachmentCount = node.querySelectorAll([ + 'img', + '[data-testid*="attachment"]', + '[data-message-attachment]', + ].join(',')).length; + + return attachmentCount ? `附件消息(${attachmentCount} 个附件)` : ''; + } + + function findScrollRoot(node) { + let current = node?.parentElement || null; + + while (current && current !== document.documentElement) { + const style = getComputedStyle(current); + const canScroll = /(auto|scroll|overlay)/.test(style.overflowY); + if (canScroll && current.clientHeight > 0 && current.scrollHeight > current.clientHeight + 8) { + return current; + } + current = current.parentElement; + } + + return document.scrollingElement || document.documentElement; + } + + function getViewportMetrics() { + const root = state.scrollRoot; + if (!root || root === document.body || root === document.documentElement || root === document.scrollingElement) { + return { top: 0, bottom: window.innerHeight, height: window.innerHeight }; + } + + const rect = root.getBoundingClientRect(); + const top = Math.max(0, rect.top); + const bottom = Math.min(window.innerHeight, rect.bottom); + return { top, bottom, height: Math.max(1, bottom - top) }; + } + + function getRouteKey() { + const activeThread = document.querySelector([ + '[data-app-action-sidebar-thread-id][aria-current="page"]', + '[data-app-action-sidebar-thread-id][data-state="active"]', + '[data-app-action-sidebar-thread-id][aria-selected="true"]', + ].join(',')); + + const threadId = activeThread?.getAttribute('data-app-action-sidebar-thread-id') || ''; + const conversationId = + document.querySelector('[data-above-composer-conversation-id]') + ?.getAttribute('data-above-composer-conversation-id') || + document.querySelector('[data-conversation-id]')?.getAttribute('data-conversation-id') || + ''; + + return `${location.href}|${threadId}|${conversationId}`; + } + + // ========================================================= + // 5. UI + // ========================================================= + + function createUI() { + state.host?.remove(); + + const host = document.createElement('div'); + host.id = HOST_ID; + + Object.assign(host.style, { + position: 'fixed', + inset: '0', + width: '0', + height: '0', + zIndex: '2147483647', + pointerEvents: 'none', + }); + + const shadow = host.attachShadow({ mode: 'open' }); + + shadow.innerHTML = ` + + +
+ +
+
+ `; + + (document.body || document.documentElement).appendChild(host); + + state.host = host; + state.shadow = shadow; + state.shell = shadow.getElementById('shell'); + state.body = shadow.getElementById('body'); + state.count = shadow.getElementById('count'); + + state.shell.addEventListener('pointerleave', () => { + state.hoverIndex = -1; + updateVisualState(false); + }); + } + + // ========================================================= + // 6. 严格一对一渲染 + // ========================================================= + + function render() { + if (!state.shell || !state.body) return; + + const n = state.items.length; + state.shell.dataset.empty = String(n === 0); + state.body.replaceChildren(); + + state.count.textContent = `${n} 个提问`; + + if (!n) { + const empty = document.createElement('div'); + empty.className = 'empty'; + empty.textContent = '暂未检测到用户提问'; + state.body.appendChild(empty); + return; + } + + const fragment = document.createDocumentFragment(); + + state.items.forEach((item, index) => { + const row = document.createElement('button'); + row.type = 'button'; + row.className = 'nav-row'; + row.dataset.index = String(index); + row.title = item.fullText; + row.setAttribute('aria-label', `${index + 1}. ${item.title}`); + row.style.setProperty('--entry-delay', `${Math.min(index, 12) * 12}ms`); + + const titleWrap = document.createElement('span'); + titleWrap.className = 'nav-title-wrap'; + + const num = document.createElement('span'); + num.className = 'nav-num'; + num.textContent = String(index + 1).padStart(2, '0'); + + const title = document.createElement('span'); + title.className = 'nav-title'; + title.textContent = item.title; + + titleWrap.append(num, title); + + const lineCell = document.createElement('span'); + lineCell.className = 'nav-line-cell'; + + // 每条提问始终只有这一根真实线段。 + const line = document.createElement('span'); + line.className = 'nav-line'; + lineCell.appendChild(line); + + row.append(titleWrap, lineCell); + + row.addEventListener('pointerenter', () => { + state.hoverIndex = index; + updateVisualState(true); + }); + + row.addEventListener('click', () => scrollToItem(index)); + + fragment.appendChild(row); + }); + + state.body.appendChild(fragment); + + assertInvariant(); + updateVisualState(false); + } + + function assertInvariant() { + const itemCount = state.items.length; + const rowCount = state.shadow.querySelectorAll('.nav-row').length; + const lineCount = state.shadow.querySelectorAll('.nav-row > .nav-line-cell > .nav-line').length; + const titleCount = state.shadow.querySelectorAll('.nav-row > .nav-title-wrap > .nav-title').length; + + const ok = + itemCount === rowCount && + itemCount === lineCount && + itemCount === titleCount; + + if (!ok) { + console.error(`[ChatGPT Navigator v${VERSION}] 渲染不变量失败`, { + itemCount, + lineCount, + rowCount, + titleCount + }); + } else { + console.debug(`[ChatGPT Navigator v${VERSION}] invariant OK`, { + itemCount, + lineCount, + rowCount, + titleCount + }); + } + } + + function updateVisualState(scrollHoveredRow = false) { + if (!state.shadow) return; + + state.shadow.querySelectorAll('.nav-row').forEach(row => { + const i = Number(row.dataset.index); + const distance = state.hoverIndex >= 0 + ? Math.abs(i - state.hoverIndex) + : Infinity; + + row.classList.toggle('row-active', i === state.activeIndex); + row.classList.toggle('row-hover', i === state.hoverIndex); + row.classList.toggle('near-1', distance === 1); + row.classList.toggle('near-2', distance === 2); + }); + + if (scrollHoveredRow && state.hoverIndex >= 0) { + requestAnimationFrame(() => { + state.shadow + .querySelector(`.nav-row[data-index="${state.hoverIndex}"]`) + ?.scrollIntoView({ block: 'nearest' }); + }); + } + } + + // ========================================================= + // 7. 跳转 / 当前阅读位置 + // ========================================================= + + function getNativeNavigationControl(item) { + const navigationId = item?.target?.getAttribute?.('data-content-search-unit-key'); + if (!navigationId) return null; + + return [...document.querySelectorAll('[data-thread-user-message-navigation-item-id]')] + .find(control => + control.getAttribute('data-thread-user-message-navigation-item-id') === navigationId + ) || null; + } + + function alignItemToStart(item, behavior = 'auto') { + if (!item?.target?.isConnected) return; + + item.target.scrollIntoView({ + behavior, + block: 'start', + inline: 'nearest', + }); + } + + function scrollToItem(index) { + const item = state.items[index]; + + if (!item?.target?.isConnected) { + scheduleScan(); + return; + } + + clearTimeout(state.jumpCorrectionTimer); + + const nativeControl = getNativeNavigationControl(item); + if (nativeControl?.isConnected) { + nativeControl.click(); + } else { + alignItemToStart(item, CONFIG.smoothScroll ? 'smooth' : 'auto'); + } + + // 平滑滚动期间内容高度可能继续变化;结束后用用户消息锚点再校准一次。 + state.jumpCorrectionTimer = setTimeout(() => { + if (!item.target?.isConnected) return; + + const viewport = getViewportMetrics(); + const rect = item.target.getBoundingClientRect(); + const scrollMarginTop = Number.parseFloat(getComputedStyle(item.target).scrollMarginTop) || 0; + const expectedTop = viewport.top + scrollMarginTop; + + if (Math.abs(rect.top - expectedTop) > 6) { + alignItemToStart(item, 'auto'); + } + }, CONFIG.jumpCorrectionMs); + + setActive(index); + } + + function navigateRelative(delta) { + if (!state.items.length) return; + + let index = state.activeIndex; + if (index < 0) index = 0; + + index = Math.max( + 0, + Math.min(state.items.length - 1, index + delta) + ); + + scrollToItem(index); + } + + function updateActiveFromViewport() { + if (!state.items.length) { + setActive(-1); + return; + } + + const viewport = getViewportMetrics(); + const anchorY = viewport.top + viewport.height * 0.32; + + let bestIndex = 0; + let bestDistance = Infinity; + + state.items.forEach((item, index) => { + if (!item.target?.isConnected) return; + + const rect = item.target.getBoundingClientRect(); + const y = rect.top + Math.min(rect.height * 0.22, 20); + const distance = Math.abs(y - anchorY); + + if (distance < bestDistance) { + bestDistance = distance; + bestIndex = index; + } + }); + + setActive(bestIndex); + } + + function setActive(index) { + if (state.activeIndex === index) return; + state.activeIndex = index; + updateVisualState(false); + } + + // ========================================================= + // 8. 扫描 / SPA + // ========================================================= + + function scanMessages() { + if (state.destroyed) return; + + let uiRecreated = false; + if (!state.host?.isConnected) { + createUI(); + uiRecreated = true; + } + + const conversationRoot = getConversationRoot(); + const nodes = getUserMessageNodes(conversationRoot); + const seenTargets = new Set(); + const nextItems = []; + + for (const node of nodes) { + const target = getScrollTarget(node); + if (!target || seenTargets.has(target)) continue; + + const fullText = extractMessageText(node); + if (!fullText) continue; + + seenTargets.add(target); + + nextItems.push({ + node, + target, + fullText, + title: truncate(fullText), + }); + } + + const changed = + nextItems.length !== state.items.length || + nextItems.some((item, i) => + item.target !== state.items[i]?.target || + item.fullText !== state.items[i]?.fullText + ); + + state.conversationRoot = conversationRoot; + state.scrollRoot = findScrollRoot(nextItems[0]?.target || conversationRoot); + state.items = nextItems; + + if (changed || uiRecreated) render(); + updateActiveFromViewport(); + } + + function scheduleScan(delay = CONFIG.scanDebounceMs) { + if (state.destroyed) return; + clearTimeout(state.scanTimer); + state.scanTimer = setTimeout(scanMessages, delay); + } + + function observeDOM() { + state.mutationObserver?.disconnect(); + + state.mutationObserver = new MutationObserver(() => { + scheduleScan(); + }); + + state.mutationObserver.observe(document.documentElement, { + childList: true, + subtree: true, + characterData: true, + attributes: true, + attributeFilter: [ + 'data-message-author-role', + 'data-message-role', + 'data-user-message-bubble', + 'data-turn-key', + 'data-content-search-turn-key', + 'data-conversation-id', + 'data-above-composer-conversation-id', + 'data-testid', + 'data-thread-find-target', + 'data-app-action-sidebar-thread-id', + 'aria-current', + 'aria-selected', + 'data-state', + ], + }); + } + + function onAnyScroll() { + if (state.destroyed) return; + if (state.scrollRAF) return; + + state.scrollRAF = requestAnimationFrame(() => { + state.scrollRAF = null; + updateActiveFromViewport(); + }); + } + + function onResize() { + render(); + onAnyScroll(); + } + + function onVisibilityChange() { + if (!document.hidden) scheduleScan(0); + } + + function onKeyDown(event) { + const target = event.target; + const tag = target?.tagName?.toLowerCase(); + + const editing = + tag === 'input' || + tag === 'textarea' || + target?.isContentEditable; + + if (editing) return; + + if ( + event.altKey && + !event.ctrlKey && + !event.metaKey && + !event.shiftKey && + event.key === 'ArrowUp' + ) { + event.preventDefault(); + navigateRelative(-1); + } + + if ( + event.altKey && + !event.ctrlKey && + !event.metaKey && + !event.shiftKey && + event.key === 'ArrowDown' + ) { + event.preventDefault(); + navigateRelative(1); + } + } + + function watchRoute() { + state.currentRouteKey = getRouteKey(); + clearInterval(state.routeTimer); + + state.routeTimer = setInterval(() => { + if (state.destroyed) return; + + if (!state.host?.isConnected) { + createUI(); + render(); + } + + const nextRouteKey = getRouteKey(); + if (nextRouteKey === state.currentRouteKey) return; + + state.currentRouteKey = nextRouteKey; + state.items = []; + state.activeIndex = -1; + state.hoverIndex = -1; + + render(); + + scheduleScan(0); + clearTimeout(state.lateScanTimer); + state.lateScanTimer = setTimeout(() => scheduleScan(0), 450); + }, CONFIG.routePollMs); + } + + // ========================================================= + // 9. 初始化 + // ========================================================= + + function init() { + if (state.destroyed || state.initialized) return; + state.initialized = true; + + createUI(); + + scanMessages(); + observeDOM(); + watchRoute(); + + window.addEventListener('scroll', onAnyScroll, { + passive: true, + capture: true, + }); + + window.addEventListener('resize', onResize, { passive: true }); + + document.addEventListener('keydown', onKeyDown, true); + document.addEventListener('visibilitychange', onVisibilityChange); + + console.info(`[ChatGPT Navigator v${VERSION}] initialized`); + } + + function destroy() { + if (state.destroyed) return; + state.destroyed = true; + + clearTimeout(state.scanTimer); + clearTimeout(state.lateScanTimer); + clearTimeout(state.jumpCorrectionTimer); + clearInterval(state.routeTimer); + if (state.scrollRAF) cancelAnimationFrame(state.scrollRAF); + + state.mutationObserver?.disconnect(); + window.removeEventListener('scroll', onAnyScroll, true); + window.removeEventListener('resize', onResize); + document.removeEventListener('keydown', onKeyDown, true); + document.removeEventListener('visibilitychange', onVisibilityChange); + if (state.domReadyHandler) { + document.removeEventListener('DOMContentLoaded', state.domReadyHandler); + } + + state.host?.remove(); + document.getElementById(LOCK_ID)?.remove(); + document.getElementById(LEGACY_STYLE_ID)?.remove(); + + if (window[INSTALL_KEY] === api) { + delete window[INSTALL_KEY]; + } + + console.info(`[ChatGPT Navigator v${VERSION}] destroyed`); + } + + const api = { + version: VERSION, + rescan: () => scheduleScan(0), + destroy, + }; + + window[INSTALL_KEY] = api; + + if (document.readyState === 'loading') { + state.domReadyHandler = init; + document.addEventListener('DOMContentLoaded', state.domReadyHandler, { once: true }); + } else { + init(); + } +})(); From 73fc9505b05b6c18f50f44ae72df46d3ebb9162f Mon Sep 17 00:00:00 2001 From: kdazhy <3095454350@qq.com> Date: Sun, 9 Aug 2026 10:40:24 +0800 Subject: [PATCH 2/7] Fix navigator scans during streaming output --- index.json | 6 +++--- scripts/chatgpt-long-conversation-navigator.js | 16 ++++++++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/index.json b/index.json index de882ee..7dea51b 100644 --- a/index.json +++ b/index.json @@ -1,6 +1,6 @@ { "version": 1, - "updated_at": "2026-08-08T03:11:36Z", + "updated_at": "2026-08-09T02:37:49Z", "scripts": [ { "id": "codex-context-used-meter", @@ -246,7 +246,7 @@ "id": "chatgpt-long-conversation-navigator", "name": "ChatGPT 长对话导航与预览", "description": "为 ChatGPT/Codex Windows 桌面端长对话提供右侧提问索引、悬浮标题预览、精确跳转、当前阅读位置高亮和 Alt+↑/↓ 快捷导航;支持浅深主题与 Codex++ 热重载。", - "version": "6.1.0", + "version": "6.1.1", "author": "kdazhy", "tags": [ "chatgpt", @@ -260,7 +260,7 @@ ], "homepage": "https://github.com/kdazhy/CodexPlusPlusScriptMarket", "script_url": "https://raw.githubusercontent.com/BigPizzaV3/CodexPlusPlusScriptMarket/main/scripts/chatgpt-long-conversation-navigator.js", - "sha256": "e8b7bbb15f22cc53c6e1710621c111731ca2bbb4273f57b507cb8ad164e07e0d" + "sha256": "bee2e8b79a5590c963f0137ac00a9cc2121a74fb90cebf4a9480fefc78d21412" } ] } diff --git a/scripts/chatgpt-long-conversation-navigator.js b/scripts/chatgpt-long-conversation-navigator.js index 6e02aa9..9713e8b 100644 --- a/scripts/chatgpt-long-conversation-navigator.js +++ b/scripts/chatgpt-long-conversation-navigator.js @@ -1,7 +1,7 @@ // ==UserScript== // @name ChatGPT 长对话导航与预览(Codex++) // @namespace https://github.com/kdazhy -// @version 6.1.0 +// @version 6.1.1 // @description 为 ChatGPT Windows 桌面端长对话提供提问索引、悬浮预览、精确跳转和快捷键导航。 // @author kdazhy // @match https://chatgpt.com/* @@ -13,7 +13,7 @@ (() => { 'use strict'; - const VERSION = '6.1.0'; + const VERSION = '6.1.1'; const INSTALL_KEY = '__codexPlusChatConversationNavigator'; const LOCK_ID = 'cgpt-codex-navigator-lock'; const HOST_ID = 'cgpt-codex-navigator-v6-host'; @@ -988,8 +988,16 @@ function scheduleScan(delay = CONFIG.scanDebounceMs) { if (state.destroyed) return; - clearTimeout(state.scanTimer); - state.scanTimer = setTimeout(scanMessages, delay); + + if (state.scanTimer) { + if (delay > 0) return; + clearTimeout(state.scanTimer); + } + + state.scanTimer = setTimeout(() => { + state.scanTimer = null; + scanMessages(); + }, delay); } function observeDOM() { From 88356dd1aa9bb34831afb08323c61d2b7058ab05 Mon Sep 17 00:00:00 2001 From: kdazhy <3095454350@qq.com> Date: Wed, 12 Aug 2026 10:25:11 +0800 Subject: [PATCH 3/7] Add current-answer chapter navigation --- index.json | 11 +- .../chatgpt-long-conversation-navigator.js | 599 ++++++++++++++++-- 2 files changed, 544 insertions(+), 66 deletions(-) diff --git a/index.json b/index.json index 7dea51b..d37d432 100644 --- a/index.json +++ b/index.json @@ -1,6 +1,6 @@ { "version": 1, - "updated_at": "2026-08-09T02:37:49Z", + "updated_at": "2026-08-12T02:23:01Z", "scripts": [ { "id": "codex-context-used-meter", @@ -244,15 +244,16 @@ }, { "id": "chatgpt-long-conversation-navigator", - "name": "ChatGPT 长对话导航与预览", - "description": "为 ChatGPT/Codex Windows 桌面端长对话提供右侧提问索引、悬浮标题预览、精确跳转、当前阅读位置高亮和 Alt+↑/↓ 快捷导航;支持浅深主题与 Codex++ 热重载。", - "version": "6.1.1", + "name": "ChatGPT 长对话双侧导航与预览", + "description": "为 ChatGPT/Codex Windows 桌面端提供右侧会话提问索引和左侧当前回答章节索引,支持标题层级、精确跳转、流式更新、当前位置高亮、原生边栏动态避让、浅深主题与 Codex++ 热重载。", + "version": "6.2.0", "author": "kdazhy", "tags": [ "chatgpt", "codex", "conversation", "navigation", + "outline", "preview", "productivity", "ui", @@ -260,7 +261,7 @@ ], "homepage": "https://github.com/kdazhy/CodexPlusPlusScriptMarket", "script_url": "https://raw.githubusercontent.com/BigPizzaV3/CodexPlusPlusScriptMarket/main/scripts/chatgpt-long-conversation-navigator.js", - "sha256": "bee2e8b79a5590c963f0137ac00a9cc2121a74fb90cebf4a9480fefc78d21412" + "sha256": "80c6330a54204ac9d866b1540d6ff3e8f9f0a4b9fdd607014ea40110b1b9f311" } ] } diff --git a/scripts/chatgpt-long-conversation-navigator.js b/scripts/chatgpt-long-conversation-navigator.js index 9713e8b..a3b958f 100644 --- a/scripts/chatgpt-long-conversation-navigator.js +++ b/scripts/chatgpt-long-conversation-navigator.js @@ -1,8 +1,8 @@ // ==UserScript== -// @name ChatGPT 长对话导航与预览(Codex++) +// @name ChatGPT 长对话双侧导航与预览(Codex++) // @namespace https://github.com/kdazhy -// @version 6.1.1 -// @description 为 ChatGPT Windows 桌面端长对话提供提问索引、悬浮预览、精确跳转和快捷键导航。 +// @version 6.2.0 +// @description 为 ChatGPT Windows 桌面端提供会话提问索引、当前回答章节索引、精确跳转和动态布局避让。 // @author kdazhy // @match https://chatgpt.com/* // @match https://chat.openai.com/* @@ -13,7 +13,7 @@ (() => { 'use strict'; - const VERSION = '6.1.1'; + const VERSION = '6.2.0'; const INSTALL_KEY = '__codexPlusChatConversationNavigator'; const LOCK_ID = 'cgpt-codex-navigator-lock'; const HOST_ID = 'cgpt-codex-navigator-v6-host'; @@ -23,6 +23,10 @@ right: 10, collapsedWidth: 54, expandedWidth: 430, + chapterCollapsedWidth: 52, + chapterExpandedWidth: 350, + chapterGapFromAnswer: 16, + chapterObstructionGap: 8, rowHeight: 38, maxShellHeightRatio: 0.72, @@ -36,9 +40,13 @@ smoothScroll: true, titleMaxChars: 72, + chapterTitleMaxChars: 70, + readingAnchorRatio: 0.30, scanDebounceMs: 180, routePollMs: 900, jumpCorrectionMs: 650, + layoutPollMs: 120, + layoutSettleMs: 720, }; // ========================================================= @@ -114,10 +122,17 @@ shell: null, body: null, count: null, + chapterShell: null, + chapterBody: null, + chapterCount: null, + chapterContext: null, items: [], activeIndex: -1, hoverIndex: -1, + chapters: [], + activeChapterIndex: -1, + hoverChapterIndex: -1, conversationRoot: null, scrollRoot: null, @@ -128,6 +143,10 @@ jumpCorrectionTimer: null, scrollRAF: null, mutationObserver: null, + resizeObserver: null, + layoutTimer: null, + layoutBurstUntil: 0, + lastChapterRight: null, routeTimer: null, domReadyHandler: null, initialized: false, @@ -233,6 +252,78 @@ return attachmentCount ? `附件消息(${attachmentCount} 个附件)` : ''; } + function isNodeBetween(node, start, end) { + if (!node?.isConnected || !start?.isConnected || start.contains(node)) return false; + + const afterStart = Boolean( + start.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_FOLLOWING + ); + if (!afterStart) return false; + + return !end || Boolean( + node.compareDocumentPosition(end) & Node.DOCUMENT_POSITION_FOLLOWING + ); + } + + function findAssistantRoot(conversationRoot, start, end) { + if (!conversationRoot || !start) return null; + + const selectorGroups = [ + '[data-markdown-text-style="assistant-message"]', + '[data-message-author-role="assistant"] .markdown, [data-message-author-role="assistant"] [class*="markdown"]', + '.markdown, [class*="MarkdownRoot"]', + ]; + + for (const selector of selectorGroups) { + const candidate = [...conversationRoot.querySelectorAll(selector)] + .find(node => + !node.closest('[data-user-message-bubble], [data-local-conversation-user-anchor]') && + isNodeBetween(node, start, end) + ); + if (candidate) return candidate; + } + + return [...conversationRoot.querySelectorAll('[data-content-search-unit-key]')] + .find(unit => { + if (!isNodeBetween(unit, start, end)) return false; + return [...unit.querySelectorAll('h4.sr-only')] + .some(label => /ChatGPT\s*说|assistant/i.test(label.textContent || '')); + }) || null; + } + + function getHeadingLevel(heading) { + const match = heading?.tagName?.match(/^H([1-6])$/i); + return match ? Number(match[1]) : 2; + } + + function buildChapterModel(item) { + const root = item?.assistantRoot; + if (!root?.isConnected) return []; + + return [...root.querySelectorAll('h1, h2, h3, h4, h5, h6')] + .filter(heading => { + if ( + heading.matches('.sr-only, [aria-hidden="true"]') || + heading.closest('[data-user-message-bubble], [data-local-conversation-user-anchor]') + ) { + return false; + } + + const rect = heading.getBoundingClientRect(); + return rect.width > 2 && rect.height > 2; + }) + .map(heading => { + const fullText = oneLine(heading.innerText || heading.textContent || ''); + return { + heading, + level: getHeadingLevel(heading), + fullText, + title: truncate(fullText, CONFIG.chapterTitleMaxChars), + }; + }) + .filter(chapter => chapter.fullText); + } + function findScrollRoot(node) { let current = node?.parentElement || null; @@ -310,8 +401,8 @@ opacity: .5; } - /* v6.1:参考 rewrite v1.1 的单 shell / 单行模型。 */ - #shell { + /* v6.2:右侧会话 + 左侧当前回答,共用单行精密导航语言。 */ + .nav-shell { position: fixed; right: ${CONFIG.right}px; top: 50%; @@ -344,17 +435,19 @@ transition: width ${CONFIG.animationMs}ms cubic-bezier(.2,.8,.2,1), + right ${CONFIG.animationMs}ms cubic-bezier(.2,.8,.2,1), background-color ${CONFIG.animationMs}ms ease, border-color ${CONFIG.animationMs}ms ease, box-shadow ${CONFIG.animationMs}ms ease; } - #shell[data-empty="true"] { + .nav-shell[data-empty="true"], + .nav-shell[data-constrained="true"] { display: none; } - #shell:hover, - #shell:focus-within { + .nav-shell:hover, + .nav-shell:focus-within { width: min(${CONFIG.expandedWidth}px, calc(100vw - 20px)); border-color: color-mix(in srgb, CanvasText 11%, transparent); background: @@ -371,7 +464,7 @@ -webkit-backdrop-filter: blur(18px) saturate(1.15); } - #header { + .nav-header { flex: 0 0 auto; min-height: 0; height: 0; @@ -398,8 +491,8 @@ box-shadow ${CONFIG.animationMs}ms ease; } - #shell:hover #header, - #shell:focus-within #header { + .nav-shell:hover .nav-header, + .nav-shell:focus-within .nav-header { min-height: 48px; height: 48px; box-shadow: inset 0 -1px 0 color-mix(in srgb, CanvasText 8%, transparent); @@ -462,7 +555,7 @@ opacity: .6; } - #body { + .nav-body { min-height: 0; padding: 0; overflow-x: hidden; @@ -471,20 +564,20 @@ scrollbar-width: none; } - #body::-webkit-scrollbar { width: 0; height: 0; } + .nav-body::-webkit-scrollbar { width: 0; height: 0; } - #shell:hover #body, - #shell:focus-within #body { + .nav-shell:hover .nav-body, + .nav-shell:focus-within .nav-body { padding: 7px; scrollbar-width: thin; scrollbar-color: color-mix(in srgb, CanvasText 18%, transparent) transparent; } - #shell:hover #body::-webkit-scrollbar, - #shell:focus-within #body::-webkit-scrollbar { width: 6px; } + .nav-shell:hover .nav-body::-webkit-scrollbar, + .nav-shell:focus-within .nav-body::-webkit-scrollbar { width: 6px; } - #shell:hover #body::-webkit-scrollbar-thumb, - #shell:focus-within #body::-webkit-scrollbar-thumb { + .nav-shell:hover .nav-body::-webkit-scrollbar-thumb, + .nav-shell:focus-within .nav-body::-webkit-scrollbar-thumb { border: 2px solid transparent; border-radius: 999px; background: color-mix(in srgb, CanvasText 20%, transparent); @@ -524,8 +617,8 @@ transform 120ms cubic-bezier(.2,.8,.2,1); } - #shell:hover .nav-row, - #shell:focus-within .nav-row { + .nav-shell:hover .nav-row, + .nav-shell:focus-within .nav-row { padding-left: 9px; } @@ -559,8 +652,8 @@ visibility 120ms step-end; } - #shell:hover .nav-title-wrap, - #shell:focus-within .nav-title-wrap { + .nav-shell:hover .nav-title-wrap, + .nav-shell:focus-within .nav-title-wrap { opacity: 1; visibility: visible; transform: translateX(0); @@ -571,7 +664,7 @@ visibility 0s; } - #shell:not(:hover):not(:focus-within) .nav-title-wrap { + .nav-shell:not(:hover):not(:focus-within) .nav-title-wrap { min-width: 0; overflow: hidden; pointer-events: none; @@ -652,6 +745,56 @@ box-shadow: none; } + #chapter-shell { + --chapter-available-width: ${CONFIG.chapterExpandedWidth}px; + container-type: inline-size; + width: ${CONFIG.chapterCollapsedWidth}px; + max-width: var(--chapter-available-width); + right: calc(100vw - ${CONFIG.chapterCollapsedWidth + 8}px); + transform-origin: right center; + } + + #chapter-shell:hover, + #chapter-shell:focus-within { + width: min(${CONFIG.chapterExpandedWidth}px, var(--chapter-available-width)); + } + + #chapter-shell .nav-row { + grid-template-columns: minmax(0, 1fr) ${CONFIG.chapterCollapsedWidth - 2}px; + } + + #chapter-shell .nav-line { right: 8px; } + #chapter-shell .nav-title-wrap { grid-template-columns: 31px minmax(0, 1fr); } + + #chapter-context { + max-width: 188px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 10px; + opacity: .45; + } + + #chapter-shell .brand-title { flex: 0 0 auto; } + + @container (max-width: 310px) { + #chapter-context { display: none; } + } + + #chapter-shell .nav-title.level-1, + #chapter-shell .nav-title.level-2 { font-weight: 640; } + #chapter-shell .nav-title.level-3 { padding-left: 9px; } + #chapter-shell .nav-title.level-4 { padding-left: 18px; opacity: .88; } + #chapter-shell .nav-title.level-5, + #chapter-shell .nav-title.level-6 { padding-left: 26px; opacity: .78; } + + #chapter-shell .nav-row[data-level="1"]:not(.row-active):not(.row-hover) .nav-line { width: 15px; } + #chapter-shell .nav-row[data-level="2"]:not(.row-active):not(.row-hover) .nav-line { width: 11px; } + #chapter-shell .nav-row[data-level="3"]:not(.row-active):not(.row-hover) .nav-line { width: 8px; } + #chapter-shell .nav-row[data-level="4"]:not(.row-active):not(.row-hover) .nav-line, + #chapter-shell .nav-row[data-level="5"]:not(.row-active):not(.row-hover) .nav-line, + #chapter-shell .nav-row[data-level="6"]:not(.row-active):not(.row-hover) .nav-line { width: 6px; } + @keyframes nav-row-in { from { opacity: 0; @@ -660,8 +803,8 @@ } @media (prefers-reduced-motion: reduce) { - #shell, - #header, + .nav-shell, + .nav-header, .nav-row, .nav-title-wrap, .nav-line { @@ -671,15 +814,29 @@ } @media (forced-colors: active) { - #shell:hover, - #shell:focus-within { border-color: CanvasText; } + .nav-shell:hover, + .nav-shell:focus-within { border-color: CanvasText; } .nav-row:focus-visible { outline: 2px solid Highlight; } .nav-line { background: CanvasText; } } -
-