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; }
}
-