// ==UserScript==
// @name ChatGPT Recent Messages
// @namespace https://github.com/nonlog/my_scripts
// @version 0.8.7
// @description Compress long conversations and tool calls in ChatGPT Web. Project labels are now a separate userscript.
// @homepage https://github.com/nonlog/my_scripts
// @supportURL https://github.com/nonlog/my_scripts/issues
// @updateURL https://raw.githubusercontent.com/nonlog/my_scripts/master/chatgpt-recent-turns.meta.js
// @downloadURL https://raw.githubusercontent.com/nonlog/my_scripts/master/chatgpt-recent-turns.user.js
// @match https://chatgpt.com/*
// @run-at document-start
// @early-start
// @inject-into page
// @grant none
// ==/UserScript==
(() => {
'use strict';
const VERSION = '0.8.7';
const INITIAL_MESSAGES = 5;
const LOAD_STEP = 5;
const TOP_THRESHOLD_PX = 220;
const PANEL_AUTO_COLLAPSE_MS = 4000;
const PANEL_EDGE_PEEK_MS = 700;
const PANEL_SNAP_PX = 48;
const PANEL_DRAG_THRESHOLD_PX = 4;
const TURBO_KEY = 'cgpt-recent-messages-turbo-v1';
const TURBO_MAX_TURNS = 3;
const TURBO_MAX_NODES = 450;
const TURBO_MAX_CHARS = 700000;
const TURBO_MIN_RESPONSE = 1000000;
const TURBO_SERVER_TURNS = 3;
const TURBO_FLAT_MAX_CHARS = 420000;
const FLAT_DEEP_TRIGGER_NODES = 100;
const FLAT_DEEP_TRIGGER_CHARS = 300000;
const FLAT_DEEP_TAIL_NODES = 80;
const DEEP_TRIGGER_NODES = 260;
const DEEP_TRIGGER_CHARS = 500000;
const DEEP_TAIL_NODES = 120;
const HISTORY_BATCH_TURNS = 5;
const TOOL_KEY = 'cgpt-recent-messages-tool-compact-v1';
const TOOL_SELECTOR = 'span.group\\/tool-message';
const TOOL_HIDDEN = 'data-cgpt-tool-compacted';
const TOOL_BUNDLE = 'cgpt-tool-bundle';
const MODERN_TURN_SELECTOR = '[data-turn-key]';
const LEGACY_TURN_SELECTOR = '[data-testid^="conversation-turn-"]';
const TURN_SELECTOR = `${MODERN_TURN_SELECTOR},${LEGACY_TURN_SELECTOR}`;
const SCROLL_SELECTOR = '[data-app-action-timeline-scroll],[class~="group/scroll-root"]';
const HIDDEN = 'data-cgpt-recent-hidden';
const PANEL_ID = 'cgpt-recent-messages-panel';
const STYLE_ID = 'cgpt-recent-messages-style';
const PANEL_POSITION_KEY = 'cgpt-recent-messages-panel-position-v1';
const HISTORY_ARCHIVE_ID = 'cgpt-recent-history-archive';
const WORKSPACE_LIMIT_HIDDEN = 'data-cgpt-workspace-limit-hidden';
const WORKSPACE_LIMIT_TITLE = 'A workspace member hit a limit';
const WORKSPACE_LIMIT_DETAIL = 'Turn on auto-reload to automatically add credits and prevent future interruptions.';
const WORKSPACE_LIMIT_ACTION = 'Turn on auto-reload';
const isZh = /^zh\b/i.test(navigator.languages?.[0] || navigator.language || '');
const t = isZh ? {
older: `加载前 ${LOAD_STEP} 条消息`, olderLoading: '正在加载更早消息…', olderNone: '没有更早消息', olderReopen: '重新打开已加载的轻量历史', all: '显示全部已加载消息', recent: `只显示最近 ${INITIAL_MESSAGES} 条消息`,
reset: `重置为最近 ${INITIAL_MESSAGES} 条消息`, turboOn: 'Turbo 已开启;点击关闭并重新加载完整历史',
turboOff: 'Turbo 已关闭;点击开启并重新加载', toolsOn: 'Tool Compactor 已开启;点击关闭',
toolsOff: 'Tool Compactor 已关闭;点击开启', open: '展开工具栏', close: '折叠工具栏', drag: '拖动工具栏',
bundle: (n, x) => x ? `${n} 个 tool calls · 收起` : `${n} 个 tool calls`, status: (v, n) => `${v}/${n}`,
historyTitle: n => `轻量历史 · ${n} 个回合`, historyClose: '关闭轻量历史', historyUser: '你', historyAssistant: 'ChatGPT', historyTools: n => `${n} 个 tool calls 已省略`,
} : {
older: `Load ${LOAD_STEP} older messages`, olderLoading: 'Loading older messages…', olderNone: 'No older messages', olderReopen: 'Reopen loaded lightweight history', all: 'Show all currently loaded messages', recent: `Show only the latest ${INITIAL_MESSAGES} messages`,
reset: `Reset to the latest ${INITIAL_MESSAGES} messages`, turboOn: 'Turbo is ON; click to disable and reload full history',
turboOff: 'Turbo is OFF; click to enable and reload', toolsOn: 'Tool Compactor is ON; click to disable',
toolsOff: 'Tool Compactor is OFF; click to enable', open: 'Open toolbar', close: 'Collapse toolbar', drag: 'Drag toolbar',
bundle: (n, x) => x ? `${n} tool calls · collapse` : `${n} tool calls`, status: (v, n) => `${v}/${n}`,
historyTitle: n => `Lightweight history · ${n} turns`, historyClose: 'Close lightweight history', historyUser: 'You', historyAssistant: 'ChatGPT', historyTools: n => `${n} tool calls omitted`,
};
let visibleCount = INITIAL_MESSAGES, showAll = false, updateTimer = null, collapseTimer = null, peekTimer = null;
let scrollRoot = null, listRoot = null, listObserver = null, discoveryObserver = null, topLoadArmed = true;
let lastUrl = location.href, toolUiTimer = null, dragState = null, suppressPanelClickUntil = 0, workspaceLimitScanTimer = null;
let turboOriginalFetch = null;
const toolObservers = new Map(), toolTimers = new Map();
const historyState = {
conversationId: null, requestHeaders: null, requestCredentials: 'same-origin', requestCache: 'default', requestRedirect: 'follow', requestReferrerPolicy: '',
initialServerTurns: TURBO_SERVER_TURNS, serverTurns: TURBO_SERVER_TURNS, initialSeenIds: new Set(), seenIds: new Set(),
initialOldestCreateTime: null, oldestCreateTime: null, loading: false, exhausted: false, turns: [],
};
const turboEnabled = () => localStorage.getItem(TURBO_KEY) !== '0';
const toolEnabled = () => localStorage.getItem(TOOL_KEY) !== '0';
const setTurbo = value => localStorage.setItem(TURBO_KEY, value ? '1' : '0');
const setTools = value => localStorage.setItem(TOOL_KEY, value ? '1' : '0');
if (localStorage.getItem(TURBO_KEY) === null) setTurbo(true);
if (localStorage.getItem(TOOL_KEY) === null) setTools(true);
function conversationFetchInfo(input, init) {
const method = String(init?.method || input?.method || 'GET').toUpperCase();
if (method !== 'GET') return null;
const raw = typeof input === 'string' ? input : input instanceof URL ? input.href : input?.url || '';
try {
const url = new URL(raw, location.origin), path = url.pathname;
if (/^\/backend-api\/conversation\/[0-9a-f-]+$/i.test(path)) return { kind: 'legacy', url };
if (/^\/backend-api\/conversations\/[0-9a-f-]+$/i.test(path)) return { kind: 'flat-main', url };
if (/^\/backend-api\/conversations\/[0-9a-f-]+\/messages$/i.test(path) && url.searchParams.has('before')) return { kind: 'flat-history', url };
} catch {}
return null;
}
function replaceFetchUrl(input, url) {
if (typeof input === 'string') return url.href;
if (input instanceof URL) return new URL(url.href);
if (typeof Request !== 'undefined' && input instanceof Request) return new Request(url.href, input);
return url.href;
}
function capFlatRequest(args, info) {
const url = new URL(info.url.href), rawTurns = Number.parseInt(url.searchParams.get('num_turns') || '', 10);
const requestedTurns = Number.isFinite(rawTurns) && rawTurns > 0 ? rawTurns : null;
const networkTurns = Math.min(requestedTurns || TURBO_SERVER_TURNS, TURBO_SERVER_TURNS);
url.searchParams.set('num_turns', String(networkTurns));
return { args: [replaceFetchUrl(args[0], url), args[1]], requestedTurns, networkTurns };
}
function resetHistoryState() {
historyState.conversationId = null;
historyState.requestHeaders = null;
historyState.requestCredentials = 'same-origin';
historyState.requestCache = 'default';
historyState.requestRedirect = 'follow';
historyState.requestReferrerPolicy = '';
historyState.initialServerTurns = TURBO_SERVER_TURNS;
historyState.serverTurns = TURBO_SERVER_TURNS;
historyState.initialSeenIds = new Set();
historyState.seenIds = new Set();
historyState.initialOldestCreateTime = null;
historyState.oldestCreateTime = null;
historyState.loading = false;
historyState.exhausted = false;
historyState.turns = [];
document.getElementById(HISTORY_ARCHIVE_ID)?.remove();
window.__cgptRecentMessagesHistoryState = null;
}
function publishHistoryState() {
window.__cgptRecentMessagesHistoryState = {
conversationId: historyState.conversationId,
serverTurns: historyState.serverTurns,
exhausted: historyState.exhausted,
canLoadRemote: Boolean(historyState.requestHeaders && turboOriginalFetch),
loading: historyState.loading,
archivedTurns: historyState.turns.length,
};
}
function captureFlatRequestContext(input, init, info, networkTurns) {
const id = info?.url?.pathname?.split('/').filter(Boolean).at(-1) || null;
const changedConversation = historyState.conversationId && historyState.conversationId !== id;
if (changedConversation) resetHistoryState();
historyState.conversationId = id;
try {
const request = typeof Request !== 'undefined' && input instanceof Request ? new Request(input, init) : new Request(info.url.href, init);
historyState.requestHeaders = new Headers(request.headers);
historyState.requestCredentials = request.credentials || 'same-origin';
historyState.requestCache = request.cache || 'default';
historyState.requestRedirect = request.redirect || 'follow';
historyState.requestReferrerPolicy = request.referrerPolicy || '';
} catch {
historyState.requestHeaders = new Headers(init?.headers || {});
historyState.requestCredentials = init?.credentials || 'same-origin';
}
historyState.initialServerTurns = networkTurns || TURBO_SERVER_TURNS;
if (!historyState.turns.length) historyState.serverTurns = historyState.initialServerTurns;
}
function captureFlatHistory(data, info) {
const id = info?.url?.pathname?.split('/').filter(Boolean).at(-1) || null;
const changedConversation = historyState.conversationId && historyState.conversationId !== id;
if (changedConversation) resetHistoryState();
historyState.conversationId = id;
if (!historyState.turns.length) {
const list = Array.isArray(data?.messages) ? data.messages : [];
const ids = new Set(list.map(message => message?.id).filter(Boolean));
const times = list.map(message => Number(message?.create_time)).filter(Number.isFinite);
historyState.initialSeenIds = new Set(ids);
historyState.seenIds = new Set(ids);
historyState.initialOldestCreateTime = times.length ? Math.min(...times) : null;
historyState.oldestCreateTime = historyState.initialOldestCreateTime;
// The current flat API may return has_previous_page=false even when a capped
// num_turns response is only the recent tail. Do not treat it as authoritative.
historyState.exhausted = false;
}
historyState.loading = false;
publishHistoryState();
queueMicrotask(() => scheduleUpdate(0));
}
function resetHistoryArchive() {
historyState.serverTurns = historyState.initialServerTurns;
historyState.seenIds = new Set(historyState.initialSeenIds);
historyState.oldestCreateTime = historyState.initialOldestCreateTime;
historyState.loading = false;
historyState.exhausted = false;
historyState.turns = [];
document.getElementById(HISTORY_ARCHIVE_ID)?.remove();
publishHistoryState();
}
function textPart(part) {
if (typeof part === 'string') return part;
if (!part || typeof part !== 'object') return '';
if (typeof part.text === 'string') return part.text;
if (typeof part.content === 'string') return part.content;
return '';
}
function messageText(message) {
if (!message || message?.metadata?.is_visually_hidden_from_conversation) return '';
const role = message?.author?.role, content = message?.content || {};
const type = content.content_type || '';
if (role === 'assistant' && !['text', 'reasoning_recap'].includes(type)) return '';
if (role === 'user' && !['text', 'multimodal_text'].includes(type)) return '';
const parts = Array.isArray(content.parts) ? content.parts : [];
const values = parts.map(textPart).filter(Boolean);
if (!values.length && typeof content.text === 'string') values.push(content.text);
return values.join('\n').trim();
}
function extractHistoryTurns(list) {
if (!Array.isArray(list)) return [];
const turns = [];
let current = null;
for (const message of list) {
const role = message?.author?.role;
if (role === 'user') {
const text = messageText(message);
if (!text) { current = null; continue; }
current = { id: message.id || `user-${turns.length}`, user: text, assistant: [], tools: 0 };
turns.push(current);
continue;
}
if (!current) continue;
if (role === 'tool') { current.tools++; continue; }
if (role === 'assistant') {
const text = messageText(message);
if (text && !current.assistant.includes(text)) current.assistant.push(text);
}
}
return turns;
}
function ensureHistoryArchive() {
let host = document.getElementById(HISTORY_ARCHIVE_ID);
if (host) return host;
host = document.createElement('section');
host.id = HISTORY_ARCHIVE_ID;
host.setAttribute('role', 'region');
document.body.appendChild(host);
return host;
}
function renderHistoryArchive() {
if (!historyState.turns.length) { document.getElementById(HISTORY_ARCHIVE_ID)?.remove(); return; }
const host = ensureHistoryArchive();
host.replaceChildren();
const header = document.createElement('div'); header.className = 'cgpt-rh-head';
const title = document.createElement('strong'); title.textContent = t.historyTitle(historyState.turns.length);
const close = document.createElement('button'); close.type = 'button'; close.className = 'cgpt-rh-close'; close.textContent = '×'; close.setAttribute('aria-label', t.historyClose); close.addEventListener('click', () => { host.remove(); updatePanel(messages().length); });
header.append(title, close); host.append(header);
const body = document.createElement('div'); body.className = 'cgpt-rh-body';
for (const turn of historyState.turns) {
const section = document.createElement('article'); section.className = 'cgpt-rh-turn';
const userLabel = document.createElement('div'); userLabel.className = 'cgpt-rh-label'; userLabel.textContent = t.historyUser;
const user = document.createElement('div'); user.className = 'cgpt-rh-user'; user.textContent = turn.user;
section.append(userLabel, user);
for (const text of turn.assistant) {
const label = document.createElement('div'); label.className = 'cgpt-rh-label'; label.textContent = t.historyAssistant;
const assistant = document.createElement('div'); assistant.className = 'cgpt-rh-assistant'; assistant.textContent = text;
section.append(label, assistant);
}
if (turn.tools) { const tools = document.createElement('div'); tools.className = 'cgpt-rh-tools'; tools.textContent = t.historyTools(turn.tools); section.append(tools); }
body.append(section);
}
host.append(body);
}
async function loadOlderHistory() {
if (!turboEnabled() || historyState.loading || historyState.exhausted || !historyState.conversationId || !historyState.requestHeaders || !turboOriginalFetch) return;
historyState.loading = true; publishHistoryState(); updatePanel(messages().length);
try {
const nextTurns = historyState.serverTurns + HISTORY_BATCH_TURNS;
const url = new URL(`/backend-api/conversations/${historyState.conversationId}`, location.origin);
url.searchParams.set('include_has_versions', 'true');
url.searchParams.set('num_turns', String(nextTurns));
const response = await turboOriginalFetch(url.href, {
method: 'GET',
headers: new Headers(historyState.requestHeaders),
credentials: historyState.requestCredentials,
cache: historyState.requestCache,
redirect: historyState.requestRedirect,
referrerPolicy: historyState.requestReferrerPolicy || undefined,
});
if (!response.ok) throw new Error(`history request failed: ${response.status}`);
const data = await response.json(), list = Array.isArray(data?.messages) ? data.messages : [];
const firstSeenIndex = list.findIndex(message => message?.id && historyState.seenIds.has(message.id));
let olderMessages = firstSeenIndex > 0 ? list.slice(0, firstSeenIndex) : [];
if (firstSeenIndex < 0 && Number.isFinite(historyState.oldestCreateTime)) {
olderMessages = list.filter(message => Number.isFinite(Number(message?.create_time)) && Number(message.create_time) < historyState.oldestCreateTime);
}
const extracted = extractHistoryTurns(olderMessages);
const seen = new Set(historyState.turns.map(x => x.id));
const fresh = extracted.filter(x => !seen.has(x.id));
if (fresh.length) historyState.turns = [...fresh, ...historyState.turns];
list.forEach(message => { if (message?.id) historyState.seenIds.add(message.id); });
const olderTimes = olderMessages.map(message => Number(message?.create_time)).filter(Number.isFinite);
if (olderTimes.length) historyState.oldestCreateTime = Math.min(historyState.oldestCreateTime ?? Infinity, ...olderTimes);
historyState.serverTurns = nextTurns;
historyState.exhausted = olderMessages.length === 0;
if (historyState.turns.length) renderHistoryArchive();
} catch (error) {
console.debug('[ChatGPT Recent Messages] Manual older-history load failed:', error);
} finally {
historyState.loading = false; publishHistoryState(); updatePanel(messages().length);
}
}
function emptyFlatHistoryResponse() {
const body = JSON.stringify({ messages: [], safe_urls: [], blocked_urls: [], page_info: { start_cursor: null, end_cursor: null, has_previous_page: false, has_next_page: false } });
return new Response(body, { status: 200, headers: { 'content-type': 'application/json' } });
}
function trimConversation(data) {
const mapping = data?.mapping, current = data?.current_node;
if (!mapping || !current) return null;
const path = [], seen = new Set();
for (let id = current; id && mapping[id] && !seen.has(id); id = mapping[id].parent) { seen.add(id); path.push(id); }
path.reverse();
if (path.length < 2) return null;
const users = [];
path.forEach((id, i) => { if (mapping[id]?.message?.author?.role === 'user') users.push(i); });
if (!users.length) return null;
const segments = users.map((start, i) => {
const end = i + 1 < users.length ? users[i + 1] : path.length;
let chars = 0;
for (let j = start; j < end; j++) { const m = mapping[path[j]]?.message; if (m) chars += JSON.stringify(m).length; }
return { start, end, nodes: end - start, chars };
});
const latest = segments.at(-1);
const deep = latest.nodes > DEEP_TRIGGER_NODES || latest.chars > DEEP_TRIGGER_CHARS;
let first = segments.length - 1, turns = 1, nodes = latest.nodes, chars = latest.chars;
if (!deep) {
while (first > 0 && turns < TURBO_MAX_TURNS) {
const c = segments[first - 1];
if (nodes + c.nodes > TURBO_MAX_NODES || chars + c.chars > TURBO_MAX_CHARS) break;
first--; turns++; nodes += c.nodes; chars += c.chars;
}
if (turns === users.length && users.length <= TURBO_MAX_TURNS) return null;
}
const root = path[0];
let keep = path.slice(segments[first].start), deepOriginal = 0, deepRetained = 0;
if (deep) {
deepOriginal = keep.length;
const userId = keep[0], tail = keep.slice(Math.max(1, keep.length - DEEP_TAIL_NODES));
keep = [userId, ...tail.filter(id => id !== userId)];
deepRetained = keep.length;
chars = keep.reduce((sum, id) => { const m = mapping[id]?.message; return sum + (m ? JSON.stringify(m).length : 0); }, 0);
}
if (!root || !mapping[root] || !keep.length) return null;
const ids = (keep[0] === root ? keep : [root, ...keep]).filter(id => mapping[id]);
const trimmed = {};
ids.forEach((id, i) => trimmed[id] = { ...mapping[id], parent: i ? ids[i - 1] : null, children: i + 1 < ids.length ? [ids[i + 1]] : [] });
data.mapping = trimmed;
return { originalNodes: Object.keys(mapping).length, retainedNodes: ids.length, userTurns: turns, retainedMessageChars: chars,
adaptive: true, deep, deepOriginalTurnNodes: deepOriginal, deepRetainedTurnNodes: deepRetained,
deepDroppedNodes: deep ? deepOriginal - deepRetained : 0 };
}
function trimFlatConversation(data) {
const list = data?.messages;
if (!Array.isArray(list) || !list.length) return null;
const originalMessages = list.length, users = [];
list.forEach((m, i) => { if (m?.author?.role === 'user') users.push(i); });
let keep = list, turns = users.length, chars = list.reduce((sum, m) => sum + JSON.stringify(m).length, 0);
let deep = false, deepOriginal = 0, deepRetained = 0;
if (users.length) {
const segments = users.map((start, i) => {
const end = i + 1 < users.length ? users[i + 1] : list.length;
let segmentChars = 0;
for (let j = start; j < end; j++) segmentChars += JSON.stringify(list[j]).length;
return { start, end, nodes: end - start, chars: segmentChars };
});
const latest = segments.at(-1);
deep = latest.nodes > FLAT_DEEP_TRIGGER_NODES || latest.chars > FLAT_DEEP_TRIGGER_CHARS;
let first = segments.length - 1, nodes = latest.nodes;
chars = latest.chars; turns = 1;
if (!deep) {
while (first > 0 && turns < TURBO_MAX_TURNS) {
const c = segments[first - 1];
if (nodes + c.nodes > TURBO_MAX_NODES || chars + c.chars > TURBO_FLAT_MAX_CHARS) break;
first--; turns++; nodes += c.nodes; chars += c.chars;
}
}
keep = list.slice(segments[first].start);
if (deep) {
deepOriginal = keep.length;
const user = keep[0], tail = keep.slice(Math.max(1, keep.length - FLAT_DEEP_TAIL_NODES));
keep = [user, ...tail.filter(m => m !== user)];
deepRetained = keep.length;
chars = keep.reduce((sum, m) => sum + JSON.stringify(m).length, 0);
}
}
let changed = keep.length !== list.length;
if (changed) data.messages = keep;
if (data.page_info) {
const firstId = keep[0]?.id, lastId = keep.at(-1)?.id;
if (data.page_info.has_previous_page !== false) changed = true;
data.page_info = { ...data.page_info, start_cursor: firstId || data.page_info.start_cursor, end_cursor: lastId || data.page_info.end_cursor, has_previous_page: false };
}
if (!changed) return null;
return {
originalNodes: originalMessages, retainedNodes: keep.length, userTurns: turns, retainedMessageChars: chars,
adaptive: true, flat: true, deep, deepOriginalTurnNodes: deepOriginal, deepRetainedTurnNodes: deepRetained,
deepDroppedNodes: deep ? deepOriginal - deepRetained : 0, historyBlocked: true,
};
}
function installTurboFetch() {
if (!turboEnabled() || window.fetch?.__cgptRecentMessagesTurbo) return;
const original = window.fetch.bind(window);
turboOriginalFetch = original;
const wrapped = async (...incoming) => {
const info = conversationFetchInfo(incoming[0], incoming[1]);
if (info?.kind === 'flat-history') {
window.__cgptRecentMessagesBlockedHistory = (window.__cgptRecentMessagesBlockedHistory || 0) + 1;
if (window.__cgptRecentMessagesTrimStats) window.__cgptRecentMessagesTrimStats.blockedHistoryPages = window.__cgptRecentMessagesBlockedHistory;
return emptyFlatHistoryResponse();
}
let args = incoming, requestStats = null;
if (info?.kind === 'flat-main') {
const capped = capFlatRequest(incoming, info);
captureFlatRequestContext(incoming[0], incoming[1], info, capped.networkTurns);
args = capped.args;
requestStats = { requestedTurns: capped.requestedTurns, networkTurns: capped.networkTurns };
window.__cgptRecentMessagesTrimStats = null;
window.__cgptRecentMessagesBlockedHistory = 0;
} else if (info?.kind === 'legacy') {
window.__cgptRecentMessagesTrimStats = null;
}
const response = await original(...args);
if (!info || !response.ok) return response;
try {
const text = await response.clone().text();
if (info.kind === 'legacy' && text.length < TURBO_MIN_RESPONSE) return response;
const data = JSON.parse(text);
if (info.kind === 'flat-main') captureFlatHistory(data, info);
const stats = info.kind === 'legacy' ? trimConversation(data) : trimFlatConversation(data);
if (!stats) {
if (info.kind === 'flat-main') {
const count = Array.isArray(data.messages) ? data.messages.length : 0;
window.__cgptRecentMessagesTrimStats = { originalNodes: count, retainedNodes: count, userTurns: 0, flat: true,
...requestStats, beforeChars: text.length, afterChars: text.length, blockedHistoryPages: 0, version: VERSION };
}
return response;
}
const body = JSON.stringify(data), headers = new Headers(response.headers);
headers.delete('content-length'); headers.delete('content-encoding');
window.__cgptRecentMessagesTrimStats = { ...stats, ...requestStats, beforeChars: text.length, afterChars: body.length,
blockedHistoryPages: window.__cgptRecentMessagesBlockedHistory || 0, version: VERSION };
return new Response(body, { status: response.status, statusText: response.statusText, headers });
} catch (error) { console.debug('[ChatGPT Recent Messages] Turbo trim skipped:', error); return response; }
};
Object.defineProperty(wrapped, '__cgptRecentMessagesTurbo', { value: true });
window.fetch = wrapped;
}
installTurboFetch();
const icons = {
older: '',
all: '',
recent: '',
reset: '',
turbo: '',
tools: '',
panel: '',
collapse: '',
grip: '',
};
function hideWorkspaceLimitBanners() {
if (!document.body) return;
for (const button of document.querySelectorAll('button')) {
if ((button.textContent || '').trim() !== WORKSPACE_LIMIT_ACTION) continue;
let node = button.parentElement;
for (let depth = 0; node && node !== document.body && depth < 5; depth++, node = node.parentElement) {
const text = (node.textContent || '').replace(/\s+/g, ' ').trim();
if (!text.includes(WORKSPACE_LIMIT_TITLE) || !text.includes(WORKSPACE_LIMIT_DETAIL)) continue;
node.setAttribute(WORKSPACE_LIMIT_HIDDEN, 'true');
break;
}
}
}
function scheduleWorkspaceLimitScan() {
clearTimeout(workspaceLimitScanTimer);
let step = 0;
const delays = [300, 900, 1600];
const run = () => {
hideWorkspaceLimitBanners();
if (step < delays.length) workspaceLimitScanTimer = setTimeout(run, delays[step++]);
};
run();
}
function ensureStyle() {
if (document.getElementById(STYLE_ID)) return;
const s = document.createElement('style'); s.id = STYLE_ID; s.textContent = `
[${HIDDEN}="true"],[${TOOL_HIDDEN}="true"],[${WORKSPACE_LIMIT_HIDDEN}="true"]{display:none!important}
.${TOOL_BUNDLE}{display:inline-flex;align-items:center;gap:6px;width:max-content;padding:5px 9px;border:1px solid color-mix(in srgb,currentColor 18%,transparent);border-radius:9px;background:transparent;color:var(--text-secondary,currentColor);cursor:pointer;font:inherit;font-size:.875em;opacity:.82}
.${TOOL_BUNDLE}:hover{opacity:1}.cgpt-tool-bundle-chevron{transition:transform .12s ease}.${TOOL_BUNDLE}[data-expanded="true"] .cgpt-tool-bundle-chevron{transform:rotate(180deg)}
#${PANEL_ID}{position:fixed;right:14px;bottom:88px;z-index:2147483647;display:flex;flex-direction:column;align-items:center;gap:6px;padding:6px;border:1px solid color-mix(in srgb,CanvasText 16%,transparent);border-radius:12px;background:color-mix(in srgb,Canvas 94%,transparent);color:CanvasText;box-shadow:0 4px 18px rgba(0,0,0,.14);backdrop-filter:blur(10px);font:11px/1.2 system-ui,sans-serif;transition:transform .16s ease;touch-action:none}
#${PANEL_ID} .cgpt-rm-grip{display:grid;place-items:center;width:30px;height:16px;opacity:.45;cursor:grab;touch-action:none}#${PANEL_ID} .cgpt-rm-grip:active{cursor:grabbing}#${PANEL_ID} .cgpt-rm-grip svg{width:16px;height:16px}#${PANEL_ID} .cgpt-rm-status{min-width:30px;padding:2px 3px;text-align:center;opacity:.68;white-space:nowrap}#${PANEL_ID} button{position:relative;display:grid;place-items:center;width:32px;height:32px;padding:0;border:1px solid color-mix(in srgb,CanvasText 16%,transparent);border-radius:9px;background:Canvas;color:CanvasText;cursor:pointer}#${PANEL_ID} button[data-active="true"]{box-shadow:inset 0 0 0 1px currentColor}#${PANEL_ID} svg,.${TOOL_BUNDLE} svg{width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round;pointer-events:none}
#${PANEL_ID}[data-collapsed="true"]{gap:0;padding:4px;border-radius:999px;cursor:grab}#${PANEL_ID}[data-collapsed="true"]>:not([data-action="panel"]){display:none!important}#${PANEL_ID}[data-collapsed="true"] [data-action="panel"]{display:grid;width:34px;height:34px;border-radius:999px;cursor:grab;touch-action:none}#${PANEL_ID}[data-dragging="true"]{transition:none!important;transform:none!important;cursor:grabbing}#${PANEL_ID}[data-dragging="true"] [data-action="panel"]{cursor:grabbing}#${PANEL_ID}[data-edge="left"][data-peek="true"]{transform:translateX(-55%)}#${PANEL_ID}[data-edge="right"][data-peek="true"]{transform:translateX(55%)}
#${PANEL_ID} button::after{content:attr(data-tooltip);position:absolute;right:calc(100% + 9px);top:50%;transform:translateY(-50%) translateX(4px);padding:6px 8px;border-radius:7px;background:#111;color:#fff;font:12px/1.2 system-ui,sans-serif;white-space:nowrap;opacity:0;visibility:hidden;pointer-events:none}#${PANEL_ID} button:hover::after,#${PANEL_ID} button:focus-visible::after{opacity:1;visibility:visible;transform:translateY(-50%)}#${PANEL_ID}[data-edge="left"] button::after{left:calc(100% + 9px);right:auto;transform:translateY(-50%) translateX(-4px)}#${PANEL_ID}[data-edge="left"] button:hover::after,#${PANEL_ID}[data-edge="left"] button:focus-visible::after{transform:translateY(-50%) translateX(0)}
#${HISTORY_ARCHIVE_ID}{position:fixed;top:72px;right:64px;z-index:2147483646;width:min(720px,calc(100vw - 96px));max-height:calc(100vh - 112px);overflow:auto;border:1px solid color-mix(in srgb,CanvasText 16%,transparent);border-radius:14px;background:color-mix(in srgb,Canvas 97%,transparent);color:CanvasText;box-shadow:0 12px 40px rgba(0,0,0,.22);backdrop-filter:blur(14px);font:14px/1.55 system-ui,sans-serif}
#${HISTORY_ARCHIVE_ID} .cgpt-rh-head{position:sticky;top:0;z-index:1;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 14px;border-bottom:1px solid color-mix(in srgb,CanvasText 12%,transparent);background:color-mix(in srgb,Canvas 96%,transparent)}#${HISTORY_ARCHIVE_ID} .cgpt-rh-close{display:grid;place-items:center;width:30px;height:30px;border:0;border-radius:8px;background:transparent;color:inherit;font:22px/1 system-ui;cursor:pointer}#${HISTORY_ARCHIVE_ID} .cgpt-rh-close:hover{background:color-mix(in srgb,CanvasText 8%,transparent)}
#${HISTORY_ARCHIVE_ID} .cgpt-rh-body{padding:4px 14px 14px}#${HISTORY_ARCHIVE_ID} .cgpt-rh-turn{padding:14px 0;border-bottom:1px solid color-mix(in srgb,CanvasText 10%,transparent)}#${HISTORY_ARCHIVE_ID} .cgpt-rh-turn:last-child{border-bottom:0}#${HISTORY_ARCHIVE_ID} .cgpt-rh-label{margin:8px 0 4px;font-size:11px;font-weight:600;opacity:.58;text-transform:uppercase;letter-spacing:.04em}#${HISTORY_ARCHIVE_ID} .cgpt-rh-user,#${HISTORY_ARCHIVE_ID} .cgpt-rh-assistant{white-space:pre-wrap;overflow-wrap:anywhere}#${HISTORY_ARCHIVE_ID} .cgpt-rh-user{margin-left:auto;max-width:88%;padding:8px 10px;border-radius:12px;background:color-mix(in srgb,CanvasText 7%,Canvas)}#${HISTORY_ARCHIVE_ID} .cgpt-rh-tools{margin-top:8px;font-size:12px;opacity:.55}
`; (document.head || document.documentElement).appendChild(s);
}
const messages = () => { const modern = [...document.querySelectorAll(MODERN_TURN_SELECTOR)]; return modern.length ? modern : [...document.querySelectorAll(LEGACY_TURN_SELECTOR)]; };
function hidden(node, attr, value) { const old = node.getAttribute(attr) === 'true'; if (old === value) return; value ? node.setAttribute(attr, 'true') : node.removeAttribute(attr); }
function toolCount(node) { if (!node?.matches) return 0; return (node.matches(TOOL_SELECTOR) ? 1 : 0) + (node.querySelectorAll?.(TOOL_SELECTOR).length || 0); }
function toolRows(container) { return [...container.children].filter(child => !child.classList?.contains(TOOL_BUNDLE) && toolCount(child)); }
function toolMutation(records) { return records.some(r => [...r.addedNodes, ...r.removedNodes].some(n => n?.nodeType === 1 && (n.matches?.(TOOL_SELECTOR) || n.querySelector?.(TOOL_SELECTOR)))); }
function restoreTools(container) { container.querySelectorAll(`:scope>.${TOOL_BUNDLE}`).forEach(x => x.remove()); container.querySelectorAll(`:scope>[${TOOL_HIDDEN}="true"]`).forEach(x => hidden(x, TOOL_HIDDEN, false)); }
function setBundle(button, tools, expanded) { button.__members = tools; button.dataset.expanded = String(expanded); const count = tools.reduce((sum, node) => sum + toolCount(node), 0); const label = t.bundle(count, expanded); button.setAttribute('aria-label', label); button.querySelector('span').textContent = label; tools.forEach(x => hidden(x, TOOL_HIDDEN, !expanded)); }
function makeBundle(container, anchor) { const b = document.createElement('button'); b.type='button'; b.className=TOOL_BUNDLE; b.__anchor=anchor; b.innerHTML=`${icons.tools}`; b.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); setBundle(b,toolRows(container),b.dataset.expanded!=='true'); }); return b; }
function compact(container) {
if (!container?.isConnected) return; const observer=toolObservers.get(container); observer?.disconnect();
try {
if (!toolEnabled()) return restoreTools(container);
const existing=[...container.querySelectorAll(`:scope>.${TOOL_BUNDLE}`)];
const tools=toolRows(container), toolSet=new Set(tools);
container.querySelectorAll(`:scope>[${TOOL_HIDDEN}="true"]`).forEach(x=>{if(!toolSet.has(x))hidden(x,TOOL_HIDDEN,false)});
const count=tools.reduce((sum,node)=>sum+toolCount(node),0);
if(count<2){existing.forEach(x=>x.remove());tools.forEach(x=>hidden(x,TOOL_HIDDEN,false));return}
const anchor=tools[0];
let b=existing.find(x=>x.__anchor===anchor);
const expanded=b ? b.dataset.expanded==='true' : existing.some(x=>x.dataset.expanded==='true');
if(!b){b=makeBundle(container,anchor);container.insertBefore(b,anchor)}
setBundle(b,tools,expanded);
existing.forEach(x=>{if(x!==b)x.remove()});
} finally { if(observer&&container.isConnected) observer.observe(container,{childList:true,subtree:true}); }
}
function toolContainers(list){const set=new Set();list.forEach(m=>m.querySelectorAll('[class~="agent-turn"] div.flex.flex-col.grow').forEach(x=>set.add(x)));return set}
function syncTools(list){const set=toolContainers(list);if(!toolEnabled()){stopTools();set.forEach(restoreTools);return}for(const [c,o] of toolObservers){if(c.isConnected&&set.has(c))continue;o.disconnect();toolObservers.delete(c)}for(const c of set){if(!toolObservers.has(c)){const o=new MutationObserver(records=>{if(!toolMutation(records))return;clearTimeout(toolTimers.get(c));toolTimers.set(c,setTimeout(()=>{compact(c);updatePanel(messages().length)},80))});o.observe(c,{childList:true,subtree:true});toolObservers.set(c,o)}compact(c)}}
function stopTools(){toolObservers.forEach(o=>o.disconnect());toolObservers.clear();toolTimers.forEach(clearTimeout);toolTimers.clear();clearTimeout(toolUiTimer)}
function turnMutation(records){return records.some(r=>[...r.addedNodes,...r.removedNodes].some(n=>n?.nodeType===1&&(n.matches?.(TURN_SELECTOR)||n.querySelector?.(TURN_SELECTOR))))}
function findRoot(list){if(!list.length)return null;let r=list[0].parentElement;while(r&&!list.every(x=>r.contains(x)))r=r.parentElement;return r}
function bindList(list){const next=findRoot(list);if(!next||(next===listRoot&&listObserver))return;listObserver?.disconnect();discoveryObserver?.disconnect();listRoot=next;listObserver=new MutationObserver(records=>{if(turnMutation(records))scheduleUpdate(40)});listObserver.observe(next,{childList:true,subtree:true})}
function discover(){if(!document.body||listObserver)return;discoveryObserver?.disconnect();discoveryObserver=new MutationObserver(rs=>{hideWorkspaceLimitBanners();if(rs.some(r=>[...r.addedNodes].some(n=>n?.nodeType===1&&(n.matches?.(TURN_SELECTOR)||n.querySelector?.(TURN_SELECTOR))))){discoveryObserver.disconnect();scheduleUpdate(0)}});discoveryObserver.observe(document.body,{childList:true,subtree:true})}
function setScroll(next){if(next===scrollRoot)return;scrollRoot?.removeEventListener('scroll',onScroll);scrollRoot=next;scrollRoot?.addEventListener('scroll',onScroll,{passive:true})}
function apply(){const list=messages();if(!list.length){removePanel();listObserver?.disconnect();listObserver=null;stopTools();discover();return}const first=showAll?0:Math.max(0,list.length-visibleCount);list.forEach((x,i)=>hidden(x,HIDDEN,iTOP_THRESHOLD_PX*2){topLoadArmed=true;return}if(topLoadArmed&&top<=TOP_THRESHOLD_PX){topLoadArmed=false;apply();revealOlder(false)}}
function clearCollapse(){clearTimeout(collapseTimer);collapseTimer=null}
function clearPeek(){clearTimeout(peekTimer);peekTimer=null}
function label(b,text){if(!b)return;b.dataset.tooltip=text;b.setAttribute('aria-label',text)}
function setPeek(value){const p=document.getElementById(PANEL_ID);if(!p)return;clearPeek();p.dataset.peek=String(Boolean(value&&p.dataset.edge))}
function schedulePeek(delay=PANEL_EDGE_PEEK_MS){clearPeek();const p=document.getElementById(PANEL_ID);if(!p||!p.dataset.edge||p.dataset.collapsed!=='true'||p.dataset.dragging==='true')return;peekTimer=setTimeout(()=>{peekTimer=null;if(!p.isConnected||p.dataset.dragging==='true'||p.dataset.collapsed!=='true')return;if(p.matches(':hover')||p.contains(document.activeElement))return schedulePeek();p.dataset.peek='true'},delay)}
function scheduleCollapse(){clearCollapse();const p=document.getElementById(PANEL_ID);if(!p||p.dataset.collapsed==='true')return;collapseTimer=setTimeout(()=>{collapseTimer=null;if(!p.isConnected)return;if(p.contains(document.activeElement))return scheduleCollapse();collapse(true)},PANEL_AUTO_COLLAPSE_MS)}
function syncPanelToggle(p){const b=p?.querySelector('[data-action="panel"]');if(!b)return;const isCollapsed=p.dataset.collapsed==='true';b.innerHTML=isCollapsed?icons.panel:icons.collapse;label(b,isCollapsed?t.open:t.close)}
function readPanelPosition(){try{const v=JSON.parse(localStorage.getItem(PANEL_POSITION_KEY)||'null');return v&&Number.isFinite(v.x)&&Number.isFinite(v.y)?v:null}catch{return null}}
function savePanelPosition(p){const x=Number.parseFloat(p.style.left),y=Number.parseFloat(p.style.top);if(!Number.isFinite(x)||!Number.isFinite(y))return;localStorage.setItem(PANEL_POSITION_KEY,JSON.stringify({x,y,edge:p.dataset.edge||''}))}
function clampPanel(p,persist=false){if(!p?.isConnected)return;setPeek(false);const w=p.offsetWidth,h=p.offsetHeight,r=p.getBoundingClientRect();let x=Number.parseFloat(p.style.left),y=Number.parseFloat(p.style.top);if(!Number.isFinite(x))x=r.left;if(!Number.isFinite(y))y=r.top;const maxX=Math.max(0,innerWidth-w),maxY=Math.max(0,innerHeight-h),edge=p.dataset.edge||'';x=edge==='left'?0:edge==='right'?maxX:Math.min(Math.max(4,x),Math.max(4,maxX-4));y=Math.min(Math.max(4,y),Math.max(4,maxY-4));p.style.left=`${Math.round(x)}px`;p.style.top=`${Math.round(y)}px`;p.style.right='auto';p.style.bottom='auto';if(persist)savePanelPosition(p)}
function parkEdge(p,persist=true){clearCollapse();setPeek(false);p.dataset.collapsed='true';syncPanelToggle(p);requestAnimationFrame(()=>{clampPanel(p,persist);schedulePeek()})}
function restorePanelPosition(p){const pos=readPanelPosition();if(!pos)return;p.dataset.edge=pos.edge==='left'||pos.edge==='right'?pos.edge:'';p.style.left=`${pos.x}px`;p.style.top=`${pos.y}px`;p.style.right='auto';p.style.bottom='auto';if(p.dataset.edge){p.dataset.collapsed='true';syncPanelToggle(p)}requestAnimationFrame(()=>{clampPanel(p,false);if(p.dataset.edge)schedulePeek()})}
function collapse(value){const p=document.getElementById(PANEL_ID);if(!p)return;clearCollapse();setPeek(false);p.dataset.collapsed=String(value);syncPanelToggle(p);requestAnimationFrame(()=>{clampPanel(p,false);if(value&&p.dataset.edge)schedulePeek()});if(!value)scheduleCollapse()}
function startPanelDrag(p,e){const collapsed=p.dataset.collapsed==='true',handle=e.target.closest('.cgpt-rm-grip')||(collapsed?e.target.closest('[data-action="panel"]'):null);if(e.button!==0||!handle)return;clearCollapse();const r=p.getBoundingClientRect(),storedLeft=Number.parseFloat(p.style.left),storedTop=Number.parseFloat(p.style.top);dragState={pointerId:e.pointerId,startX:e.clientX,startY:e.clientY,left:Number.isFinite(storedLeft)?storedLeft:r.left,top:Number.isFinite(storedTop)?storedTop:r.top,moved:false,collapsed};}
function movePanelDrag(p,e){if(!dragState||e.pointerId!==dragState.pointerId)return;const dx=e.clientX-dragState.startX,dy=e.clientY-dragState.startY;if(!dragState.moved){if(Math.hypot(dx,dy)${icons.grip}`;p.addEventListener('click',e=>{const b=e.target.closest('button'),a=b?.dataset.action;if(a==='panel'){if(performance.now()startPanelDrag(p,e));p.addEventListener('pointermove',e=>{if(dragState)movePanelDrag(p,e);else{setPeek(false);scheduleCollapse();if(p.dataset.collapsed==='true'&&p.dataset.edge)schedulePeek()}});p.addEventListener('pointerup',e=>endPanelDrag(p,e));p.addEventListener('pointercancel',e=>endPanelDrag(p,e));p.addEventListener('pointerenter',()=>{setPeek(false);scheduleCollapse();if(p.dataset.collapsed==='true'&&p.dataset.edge)schedulePeek()});p.addEventListener('pointerleave',()=>{scheduleCollapse();schedulePeek()});p.addEventListener('focusin',()=>{setPeek(false);scheduleCollapse()});p.addEventListener('focusout',()=>{scheduleCollapse();schedulePeek()});p.addEventListener('keydown',scheduleCollapse);document.body.appendChild(p);restorePanelPosition(p);syncPanelToggle(p);scheduleCollapse()}
function updatePanel(total){const p=document.getElementById(PANEL_ID);if(!p)return;syncPanelToggle(p);const visible=showAll?total:Math.min(total,visibleCount);p.querySelector('.cgpt-rm-status').textContent=t.status(visible,total);const old=p.querySelector('[data-action="older"]'),nativeOlder=!showAll&&visible0&&!document.getElementById(HISTORY_ARCHIVE_ID),remoteReady=turboEnabled()&&Boolean(historyState.conversationId&&historyState.requestHeaders&&turboOriginalFetch)&&!historyState.exhausted,remoteOlder=remoteReady||archiveClosed;old.disabled=historyState.loading||(!nativeOlder&&!remoteOlder);label(old,historyState.loading?t.olderLoading:(archiveClosed?t.olderReopen:(!nativeOlder&&!remoteOlder?t.olderNone:t.older)));const toggle=p.querySelector('[data-action="toggle"]');label(toggle,showAll?t.recent:t.all);toggle.innerHTML=showAll?icons.recent:icons.all;label(p.querySelector('[data-action="reset"]'),t.reset);const turbo=p.querySelector('[data-action="turbo"]'),enabled=turboEnabled(),stats=window.__cgptRecentMessagesTrimStats;let tl=enabled?t.turboOn:t.turboOff;if(enabled&&stats?.beforeChars&&stats?.afterChars){tl+=` (${(stats.beforeChars/1e6).toFixed(2)} MB → ${(stats.afterChars/1e6).toFixed(2)} MB) · ${stats.retainedNodes} ${stats.flat?'messages':'nodes'}`;if(stats.flat&&stats.requestedTurns&&stats.networkTurns)tl+=` · API ${stats.requestedTurns}→${stats.networkTurns} turns`;if(stats.historyBlocked)tl+=` · auto history blocked`;if(stats.deep)tl+=` · deep ${stats.deepOriginalTurnNodes}→${stats.deepRetainedTurnNodes}`}label(turbo,tl);turbo.dataset.active=String(enabled);const tools=p.querySelector('[data-action="tools"]');label(tools,toolEnabled()?t.toolsOn:t.toolsOff);tools.dataset.active=String(toolEnabled())}
function removePanel(){clearCollapse();clearPeek();dragState=null;document.getElementById(PANEL_ID)?.remove()}
function scheduleUpdate(delay=60){clearTimeout(updateTimer);updateTimer=setTimeout(apply,delay)}
function routeChange(){if(location.href===lastUrl)return;lastUrl=location.href;installTurboFetch();showAll=false;visibleCount=INITIAL_MESSAGES;resetHistoryState();scheduleWorkspaceLimitScan();listObserver?.disconnect();listObserver=null;listRoot=null;stopTools();setScroll(null);discover();scheduleUpdate(0)}
function installRoutes(){for(const method of ['pushState','replaceState']){const original=history[method];if(original.__cgptRecentMessagesWrapped)continue;const wrapped=function(...args){installTurboFetch();const result=original.apply(this,args);queueMicrotask(routeChange);return result};Object.defineProperty(wrapped,'__cgptRecentMessagesWrapped',{value:true});history[method]=wrapped}document.addEventListener('click',e=>{const a=e.target.closest?.('a[href]');if(!a)return;try{if(new URL(a.href,location.href).origin===location.origin&&/\/c\/[0-9a-f-]+$/i.test(new URL(a.href,location.href).pathname))installTurboFetch()}catch{}},true);window.addEventListener('popstate',()=>{installTurboFetch();queueMicrotask(routeChange)})}
function init(){ensureStyle();scheduleWorkspaceLimitScan();installRoutes();installTurboFetch();window.addEventListener('resize',()=>{const p=document.getElementById(PANEL_ID);if(!p)return;setPeek(false);requestAnimationFrame(()=>{clampPanel(p,true);if(p.dataset.edge)schedulePeek()})},{passive:true});discover();scheduleUpdate(0)}
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init,{once:true});else init();
})();