// ==UserScript== // @name Claude Ultimate Enhancer // @namespace https://github.com/SysAdminDoc/Claude-Ultimate-Enhancer // @version 1.3.0 // @description All-in-one Claude.ai enhancement suite - theme engine, usage monitor, conversation search, prompt library, auto-scroll, DOM trimmer, code folding, visual upgrades, panel tools, and more // @author SysAdminDoc // @homepageURL https://github.com/SysAdminDoc/Claude-Ultimate-Enhancer // @supportURL https://github.com/SysAdminDoc/Claude-Ultimate-Enhancer/issues // @updateURL https://raw.githubusercontent.com/SysAdminDoc/Claude-Ultimate-Enhancer/main/Claude%20Ultimate%20Enhancer.user.js // @downloadURL https://raw.githubusercontent.com/SysAdminDoc/Claude-Ultimate-Enhancer/main/Claude%20Ultimate%20Enhancer.user.js // @match https://claude.ai/* // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @run-at document-start // @inject-into content // @license MIT // ==/UserScript== (function () { 'use strict'; if (window.__claudeUltimateLoaded) return; window.__claudeUltimateLoaded = true; const VERSION = '1.3.0'; const PREFIX = 'cue'; const LOG_TAG = '[CUE]'; // ===================================================================== // TRUSTED TYPES (claude.ai sets require-trusted-types-for 'script' on // some routes — without a policy, every innerHTML assignment throws.) // ===================================================================== let _ttPolicy = null; try { if (window.trustedTypes && window.trustedTypes.createPolicy) { _ttPolicy = window.trustedTypes.createPolicy(PREFIX + '-html', { createHTML: s => s }); } } catch (e) { /* policy already exists or TT disabled */ } function setHTML(el, html) { if (!el) return; el.innerHTML = _ttPolicy ? _ttPolicy.createHTML(html) : html; } // ===================================================================== // SETTINGS MANAGER // ===================================================================== const Settings = { _cache: {}, _defaults: { // -- Theme -- themeEnabled: true, themeVariant: 'oceanic', // oceanic | midnight | none fontOverride: true, // replace serif with sans // -- Layout -- wideMode: true, chatWidthPct: 90, densityMode: 'comfortable', // comfortable | compact | reading focusMode: false, // hide sidebar for deep work // -- Visual -- coloredButtons: true, coloredBoldItalic: true, smoothAnimations: true, customScrollbar: true, // -- Usage Monitor -- usageMonitor: true, usagePlan: 'pro', // pro | max5 | max20 usageFetchInterval: 300, // seconds between API polls // -- Feature Toggles -- featureToggles: true, // -- DOM Trimmer -- domTrimmer: false, domKeepVisible: 20, // -- Auto Features -- autoScroll: true, autoApprove: false, // -- Context Tracker -- contextTracker: true, contextWindow: 200000, warnThreshold: 0.55, criticalThreshold: 0.75, // -- Code Fold -- codeFold: true, // -- Copy Turn -- copyTurn: true, // -- Snippet Trigger -- snippetTrigger: true, // -- Conversation Tools -- conversationSearch: true, forkConversation: true, voiceDictation: true, // -- Prompt Library -- promptLibrary: true, // -- Response Monitor -- responseMonitor: true, notifySound: true, notifyFlash: true, // -- Paste Fix -- pasteFix: true, // -- Panel -- panelPosition: 'bottom-right', panelCollapsed: true, panelPinned: false, panelWidth: 320, }, get(key) { if (key in this._cache) return this._cache[key]; const val = GM_getValue(PREFIX + '_' + key, this._defaults[key]); this._cache[key] = val; return val; }, set(key, val) { this._cache[key] = val; GM_setValue(PREFIX + '_' + key, val); EventBus.emit('setting:' + key, val); EventBus.emit('settings:changed', { key, val }); }, toggle(key) { const v = !this.get(key); this.set(key, v); return v; }, defaults() { return { ...this._defaults }; }, reset() { Object.keys(this._defaults).forEach(k => this.set(k, this._defaults[k])); } }; // ===================================================================== // EVENT BUS // ===================================================================== const EventBus = { _listeners: {}, on(event, fn) { (this._listeners[event] ||= []).push(fn); return () => this.off(event, fn); }, off(event, fn) { const arr = this._listeners[event]; if (arr) this._listeners[event] = arr.filter(f => f !== fn); }, emit(event, data) { (this._listeners[event] || []).forEach(fn => { try { fn(data); } catch (e) { console.error(LOG_TAG, 'Event error:', event, e); } }); } }; // ===================================================================== // UTILITY HELPERS // ===================================================================== const $ = (sel, ctx = document) => ctx.querySelector(sel); const $$ = (sel, ctx = document) => [...ctx.querySelectorAll(sel)]; const sleep = ms => new Promise(r => setTimeout(r, ms)); const esc = s => s.replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]); const fmtNum = n => n >= 1e6 ? (n / 1e6).toFixed(1) + 'M' : n >= 1e3 ? (n / 1e3).toFixed(1) + 'K' : String(n); const fmtDur = ms => { const s = Math.floor(ms / 1000), m = Math.floor(s / 60), h = Math.floor(m / 60); return h > 0 ? `${h}h ${m % 60}m` : m > 0 ? `${m}m ${s % 60}s` : `${s}s`; }; const ts = () => new Date().toLocaleTimeString('en', { hour12: false }); function waitForElement(sel, timeout = 15000) { return new Promise((resolve, reject) => { const el = $(sel); if (el) return resolve(el); const obs = new MutationObserver(() => { const el = $(sel); if (el) { obs.disconnect(); clearTimeout(t); resolve(el); } }); obs.observe(document.documentElement, { childList: true, subtree: true }); const t = setTimeout(() => { obs.disconnect(); reject(new Error('Timeout: ' + sel)); }, timeout); }); } function injectCSS(id, css) { let el = document.getElementById(id); if (el) { el.textContent = css; return el; } el = document.createElement('style'); el.id = id; el.textContent = css; (document.head || document.documentElement).appendChild(el); return el; } function removeCSS(id) { const el = document.getElementById(id); if (el) el.remove(); } function showToast(msg, duration = 3500, type = 'info') { const colors = { info: '#58a6ff', success: '#3fb950', warn: '#d29922', error: '#f85149' }; const toast = document.createElement('div'); Object.assign(toast.style, { position: 'fixed', bottom: '24px', left: '50%', transform: 'translateX(-50%)', background: '#1a1a2e', color: '#e0e0e0', padding: '12px 24px', borderRadius: '10px', boxShadow: `0 4px 24px rgba(0,0,0,0.5), 0 0 0 1px ${colors[type]}40`, zIndex: '999999', fontFamily: '-apple-system, BlinkMacSystemFont, sans-serif', fontSize: '14px', borderLeft: `4px solid ${colors[type]}`, transition: 'opacity 0.4s, transform 0.4s', opacity: '0', maxWidth: '500px' }); toast.textContent = msg; document.body.appendChild(toast); requestAnimationFrame(() => { toast.style.opacity = '1'; }); setTimeout(() => { toast.style.opacity = '0'; toast.style.transform = 'translateX(-50%) translateY(10px)'; setTimeout(() => toast.remove(), 400); }, duration); } // ===================================================================== // DOM SELECTORS // ===================================================================== const SEL = { editor: '.ProseMirror', editorAlt: 'div[contenteditable="true"][translate="no"].ProseMirror', sendBtn: '[data-testid="send-button"]', sendBtnAlt: 'button[aria-label*="Send"]', stopBtn: '[data-testid="stop-button"]', stopBtnAlt: 'button[aria-label*="Stop"]', userMsg: '[data-testid="user-message"]', msgGroup: '.group', streaming: '[data-is-streaming="true"]', dialog: '[role="dialog"]', dialogOpen: '[role="dialog"][data-state="open"]', main: 'main', }; // ===================================================================== // DOM INTERFACE (from Prompt Deck v1.4) // ===================================================================== const DOM = { find(sel, ...fb) { for (const s of [sel, ...fb]) { const el = document.querySelector(s); if (el) return el; } return null; }, getEditor() { return this.find(SEL.editor, SEL.editorAlt); }, getSendButton() { return this.find(SEL.sendBtn, SEL.sendBtnAlt); }, getStopButton() { return this.find(SEL.stopBtn, SEL.stopBtnAlt); }, isGenerating() { const stop = this.getStopButton(); if (stop && stop.offsetParent !== null) return true; const send = this.getSendButton(); if (send && !send.disabled && send.offsetParent !== null) return false; return !!document.querySelector(SEL.streaming); }, async typeMessage(text) { const pm = this.getEditor(); if (!pm) throw new Error('Editor not found'); const editor = pm.editor; if (editor?.chain) { try { editor.chain().focus().clearContent().insertContent({ type: 'paragraph', content: [{ type: 'text', text }] }).run(); await sleep(300); return; } catch (e) { /* fb */ } } try { pm.focus(); document.execCommand('selectAll', false, null); document.execCommand('delete', false, null); document.execCommand('insertText', false, text); await sleep(300); return; } catch (e) { /* fb */ } pm.focus(); const p = document.createElement('p'); p.textContent = text; setHTML(pm, ''); pm.appendChild(p); pm.dispatchEvent(new Event('input', { bubbles: true })); await sleep(300); }, async sendMessage(text) { await this.typeMessage(text); await sleep(500); const btn = this.getSendButton(); if (btn && !btn.disabled) { btn.click(); return; } const pm = this.getEditor(); if (pm) { pm.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true })); return; } throw new Error('Cannot send'); }, getLastResponse() { const groups = document.querySelectorAll(SEL.msgGroup); for (let i = groups.length - 1; i >= 0; i--) { if (!groups[i].querySelector(SEL.userMsg)) return getCleanElementText(groups[i]); } return ''; }, }; function getCleanElementText(el) { if (!el) return ''; const clone = el.cloneNode(true); clone.querySelectorAll('[class^="' + PREFIX + '-"], [class*=" ' + PREFIX + '-"]').forEach(node => node.remove()); return (clone.innerText || clone.textContent || '').trim(); } function getConversationMessages(maxIndex = null) { const main = document.querySelector('main'); if (!main) return []; const groups = $$(SEL.msgGroup, main); const last = maxIndex === null ? groups.length - 1 : Math.min(maxIndex, groups.length - 1); const messages = []; for (let i = 0; i <= last; i++) { const group = groups[i]; const text = getCleanElementText(group); if (!text) continue; messages.push({ index: i, role: group.querySelector(SEL.userMsg) ? 'human' : 'assistant', text, html: group.innerHTML }); } return messages; } function getCurrentConversationId() { const match = location.pathname.match(/\/chat\/([a-f0-9-]+)/i); return match ? match[1] : null; } // ===================================================================== // CLAUDE API HELPERS // ===================================================================== const ClaudeAPI = { _orgId: null, async getOrgs() { const r = await fetch('/api/organizations', { credentials: 'include' }); const data = await r.json(); if (Array.isArray(data)) return data; return data.organizations || data.data || []; }, async getOrgId() { if (this._orgId) return this._orgId; const orgs = await this.getOrgs(); this._orgId = orgs[0]?.uuid || orgs[0]?.id || null; return this._orgId; }, async getUsage() { try { const orgId = await this.getOrgId(); if (!orgId) return null; const r = await fetch(`/api/organizations/${orgId}/usage`, { credentials: 'include' }); return r.json(); } catch (e) { return null; } }, async getSettings() { try { const r = await fetch('/api/account', { credentials: 'include' }); const data = await r.json(); return data.settings; } catch (e) { return null; } }, async toggleFeature(key, value, exclusiveKey = null) { try { const body = { [key]: value }; if (exclusiveKey && value) body[exclusiveKey] = false; const r = await fetch('/api/account/settings', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(body) }); return r.ok ? await r.json() : null; } catch (e) { return null; } }, _extractConversationArray(data) { if (Array.isArray(data)) return data; return data?.chat_conversations || data?.conversations || data?.items || data?.data || []; }, async listConversations(limit = 200) { try { const orgId = await this.getOrgId(); if (!orgId) return []; const out = []; let cursor = null; let offset = 0; for (let page = 0; page < 8 && out.length < limit; page++) { const pageLimit = Math.min(100, limit - out.length); const query = cursor ? `limit=${pageLimit}&cursor=${encodeURIComponent(cursor)}` : `limit=${pageLimit}&offset=${offset}`; const r = await fetch(`/api/organizations/${orgId}/chat_conversations?${query}`, { credentials: 'include' }); if (!r.ok) break; const data = await r.json(); const items = this._extractConversationArray(data); if (!items.length) break; out.push(...items); cursor = data.next_cursor || data.nextCursor || data.cursor || null; offset += items.length; if (!cursor && data.has_more !== true && data.hasMore !== true) break; } return out; } catch (e) { return []; } }, async getConversation(id) { try { const orgId = await this.getOrgId(); if (!orgId || !id) return null; const r = await fetch(`/api/organizations/${orgId}/chat_conversations/${id}`, { credentials: 'include' }); return r.ok ? await r.json() : null; } catch (e) { return null; } } }; // ===================================================================== // FETCH INTERCEPTOR (SSE stream usage data) // ===================================================================== const StreamMonitor = { _installed: false, lastUsage: null, lastMessageLimit: null, install() { if (this._installed) return; this._installed = true; const origFetch = window.fetch; const self = this; window.fetch = async function (...args) { try { const request = args[0]; const init = args[1] || {}; const url = typeof request === 'string' ? request : request?.url || ''; const method = (init.method || request?.method || 'GET').toUpperCase(); if (method === 'POST' && (url.includes('/completion') || url.includes('/chat_conversations'))) { EventBus.emit('usage:sent', { time: Date.now(), url }); } } catch (e) { /* never break app */ } const response = await origFetch.apply(this, args); try { const url = typeof args[0] === 'string' ? args[0] : args[0]?.url || ''; if (url.includes('/completion') || url.includes('/chat_conversations')) { const ct = response.headers.get('content-type') || ''; if (ct.includes('text/event-stream')) { self._processStream(response.clone()).catch(() => {}); } } } catch (e) { /* never break app */ } return response; }; }, async _processStream(response) { const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; try { while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (!line.startsWith('data: ')) continue; const j = line.substring(6).trim(); if (!j || j === '[DONE]') continue; try { this._processSSE(JSON.parse(j)); } catch (e) { /* skip */ } } } } catch (e) { /* stream aborted */ } EventBus.emit('stream:end'); }, _processSSE(data) { // Message limit data if (data.message_limit !== undefined) { this.lastMessageLimit = data.message_limit; EventBus.emit('stream:messageLimit', data.message_limit); } // Direct utilization value if (data.utilization !== undefined && typeof data.utilization === 'number') { EventBus.emit('stream:utilization', data.utilization); } // Token usage data if (data.usage) { this.lastUsage = data.usage; EventBus.emit('stream:usage', data.usage); } // Message limit within_limit type if (data.type === 'message_limit' && data.message_limit?.type === 'within_limit') { EventBus.emit('stream:messageLimit', data.message_limit); } // Rate limit data if (data.rate_limit) { EventBus.emit('stream:rateLimit', data.rate_limit); } // Generation lifecycle if (data.type === 'message_start') EventBus.emit('stream:start', data); if (data.type === 'message_stop') EventBus.emit('stream:stop', data); } }; // ===================================================================== // MODULE: THEME ENGINE // ===================================================================== const ThemeModule = { id: 'theme', THEMES: { oceanic: { name: 'Oceanic Dark', vars: ` --accent-main-000: 195 54.2% 44.2%; --accent-main-100: 195 63.1% 52.6%; --accent-main-200: 195 63.1% 52.6%; --accent-main-900: 0 0% 0%; --bg-000: 240 8% 11.4%; --bg-100: 240 8% 7.5%; --bg-200: 210 8% 4.8%; --bg-300: var(--bg-000); --bg-400: var(--bg-000); --bg-500: 0 0% 0%; --text-000: 228 33.3% 90.1%; --text-100: 228 33.3% 90.1%; --text-200: 230 9% 66.7%; --text-300: 230 9% 66.7%; --text-400: 228 4.8% 52.2%; --text-500: 228 4.8% 52.2%; --border-100: 231 16.5% 77.5%; --border-200: 231 16.5% 77.5%; --border-300: 231 16.5% 77.5%; --border-400: 231 16.5% 77.5%; --danger-000: 180 98.4% 68.1%; --danger-100: 180 67% 52.6%; --danger-200: 180 67% 52.6%; --danger-900: 180 46.5% 20.8%; --success-000: 277 59.1% 39.1%; --success-100: 277 75% 25.9%; --success-200: 277 75% 25.9%; --success-900: 307 100% 6.9%; --accent-pro-000: 71 84.6% 67.5%; --accent-pro-100: 71 40.2% 47.1%; --accent-secondary-000: 30 65.5% 60.1%; --accent-secondary-100: 30 70.9% 44.6%; --oncolor-100: 0 0% 93%; --oncolor-200: 240 6.7% 90.1%; --oncolor-300: 240 6.7% 90.1%; ` }, midnight: { name: 'Midnight', vars: ` --accent-main-000: 260 60% 50%; --accent-main-100: 260 70% 60%; --accent-main-200: 260 70% 60%; --accent-main-900: 0 0% 0%; --bg-000: 240 12% 9%; --bg-100: 240 12% 6%; --bg-200: 240 12% 4%; --bg-300: var(--bg-000); --bg-400: var(--bg-000); --bg-500: 0 0% 0%; --text-000: 220 30% 92%; --text-100: 220 30% 92%; --text-200: 220 10% 65%; --text-300: 220 10% 65%; --text-400: 220 5% 50%; --text-500: 220 5% 50%; --border-100: 240 10% 25%; --border-200: 240 10% 25%; --border-300: 240 10% 25%; --border-400: 240 10% 25%; --danger-000: 0 80% 65%; --danger-100: 0 70% 55%; --success-000: 150 60% 45%; --success-100: 150 50% 35%; --oncolor-100: 0 0% 93%; --oncolor-200: 0 0% 88%; --oncolor-300: 0 0% 88%; ` }, // Catppuccin Mocha — https://github.com/catppuccin/catppuccin // Base palette converted to HSL: base=#1e1e2e, mantle=#181825, crust=#11111b, // text=#cdd6f4, subtext0=#a6adc8, mauve=#cba6f7 (accent), red=#f38ba8, green=#a6e3a1 mocha: { name: 'Catppuccin Mocha', vars: ` --accent-main-000: 267 84% 81%; --accent-main-100: 267 84% 81%; --accent-main-200: 267 84% 75%; --accent-main-900: 240 21% 15%; --bg-000: 240 21% 15%; --bg-100: 240 23% 12%; --bg-200: 240 23% 9%; --bg-300: var(--bg-000); --bg-400: var(--bg-000); --bg-500: 240 23% 9%; --text-000: 226 64% 88%; --text-100: 226 64% 88%; --text-200: 228 24% 72%; --text-300: 228 24% 72%; --text-400: 228 17% 64%; --text-500: 228 17% 64%; --border-100: 234 13% 31%; --border-200: 234 13% 31%; --border-300: 234 13% 31%; --border-400: 234 13% 31%; --danger-000: 343 81% 75%; --danger-100: 343 81% 68%; --danger-200: 343 81% 68%; --danger-900: 343 50% 25%; --success-000: 115 54% 76%; --success-100: 115 54% 68%; --success-200: 115 54% 68%; --success-900: 115 50% 20%; --accent-pro-000: 41 86% 83%; --accent-pro-100: 41 86% 70%; --accent-secondary-000: 23 92% 75%; --accent-secondary-100: 23 92% 65%; --oncolor-100: 240 21% 15%; --oncolor-200: 240 21% 15%; --oncolor-300: 240 21% 15%; ` }, // Catppuccin Macchiato — base=#24273a, mantle=#1e2030, crust=#181926, // text=#cad3f5, subtext0=#a5adcb, mauve=#c6a0f6 (accent), red=#ed8796, green=#a6da95 macchiato: { name: 'Catppuccin Macchiato', vars: ` --accent-main-000: 267 83% 80%; --accent-main-100: 267 83% 80%; --accent-main-200: 267 83% 74%; --accent-main-900: 232 23% 18%; --bg-000: 232 23% 18%; --bg-100: 233 24% 15%; --bg-200: 233 30% 11%; --bg-300: var(--bg-000); --bg-400: var(--bg-000); --bg-500: 233 30% 11%; --text-000: 227 68% 88%; --text-100: 227 68% 88%; --text-200: 228 20% 73%; --text-300: 228 20% 73%; --text-400: 228 15% 65%; --text-500: 228 15% 65%; --border-100: 231 16% 34%; --border-200: 231 16% 34%; --border-300: 231 16% 34%; --border-400: 231 16% 34%; --danger-000: 351 74% 73%; --danger-100: 351 74% 66%; --danger-200: 351 74% 66%; --danger-900: 351 50% 25%; --success-000: 105 48% 72%; --success-100: 105 48% 64%; --success-200: 105 48% 64%; --success-900: 105 40% 20%; --accent-pro-000: 40 70% 78%; --accent-pro-100: 40 70% 65%; --accent-secondary-000: 21 86% 73%; --accent-secondary-100: 21 86% 63%; --oncolor-100: 232 23% 18%; --oncolor-200: 232 23% 18%; --oncolor-300: 232 23% 18%; ` }, // Catppuccin Frappe — base=#303446, mantle=#292c3c, crust=#232634, // text=#c6d0f5, subtext0=#a5adce, mauve=#ca9ee6 (accent), red=#e78284, green=#a6d189 frappe: { name: 'Catppuccin Frappé', vars: ` --accent-main-000: 277 59% 76%; --accent-main-100: 277 59% 76%; --accent-main-200: 277 59% 70%; --accent-main-900: 229 19% 23%; --bg-000: 229 19% 23%; --bg-100: 231 19% 20%; --bg-200: 231 20% 17%; --bg-300: var(--bg-000); --bg-400: var(--bg-000); --bg-500: 231 20% 17%; --text-000: 227 70% 87%; --text-100: 227 70% 87%; --text-200: 228 17% 73%; --text-300: 228 17% 73%; --text-400: 228 13% 65%; --text-500: 228 13% 65%; --border-100: 230 13% 38%; --border-200: 230 13% 38%; --border-300: 230 13% 38%; --border-400: 230 13% 38%; --danger-000: 359 68% 71%; --danger-100: 359 68% 64%; --danger-200: 359 68% 64%; --danger-900: 359 50% 25%; --success-000: 96 44% 68%; --success-100: 96 44% 60%; --success-200: 96 44% 60%; --success-900: 96 40% 20%; --accent-pro-000: 40 62% 73%; --accent-pro-100: 40 62% 60%; --accent-secondary-000: 20 79% 70%; --accent-secondary-100: 20 79% 60%; --oncolor-100: 229 19% 23%; --oncolor-200: 229 19% 23%; --oncolor-300: 229 19% 23%; ` }, // Catppuccin Latte (LIGHT theme) — base=#eff1f5, mantle=#e6e9ef, crust=#dce0e8, // text=#4c4f69, subtext0=#6c6f85, mauve=#8839ef (accent), red=#d20f39, green=#40a02b latte: { name: 'Catppuccin Latte', light: true, vars: ` --accent-main-000: 266 85% 58%; --accent-main-100: 266 85% 58%; --accent-main-200: 266 85% 50%; --accent-main-900: 220 23% 95%; --bg-000: 220 23% 95%; --bg-100: 227 16% 92%; --bg-200: 223 16% 88%; --bg-300: var(--bg-000); --bg-400: var(--bg-000); --bg-500: 223 16% 88%; --text-000: 234 16% 35%; --text-100: 234 16% 35%; --text-200: 233 10% 47%; --text-300: 233 10% 47%; --text-400: 233 10% 55%; --text-500: 233 10% 55%; --border-100: 225 14% 77%; --border-200: 225 14% 77%; --border-300: 225 14% 77%; --border-400: 225 14% 77%; --danger-000: 347 87% 44%; --danger-100: 347 87% 38%; --danger-200: 347 87% 38%; --danger-900: 347 60% 85%; --success-000: 109 58% 40%; --success-100: 109 58% 34%; --success-200: 109 58% 34%; --success-900: 109 40% 85%; --accent-pro-000: 35 77% 49%; --accent-pro-100: 35 77% 42%; --accent-secondary-000: 22 99% 52%; --accent-secondary-100: 22 99% 45%; --oncolor-100: 220 23% 95%; --oncolor-200: 227 16% 92%; --oncolor-300: 227 16% 92%; ` } }, init() { this._apply(); EventBus.on('setting:themeEnabled', () => this._apply()); EventBus.on('setting:themeVariant', () => this._apply()); EventBus.on('setting:fontOverride', () => this._apply()); }, _apply() { if (!Settings.get('themeEnabled') || Settings.get('themeVariant') === 'none') { removeCSS(PREFIX + '-theme'); return; } const theme = this.THEMES[Settings.get('themeVariant')]; if (!theme) { removeCSS(PREFIX + '-theme'); return; } const fontCSS = Settings.get('fontOverride') ? ` :root { --font-anthropic-serif: var(--font-anthropic-sans) !important; --font-ui-serif: var(--font-ui) !important; } ` : ''; const modeSelector = theme.light ? '[data-theme=claude][data-mode=light]' : '[data-theme=claude][data-mode=dark]'; injectCSS(PREFIX + '-theme', ` ${modeSelector} { ${theme.vars} } ${fontCSS} * { scrollbar-color: hsla(var(--bg-300, 240 8% 11.4%)/50%) transparent !important; } *, *:after, *:before { --tw-gradient-from-position: none !important; } `); }, destroy() { removeCSS(PREFIX + '-theme'); } }; // ===================================================================== // MODULE: FOCUS MODE (anti-distraction) // ===================================================================== const FocusModule = { id: 'focusMode', init() { this._apply(); EventBus.on('setting:focusMode', () => this._apply()); }, _apply() { if (!Settings.get('focusMode')) { removeCSS(PREFIX + '-focus'); return; } injectCSS(PREFIX + '-focus', ` /* Hide the sidebar/navigation */ nav, [class*="sidebar"], [class*="side-navigation"] { display: none !important; } /* Expand main content area */ main { margin-left: 0 !important; } [class*="has-sidebar"] { grid-template-columns: 1fr !important; } `); }, destroy() { removeCSS(PREFIX + '-focus'); } }; // ===================================================================== // MODULE: DENSITY // ===================================================================== const DensityModule = { id: 'density', MODES: { comfortable: { name: 'Comfortable', css: '' }, // default — no overrides compact: { name: 'Compact', css: ` .font-claude-message, [class*="prose"] { font-size: 14px !important; line-height: 1.45 !important; } .group, [data-testid="user-message"] { padding-top: 4px !important; padding-bottom: 4px !important; } h1 { font-size: 1.3em !important; } h2 { font-size: 1.15em !important; } h3 { font-size: 1.05em !important; } pre { padding: 8px !important; margin: 6px 0 !important; } ul, ol { margin: 4px 0 !important; } p { margin: 4px 0 !important; } ` }, reading: { name: 'Reading', css: ` .font-claude-message, [class*="prose"] { font-size: 17px !important; line-height: 1.8 !important; letter-spacing: 0.01em !important; } .group, [data-testid="user-message"] { padding-top: 16px !important; padding-bottom: 16px !important; } p { margin: 12px 0 !important; } pre { padding: 16px !important; margin: 16px 0 !important; font-size: 15px !important; } ` } }, init() { this._apply(); EventBus.on('setting:densityMode', () => this._apply()); }, _apply() { const mode = Settings.get('densityMode'); const m = this.MODES[mode]; if (!m || !m.css) { removeCSS(PREFIX + '-density'); return; } injectCSS(PREFIX + '-density', m.css); }, destroy() { removeCSS(PREFIX + '-density'); } }; // ===================================================================== // MODULE: WIDE LAYOUT // ===================================================================== const LayoutModule = { id: 'layout', _observer: null, init() { this._apply(); EventBus.on('setting:wideMode', () => this._apply()); EventBus.on('setting:chatWidthPct', () => this._apply()); }, _apply() { if (!Settings.get('wideMode')) { removeCSS(PREFIX + '-layout'); this._stopObserver(); return; } const pct = Settings.get('chatWidthPct'); injectCSS(PREFIX + '-layout', ` /* Wide layout override */ [class*="mx-auto"] { max-width: ${pct}% !important; } .mx-auto { max-width: ${pct}% !important; } div[data-test-render-count] { max-width: ${pct}% !important; } div[data-test-render-count] > * { max-width: 100% !important; } /* Also widen parent containers */ main > div > div > div { max-width: ${pct}% !important; } `); }, _stopObserver() { if (this._observer) { this._observer.disconnect(); this._observer = null; } }, destroy() { removeCSS(PREFIX + '-layout'); this._stopObserver(); } }; // ===================================================================== // MODULE: VISUAL ENHANCEMENT // ===================================================================== const VisualModule = { id: 'visual', _boldObserver: null, COLORS: { orange: 'darkorange', green: 'springgreen', lime: 'limegreen', darkGreen: '#00ad00', red: 'crimson', yellow: 'gold', skyblue: 'deepskyblue', blue: '#4285f4', violet: 'darkviolet', purple: '#9c27b0', cyan: '#00bcd4', pink: '#e91e63', gray: 'gray', teal: '#009688' }, init() { this._applyButtons(); this._applyBoldItalic(); this._applyAnimations(); this._applyScrollbar(); this._startBoldObserver(); EventBus.on('setting:coloredButtons', () => this._applyButtons()); EventBus.on('setting:coloredBoldItalic', () => { this._applyBoldItalic(); this._colorBoldElements(); }); EventBus.on('setting:smoothAnimations', () => this._applyAnimations()); EventBus.on('setting:customScrollbar', () => this._applyScrollbar()); }, _applyButtons() { if (!Settings.get('coloredButtons')) { removeCSS(PREFIX + '-buttons'); return; } const C = this.COLORS; injectCSS(PREFIX + '-buttons', ` /* Copy - Orange */ button[aria-label*="Copy" i] svg { color: ${C.orange} !important; opacity: 0.9; transition: all 0.3s ease !important; } button[aria-label*="Copy" i]:hover svg { filter: drop-shadow(0 0 8px ${C.orange}) !important; opacity: 1 !important; } /* Edit - Yellow */ button[aria-label*="Edit" i] svg { color: ${C.yellow} !important; opacity: 0.8; transition: all 0.3s ease !important; } button[aria-label*="Edit" i]:hover svg { filter: drop-shadow(0 0 8px ${C.yellow}) !important; opacity: 1 !important; } /* Retry - Sky Blue */ button[aria-label*="Retry" i] svg, button[aria-label*="Regenerate" i] svg { color: ${C.skyblue} !important; opacity: 0.9; transition: all 0.3s ease !important; } button[aria-label*="Retry" i]:hover svg { filter: drop-shadow(0 0 8px ${C.skyblue}) !important; opacity: 1 !important; } /* Thumbs Up - Green */ button[aria-label*="Good" i] svg { color: ${C.darkGreen} !important; opacity: 0.9; transition: all 0.3s ease !important; } button[aria-label*="Good" i]:hover svg { filter: drop-shadow(0 0 8px ${C.darkGreen}) !important; opacity: 1 !important; } button[aria-label*="Good" i]:hover { background: rgba(0,173,0,0.12) !important; } /* Thumbs Down - Red */ button[aria-label*="Bad" i] svg { color: ${C.red} !important; opacity: 0.9; transition: all 0.3s ease !important; } button[aria-label*="Bad" i]:hover svg { filter: drop-shadow(0 0 8px ${C.red}) !important; opacity: 1 !important; } button[aria-label*="Bad" i]:hover { background: rgba(220,53,69,0.12) !important; } /* Delete - Red */ button[aria-label*="Delete" i] svg { color: #e02e2a !important; } button[aria-label*="Delete" i]:hover { background: rgba(224,46,42,0.15) !important; box-shadow: 0 0 15px rgba(224,46,42,0.3) !important; } /* Share - Sky Blue */ button[aria-label*="Share" i] svg { color: ${C.skyblue} !important; opacity: 0.8; transition: all 0.3s ease !important; } button[aria-label*="Share" i]:hover svg { filter: drop-shadow(0 0 8px ${C.skyblue}) !important; } /* List markers */ .font-claude-message ul li::marker, [class*="prose"] ul li::marker { color: ${C.green} !important; } .font-claude-message ol li::marker, [class*="prose"] ol li::marker { color: ${C.blue} !important; font-weight: bold !important; } /* Blockquote */ blockquote { border-left: 4px solid ${C.green} !important; padding-left: 16px !important; opacity: 0.9; } /* Code blocks */ pre { border-radius: 8px !important; } `); }, _applyBoldItalic() { if (!Settings.get('coloredBoldItalic')) { removeCSS(PREFIX + '-bold'); return; } const C = this.COLORS; injectCSS(PREFIX + '-bold', ` .font-claude-message strong, .font-claude-message b, [class*="prose"] strong, [class*="prose"] b { color: ${C.green} !important; } .font-claude-message em, .font-claude-message i, [class*="prose"] em, [class*="prose"] i { color: ${C.skyblue} !important; } `); }, _colorBoldElements() { if (!Settings.get('coloredBoldItalic')) return; $$('b:not([data-cue-styled]), strong:not([data-cue-styled])').forEach(el => { el.setAttribute('data-cue-styled', '1'); el.style.setProperty('color', this.COLORS.green, 'important'); }); $$('i:not([data-cue-styled]), em:not([data-cue-styled])').forEach(el => { el.setAttribute('data-cue-styled', '1'); el.style.setProperty('color', this.COLORS.skyblue, 'important'); }); }, _startBoldObserver() { if (this._boldObserver) this._boldObserver.disconnect(); let timer; this._boldObserver = new MutationObserver(() => { clearTimeout(timer); timer = setTimeout(() => this._colorBoldElements(), 200); }); const start = () => { if (document.body) this._boldObserver.observe(document.body, { childList: true, subtree: true }); else setTimeout(start, 200); }; start(); }, _applyAnimations() { if (!Settings.get('smoothAnimations')) { removeCSS(PREFIX + '-anim'); return; } injectCSS(PREFIX + '-anim', ` @keyframes cue-fade-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } button svg { transition: all 0.3s cubic-bezier(0.4,0,0.2,1) !important; } button:focus-visible { outline: 2px solid #4285f4 !important; outline-offset: 2px !important; } `); }, _applyScrollbar() { if (!Settings.get('customScrollbar')) { removeCSS(PREFIX + '-scroll'); return; } injectCSS(PREFIX + '-scroll', ` ::-webkit-scrollbar { width: 10px; height: 10px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: rgba(128,128,128,0.25); border-radius: 5px; } ::-webkit-scrollbar-thumb:hover { background: rgba(128,128,128,0.4); } `); }, destroy() { removeCSS(PREFIX + '-buttons'); removeCSS(PREFIX + '-bold'); removeCSS(PREFIX + '-anim'); removeCSS(PREFIX + '-scroll'); if (this._boldObserver) this._boldObserver.disconnect(); } }; // ===================================================================== // MODULE: PASTE FIX // ===================================================================== const PasteFixModule = { id: 'pasteFix', _handler: null, init() { this._handler = (e) => { if (!Settings.get('pasteFix')) return; const cd = e.clipboardData || window.clipboardData; if (cd.types.includes('text/plain') && cd.types.includes('text/html')) { e.preventDefault(); e.stopImmediatePropagation(); const plain = cd.getData('text/plain'); const dt = new DataTransfer(); dt.setData('text/plain', plain.trimStart()); e.target.dispatchEvent(new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true })); } }; document.addEventListener('paste', this._handler, true); }, destroy() { if (this._handler) document.removeEventListener('paste', this._handler, true); } }; // ===================================================================== // MODULE: AUTO-SCROLL // ===================================================================== const AutoScrollModule = { id: 'autoScroll', _observer: null, _timer: null, init() { this._start(); EventBus.on('setting:autoScroll', (v) => v ? this._start() : this._stop()); }, scrollToBottom() { for (const el of [document.querySelector('main'), document.querySelector('[class*="overflow-y"]'), document.querySelector('[class*="scroll"]')].filter(Boolean)) { if (el.scrollHeight > el.clientHeight) { el.scrollTop = el.scrollHeight; return; } } window.scrollTo({ top: document.body.scrollHeight, behavior: 'instant' }); }, _start() { if (this._observer) this._observer.disconnect(); this._observer = new MutationObserver(() => { if (!Settings.get('autoScroll')) return; clearTimeout(this._timer); this._timer = setTimeout(() => this.scrollToBottom(), 150); }); this._observer.observe(document.querySelector('main') || document.body, { childList: true, subtree: true, characterData: true }); }, _stop() { if (this._observer) this._observer.disconnect(); }, destroy() { this._stop(); } }; // ===================================================================== // MODULE: AUTO-APPROVE // ===================================================================== const AutoApproveModule = { id: 'autoApprove', _observer: null, init() { this._start(); EventBus.on('setting:autoApprove', (v) => v ? this._start() : this._stop()); }, _start() { if (this._observer) this._observer.disconnect(); this._observer = new MutationObserver(() => { if (!Settings.get('autoApprove')) return; const dlg = $(SEL.dialogOpen) || $(SEL.dialog); if (!dlg) return; for (const btn of dlg.querySelectorAll('button')) { const t = btn.textContent.toLowerCase().trim(); if (t.includes('allow for this chat') || t.includes('allow once') || t.includes('allow always')) { btn.click(); showToast('Auto-approved: ' + btn.textContent.trim(), 2000, 'info'); return; } } }); this._observer.observe(document.body, { childList: true, subtree: true }); }, _stop() { if (this._observer) this._observer.disconnect(); }, destroy() { this._stop(); } }; // ===================================================================== // MODULE: USAGE TRACKER (local rolling send counters) // ===================================================================== const UsageTrackerModule = { id: 'usageTracker', STORAGE_KEY: PREFIX + '_usage_events', _events: [], _sessionStart: Date.now(), init() { this._load(); EventBus.on('usage:sent', (d) => this.record(d?.time || Date.now())); }, _load() { try { const saved = GM_getValue(this.STORAGE_KEY, '[]'); this._events = JSON.parse(saved).filter(t => Number.isFinite(t)); } catch (e) { this._events = []; } this._prune(); }, _save() { GM_setValue(this.STORAGE_KEY, JSON.stringify(this._events)); }, _prune() { const cutoff = Date.now() - (8 * 24 * 60 * 60 * 1000); this._events = this._events.filter(t => t >= cutoff); }, record(time) { this._events.push(time); this._prune(); this._save(); EventBus.emit('usage:localUpdated', this.getStats()); }, getStats() { const now = Date.now(); const within = ms => this._events.filter(t => now - t <= ms).length; return { session: this._events.filter(t => t >= this._sessionStart).length, fiveHour: within(5 * 60 * 60 * 1000), sevenDay: within(7 * 24 * 60 * 60 * 1000), totalCached: this._events.length }; }, destroy() {} }; // ===================================================================== // MODULE: CONTEXT TRACKER // ===================================================================== const ContextModule = { id: 'context', _interval: null, QUALITY_DEGRADE_TURNS: 25, NEW_CHAT_THRESHOLD: 0.85, data: { turns: 0, userMsgs: 0, assistantMsgs: 0, estimatedTokens: 0, chatStartTime: null, sseUtilization: null, history: [], }, init() { this._count(); this._interval = setInterval(() => { this._count(); }, 10000); EventBus.on('stream:usage', (u) => { const total = (u.input_tokens || 0) + (u.output_tokens || 0) + (u.cache_read_input_tokens || 0) + (u.cache_creation_input_tokens || 0); if (total > 0) this.data.sseUtilization = total / Settings.get('contextWindow'); }); EventBus.on('stream:utilization', (v) => { this.data.sseUtilization = v; }); }, _count() { if (!Settings.get('contextTracker')) return; const main = document.querySelector('main') || document.querySelector('[class*="conversation"]'); if (!main) return; const userMsgs = main.querySelectorAll(SEL.userMsg); const allGroups = main.querySelectorAll(SEL.msgGroup); let ac = 0; allGroups.forEach(g => { if (!g.querySelector(SEL.userMsg)) ac++; }); this.data.userMsgs = userMsgs.length; this.data.assistantMsgs = ac; this.data.turns = Math.min(userMsgs.length, ac); if (this.data.turns > 0 && !this.data.chatStartTime) this.data.chatStartTime = Date.now(); // Token estimation: max of char-based and word-based const text = main.innerText || ''; const charEst = Math.ceil(text.length / 4); const wordEst = Math.ceil(text.split(/\s+/).filter(w => w).length * 1.3); this.data.estimatedTokens = Math.max(charEst, wordEst); // History for burn rate const lastPt = this.data.history[this.data.history.length - 1]; if (!lastPt || Date.now() - lastPt.time > 30000) { this.data.history.push({ time: Date.now(), tokens: this.data.estimatedTokens, turns: this.data.turns }); if (this.data.history.length > 120) this.data.history.shift(); } EventBus.emit('context:updated', this.getHealth()); }, getFill() { return this.data.sseUtilization !== null ? this.data.sseUtilization : this.data.estimatedTokens / Settings.get('contextWindow'); }, getBurnRate() { const h = this.data.history; if (h.length < 2) return 0; const now = Date.now(), recent = h.filter(p => now - p.time < 300000); if (recent.length < 2) return 0; const dt = (recent[recent.length - 1].time - recent[0].time) / 60000; return dt < 0.5 ? 0 : Math.round((recent[recent.length - 1].tokens - recent[0].tokens) / dt); }, getTimeToFull() { const rate = this.getBurnRate(); if (rate <= 0) return null; const remaining = (this.NEW_CHAT_THRESHOLD - this.getFill()) * Settings.get('contextWindow'); return remaining <= 0 ? 0 : remaining / rate; }, getHealth() { const fill = this.getFill(), turns = this.data.turns, ttf = this.getTimeToFull(); let score = 100; // Fill scoring if (fill > this.NEW_CHAT_THRESHOLD) score -= 60; else if (fill > Settings.get('criticalThreshold')) score -= 40; else if (fill > Settings.get('warnThreshold')) score -= 20; // Turn quality degradation if (turns > this.QUALITY_DEGRADE_TURNS * 2) score -= 30; else if (turns > this.QUALITY_DEGRADE_TURNS) score -= 15; else if (turns > this.QUALITY_DEGRADE_TURNS * 0.6) score -= 5; // Time-to-full urgency if (ttf !== null && ttf < 5) score -= 15; else if (ttf !== null && ttf < 15) score -= 5; score = Math.max(0, Math.min(100, score)); let level, advice; if (score >= 70) { level = 'good'; advice = 'Context is healthy'; } else if (score >= 40) { level = 'warn'; advice = 'Consider wrapping up soon'; } else if (score >= 15) { level = 'critical'; advice = 'Start a new chat soon'; } else { level = 'danger'; advice = 'Start a new chat now'; } return { score, fill: Math.min(1, fill), level, advice, turns, ttf, tokens: this.data.estimatedTokens, burnRate: this.getBurnRate(), duration: this.data.chatStartTime ? Date.now() - this.data.chatStartTime : 0 }; }, destroy() { clearInterval(this._interval); } }; // ===================================================================== // MODULE: CACHE INDICATOR // ===================================================================== const CacheModule = { id: 'cacheIndicator', _timerInterval: null, lastCacheHit: false, lastCacheTime: null, CACHE_TTL: 300, // 5 minutes default TTL for prompt caching init() { EventBus.on('stream:usage', (u) => { const cacheRead = u.cache_read_input_tokens || 0; if (cacheRead > 0) { this.lastCacheHit = true; this.lastCacheTime = Date.now(); EventBus.emit('cache:hit', { tokens: cacheRead }); this._startCountdown(); } else { this.lastCacheHit = false; EventBus.emit('cache:miss'); } }); EventBus.on('navigation', () => { this.lastCacheHit = false; this.lastCacheTime = null; this._stopCountdown(); }); }, _startCountdown() { this._stopCountdown(); this._timerInterval = setInterval(() => { if (!this.lastCacheTime) { this._stopCountdown(); return; } const elapsed = (Date.now() - this.lastCacheTime) / 1000; const remaining = Math.max(0, this.CACHE_TTL - elapsed); EventBus.emit('cache:timer', { remaining, total: this.CACHE_TTL }); if (remaining <= 0) { this.lastCacheHit = false; this.lastCacheTime = null; EventBus.emit('cache:expired'); this._stopCountdown(); } }, 1000); }, _stopCountdown() { if (this._timerInterval) { clearInterval(this._timerInterval); this._timerInterval = null; } }, destroy() { this._stopCountdown(); } }; // ===================================================================== // MODULE: COST ESTIMATOR // ===================================================================== const CostModule = { id: 'costEstimate', // Pricing per million tokens (as of mid-2026) PRICING: { 'opus': { input: 15.00, output: 75.00, cache_read: 1.50, cache_write: 18.75 }, 'sonnet': { input: 3.00, output: 15.00, cache_read: 0.30, cache_write: 3.75 }, 'haiku': { input: 0.80, output: 4.00, cache_read: 0.08, cache_write: 1.00 }, }, sessionCost: 0, lastModel: 'sonnet', messageCosts: [], init() { EventBus.on('stream:usage', (u) => this._calcCost(u)); EventBus.on('stream:start', (d) => { // Try to detect model from stream data if (d.message?.model) { const m = d.message.model.toLowerCase(); if (m.includes('opus')) this.lastModel = 'opus'; else if (m.includes('haiku')) this.lastModel = 'haiku'; else this.lastModel = 'sonnet'; } }); EventBus.on('navigation', () => { this.sessionCost = 0; this.messageCosts = []; }); }, _calcCost(usage) { const pricing = this.PRICING[this.lastModel] || this.PRICING.sonnet; const inputTokens = usage.input_tokens || 0; const outputTokens = usage.output_tokens || 0; const cacheRead = usage.cache_read_input_tokens || 0; const cacheWrite = usage.cache_creation_input_tokens || 0; const cost = (inputTokens * pricing.input / 1e6) + (outputTokens * pricing.output / 1e6) + (cacheRead * pricing.cache_read / 1e6) + (cacheWrite * pricing.cache_write / 1e6); this.sessionCost += cost; this.messageCosts.push({ time: Date.now(), cost, model: this.lastModel }); EventBus.emit('cost:updated', { messageCost: cost, sessionCost: this.sessionCost, model: this.lastModel }); }, getSessionCost() { return this.sessionCost; }, getLastModel() { return this.lastModel; }, destroy() {} }; // ===================================================================== // MODULE: RESPONSE MONITOR // ===================================================================== const ResponseModule = { id: 'responseMonitor', _interval: null, _timerInterval: null, _lastLen: 0, _lastChangeTs: 0, _stableCount: 0, _audioCtx: null, _flashTimer: null, _originalTitle: '', status: 'idle', // idle | generating | stuck | complete | truncated genStartTime: null, lastDuration: 0, lastWords: 0, lastChars: 0, init() { this._lastChangeTs = Date.now(); this._interval = setInterval(() => this._poll(), 2000); }, _poll() { if (!Settings.get('responseMonitor')) return; const gen = DOM.isGenerating(); const convo = (document.querySelector('main') || document.body).innerText; const now = Date.now(); if (convo.length !== this._lastLen) { this._lastLen = convo.length; this._lastChangeTs = now; this._stableCount = 0; } else { this._stableCount++; } const wasGen = this.status === 'generating'; if (gen) { if (this.status !== 'generating') { this.genStartTime = Date.now(); this._startTimer(); } this.status = 'generating'; if (Settings.get('autoScroll')) AutoScrollModule.scrollToBottom(); if (now - this._lastChangeTs > 90000) this.status = 'stuck'; EventBus.emit('response:status', this.status); } else if (wasGen && this._stableCount >= 2) { const resp = DOM.getLastResponse(); this.lastDuration = this.genStartTime ? Date.now() - this.genStartTime : 0; this._stopTimer(); // Stats this.lastChars = resp.length; this.lastWords = resp.split(/\s+/).filter(w => w.length > 0).length; this.status = this._isTruncated(resp) ? 'truncated' : 'complete'; this.genStartTime = null; EventBus.emit('response:status', this.status); EventBus.emit('response:complete', { duration: this.lastDuration, words: this.lastWords, chars: this.lastChars }); if (Settings.get('autoScroll')) AutoScrollModule.scrollToBottom(); // Notifications if (Settings.get('notifySound')) this._playSound(); if (Settings.get('notifyFlash')) this._flashTab(); } else if (!gen && this.status !== 'truncated' && this.status !== 'complete') { this.status = 'idle'; EventBus.emit('response:status', this.status); } }, _startTimer() { this._stopTimer(); this._timerInterval = setInterval(() => EventBus.emit('response:timer', this.genStartTime ? Date.now() - this.genStartTime : 0), 200); }, _stopTimer() { if (this._timerInterval) { clearInterval(this._timerInterval); this._timerInterval = null; } }, _isTruncated(text) { if (!text || text.length < 50) return false; const t = text.trim(); if ((t.match(/```/g) || []).length % 2 !== 0) return true; const tail = t.toLowerCase().slice(-300); for (const s of ['continue to keep the chat going', 'response was cut off', 'character limit', 'length limit', 'hit the limit']) { if (tail.includes(s)) return true; } const ll = t.split('\n').filter(l => l.trim()).pop() || ''; if (!/[.!?:)`}\]>]$|COMPLETE$/i.test(ll.trim()) && !/[{;=,]$/.test(ll.trim()) && ll.length > 30) return true; return false; }, _playSound() { if (document.hasFocus()) return; try { if (!this._audioCtx) this._audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const now = this._audioCtx.currentTime; [660, 880].forEach((freq, i) => { const osc = this._audioCtx.createOscillator(); const gain = this._audioCtx.createGain(); osc.type = 'sine'; osc.frequency.value = freq; gain.gain.setValueAtTime(0.12, now + i * 0.12); gain.gain.exponentialRampToValueAtTime(0.001, now + i * 0.12 + 0.25); osc.connect(gain); gain.connect(this._audioCtx.destination); osc.start(now + i * 0.12); osc.stop(now + i * 0.12 + 0.25); }); } catch (e) { /* AudioContext not available */ } }, _flashTab() { if (document.hasFocus()) return; this._stopFlash(); this._originalTitle = document.title; let on = true; this._flashTimer = setInterval(() => { document.title = on ? '>> Claude Done <<' : this._originalTitle; on = !on; }, 800); const stopOnFocus = () => { this._stopFlash(); window.removeEventListener('focus', stopOnFocus); }; window.addEventListener('focus', stopOnFocus); setTimeout(() => this._stopFlash(), 30000); }, _stopFlash() { if (this._flashTimer) { clearInterval(this._flashTimer); this._flashTimer = null; if (this._originalTitle) document.title = this._originalTitle; } }, destroy() { clearInterval(this._interval); this._stopTimer(); this._stopFlash(); } }; // ===================================================================== // MODULE: DOM TRIMMER // ===================================================================== const DomTrimmerModule = { id: 'domTrimmer', _observer: null, _cache: new Map(), init() { EventBus.on('setting:domTrimmer', (v) => { if (!v) this.restoreAll(); }); this._observer = new MutationObserver(() => { if (Settings.get('domTrimmer')) this._prune(); }); if (document.body) this._observer.observe(document.body, { childList: true, subtree: true }); }, _getMessages() { const main = $(SEL.main); if (!main) return []; // Look for conversation message groups return $$('.group, [data-testid="user-message"]', main).map(el => { // Walk up to find the actual removable container let target = el; while (target.parentElement && target.parentElement !== main && !target.parentElement.matches('main, [class*="flex-col"]')) { target = target.parentElement; } return target; }).filter((el, i, arr) => arr.indexOf(el) === i); // deduplicate }, _prune() { const msgs = this._getMessages(); const keep = Settings.get('domKeepVisible'); if (msgs.length <= keep) return; const toRemove = msgs.slice(0, msgs.length - keep); toRemove.forEach(el => { if (el.dataset.cueTrimmed) return; const id = 'trim-' + (this._cache.size + 1); const placeholder = document.createElement('div'); placeholder.className = PREFIX + '-trim-placeholder'; placeholder.dataset.trimId = id; placeholder.style.cssText = 'height:4px;margin:2px 0;background:rgba(128,128,128,0.1);border-radius:2px;'; this._cache.set(id, { html: el.outerHTML, parent: el.parentElement, next: el.nextSibling }); el.replaceWith(placeholder); }); EventBus.emit('trimmer:pruned', { removed: toRemove.length, total: msgs.length }); }, restoreAll() { this._cache.forEach((data, id) => { const ph = $(`[data-trim-id="${id}"]`); if (ph) { const tmp = document.createElement('div'); setHTML(tmp, data.html); ph.replaceWith(tmp.firstElementChild); } }); this._cache.clear(); EventBus.emit('trimmer:restored'); }, destroy() { if (this._observer) this._observer.disconnect(); this.restoreAll(); } }; // ===================================================================== // MODULE: CODE FOLD // ===================================================================== const CodeFoldModule = { id: 'codeFold', _observer: null, FOLD_THRESHOLD: 15, // lines before folding kicks in VISIBLE_LINES: 6, // lines to show when folded init() { this._applyStyles(); this._start(); EventBus.on('setting:codeFold', (v) => { if (v) this._start(); else this._stop(); }); }, _applyStyles() { injectCSS(PREFIX + '-codefold', ` .${PREFIX}-fold-container { position: relative; } .${PREFIX}-fold-container.folded pre { max-height: none; } .${PREFIX}-fold-container.folded .${PREFIX}-fold-code { display: none; } .${PREFIX}-fold-container.folded .${PREFIX}-fold-preview { display: block; } .${PREFIX}-fold-container:not(.folded) .${PREFIX}-fold-code { display: block; } .${PREFIX}-fold-container:not(.folded) .${PREFIX}-fold-preview { display: none; } .${PREFIX}-fold-toggle { display: block; width: 100%; padding: 4px 12px; margin: 0; background: rgba(88,166,255,0.06); border: none; border-top: 1px solid rgba(88,166,255,0.15); color: #58a6ff; font-size: 11px; font-family: monospace; cursor: pointer; text-align: left; transition: background 0.2s; } .${PREFIX}-fold-toggle:hover { background: rgba(88,166,255,0.12); } .${PREFIX}-fold-preview { white-space: pre; overflow: hidden; } `); }, _start() { if (!Settings.get('codeFold')) return; // Process existing code blocks this._processAll(); // Observe for new ones if (this._observer) this._observer.disconnect(); let timer; this._observer = new MutationObserver(() => { if (!Settings.get('codeFold')) return; clearTimeout(timer); timer = setTimeout(() => this._processAll(), 500); }); this._observer.observe(document.querySelector('main') || document.body, { childList: true, subtree: true }); }, _stop() { if (this._observer) this._observer.disconnect(); // Unfold all $$('.' + PREFIX + '-fold-container').forEach(c => { c.classList.remove('folded'); const toggle = c.querySelector('.' + PREFIX + '-fold-toggle'); if (toggle) toggle.remove(); const preview = c.querySelector('.' + PREFIX + '-fold-preview'); if (preview) preview.remove(); c.classList.remove(PREFIX + '-fold-container'); }); }, _processAll() { $$('pre').forEach(pre => { if (pre.closest('#' + PREFIX + '-panel')) return; if (pre.dataset.cueFolded) return; const code = pre.querySelector('code') || pre; const text = code.innerText || code.textContent || ''; const lines = text.split('\n'); if (lines.length <= this.FOLD_THRESHOLD) return; pre.dataset.cueFolded = '1'; // Wrap in container const container = document.createElement('div'); container.className = PREFIX + '-fold-container folded'; pre.parentNode.insertBefore(container, pre); // Move pre into container as the full code pre.classList.add(PREFIX + '-fold-code'); container.appendChild(pre); // Create preview (first N lines) const previewEl = document.createElement('pre'); previewEl.className = PREFIX + '-fold-preview'; const previewCode = document.createElement('code'); // Copy class from original code element for syntax highlighting if (code.className) previewCode.className = code.className; previewCode.textContent = lines.slice(0, this.VISIBLE_LINES).join('\n'); previewEl.appendChild(previewCode); container.insertBefore(previewEl, pre); // Add toggle button const hidden = lines.length - this.VISIBLE_LINES; const toggle = document.createElement('button'); toggle.className = PREFIX + '-fold-toggle'; toggle.textContent = `[+${hidden} lines] Click to expand`; toggle.addEventListener('click', () => { const isFolded = container.classList.contains('folded'); container.classList.toggle('folded'); toggle.textContent = isFolded ? `[-${hidden} lines] Click to collapse` : `[+${hidden} lines] Click to expand`; }); container.appendChild(toggle); }); }, destroy() { this._stop(); removeCSS(PREFIX + '-codefold'); } }; // ===================================================================== // MODULE: COPY TURN // ===================================================================== const CopyTurnModule = { id: 'copyTurn', _observer: null, init() { this._applyStyles(); this._start(); EventBus.on('setting:copyTurn', (v) => { if (v) this._start(); else this._stop(); }); }, _applyStyles() { injectCSS(PREFIX + '-copyturn', ` .${PREFIX}-copy-turn-btn { position: absolute; top: 4px; right: 4px; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); color: #888; font-size: 10px; padding: 2px 6px; border-radius: 4px; cursor: pointer; opacity: 0; transition: opacity 0.2s, background 0.2s; font-family: -apple-system, BlinkMacSystemFont, sans-serif; z-index: 10; } .group:hover .${PREFIX}-copy-turn-btn, [data-testid="user-message"]:hover ~ .${PREFIX}-copy-turn-btn, .${PREFIX}-copy-turn-btn:hover { opacity: 1; } .${PREFIX}-copy-turn-btn:hover { background: rgba(88,166,255,0.15); color: #58a6ff; } .${PREFIX}-copy-turn-btn.copied { color: #3fb950; border-color: rgba(63,185,80,0.3); } `); }, _start() { if (!Settings.get('copyTurn')) return; this._processAll(); if (this._observer) this._observer.disconnect(); let timer; this._observer = new MutationObserver(() => { if (!Settings.get('copyTurn')) return; clearTimeout(timer); timer = setTimeout(() => this._processAll(), 500); }); this._observer.observe(document.querySelector('main') || document.body, { childList: true, subtree: true }); }, _stop() { if (this._observer) this._observer.disconnect(); $$('.' + PREFIX + '-copy-turn-btn').forEach(b => b.remove()); }, _processAll() { const main = document.querySelector('main'); if (!main) return; $$(SEL.msgGroup, main).forEach(group => { if (group.querySelector('.' + PREFIX + '-copy-turn-btn')) return; // Ensure the group has relative positioning for absolute child const cs = window.getComputedStyle(group); if (cs.position === 'static') group.style.position = 'relative'; const btn = document.createElement('button'); btn.className = PREFIX + '-copy-turn-btn'; btn.textContent = 'Copy'; btn.title = 'Copy this turn'; btn.addEventListener('click', (e) => { e.stopPropagation(); const text = getCleanElementText(group); navigator.clipboard.writeText(text).then(() => { btn.textContent = 'Copied!'; btn.classList.add('copied'); setTimeout(() => { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 1500); }).catch(() => { // Fallback const ta = document.createElement('textarea'); ta.value = text; ta.style.cssText = 'position:fixed;left:-9999px'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); btn.textContent = 'Copied!'; setTimeout(() => { btn.textContent = 'Copy'; }, 1500); }); }); group.appendChild(btn); }); }, destroy() { this._stop(); removeCSS(PREFIX + '-copyturn'); } }; // ===================================================================== // MODULE: ERROR LOG // ===================================================================== const ErrorLogModule = { id: 'errorLog', _logs: [], MAX_LOGS: 100, init() { // Capture module errors EventBus.on('module:error', (data) => this._add('error', data.module, data.error)); }, _add(level, source, message) { this._logs.push({ time: new Date().toLocaleTimeString('en', { hour12: false }), level, source, message: typeof message === 'string' ? message : (message?.message || String(message)) }); if (this._logs.length > this.MAX_LOGS) this._logs.shift(); EventBus.emit('errorlog:updated', this._logs); }, getLogs() { return [...this._logs]; }, clear() { this._logs = []; EventBus.emit('errorlog:updated', this._logs); }, getHTML() { if (this._logs.length === 0) return 'No errors'; return this._logs.slice(-10).map(l => { const color = l.level === 'error' ? '#f85149' : l.level === 'warn' ? '#d29922' : '#888'; return `
elements
document.querySelectorAll('pre').forEach(pre => {
if (pre.closest('#' + PREFIX + '-panel')) return;
const code = pre.querySelector('code') || pre;
addBlock(code, null, 'pre');
});
// Strategy 2: Multi-line not inside
document.querySelectorAll('code').forEach(code => {
if (code.closest('#' + PREFIX + '-panel') || code.closest('pre')) return;
const raw = code.innerText || code.textContent || '';
if (raw.includes('\n') && raw.trim().length >= 10) addBlock(code, null, 'code');
});
// Strategy 3: Code-related classes/attributes
const codeSelectors = [
'[class*="code-block"]', '[class*="code_block"]', '[class*="codeblock"]',
'[class*="CodeBlock"]', '[class*="code-content"]', '[class*="hljs"]',
'[class*="shiki"]', '[class*="prism"]', '[class*="highlight"]',
'[data-code]', '[data-language]',
].join(',');
try {
document.querySelectorAll(codeSelectors).forEach(el => {
if (el.closest('#' + PREFIX + '-panel')) return;
if (el.querySelector('pre') || el.closest('pre')) return;
const text = el.innerText || el.textContent || '';
if (text.includes('\n') && text.trim().length >= 10) addBlock(el, null, 'class');
});
} catch (e) { /* invalid selector */ }
// Strategy 4: Find copy buttons and trace back to code containers
document.querySelectorAll('button').forEach(btn => {
if (btn.closest('#' + PREFIX + '-panel')) return;
const txt = (btn.textContent || '').toLowerCase().trim();
const ariaLabel = (btn.getAttribute('aria-label') || '').toLowerCase();
if (txt === 'copy' || txt === 'copy code' || ariaLabel.includes('copy')) {
let container = btn.closest('[class*="code"]') || btn.closest('[class*="Code"]') || btn.parentElement?.parentElement;
if (!container) return;
const pre = container.querySelector('pre');
const code = container.querySelector('code');
const target = pre || code || container;
const text = target.innerText || target.textContent || '';
if (text.trim().length >= 10) addBlock(target, null, 'copy-btn');
}
});
return blocks;
}
// =====================================================================
// MODULE: PANEL TOOLS
// =====================================================================
const PanelToolsModule = {
id: 'panelTools',
init() {},
_copyLastCode() {
const blocks = scanCodeBlocks();
if (blocks.length > 0) {
const last = blocks[blocks.length - 1];
navigator.clipboard.writeText(last.text).then(() => {
showToast('Copied ' + last.lang + ' (' + last.lines + 'L)', 2000, 'success');
}).catch(() => {});
} else { showToast('No code blocks found', 2000, 'warn'); }
},
_copyLastResponse() {
const text = DOM.getLastResponse();
if (!text) { showToast('No response to copy', 2000, 'warn'); return; }
navigator.clipboard.writeText(text).then(() => {
showToast('Copied response (' + text.split(/\s+/).length + ' words)', 2000, 'success');
}).catch(() => {
// Fallback
const ta = document.createElement('textarea'); ta.value = text;
ta.style.cssText = 'position:fixed;left:-9999px'; document.body.appendChild(ta);
ta.select(); document.execCommand('copy'); document.body.removeChild(ta);
showToast('Copied response (fallback)', 2000, 'success');
});
},
_showExportMenu() {
const main = document.querySelector('main');
if (!main) { showToast('No conversation found', 2000, 'warn'); return; }
const groups = main.querySelectorAll(SEL.msgGroup);
if (groups.length === 0) { showToast('No messages found', 2000, 'warn'); return; }
// Gather messages
const messages = [];
groups.forEach((g) => {
const isUser = !!g.querySelector(SEL.userMsg);
const text = getCleanElementText(g);
const html = g.innerHTML;
if (text) messages.push({ role: isUser ? 'human' : 'assistant', text, html });
});
const overlay = document.createElement('div');
overlay.className = PREFIX + '-modal-overlay';
const modal = document.createElement('div');
modal.className = PREFIX + '-modal';
setHTML(modal, `
Export Conversation
${messages.length} messages
`);
overlay.appendChild(modal);
document.body.appendChild(overlay);
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
$(`#${PREFIX}-export-cancel`, modal).addEventListener('click', () => overlay.remove());
modal.querySelectorAll('[data-fmt]').forEach(btn => {
btn.addEventListener('click', () => {
const fmt = btn.dataset.fmt;
const datestamp = new Date().toISOString().slice(0, 10);
let content, mime, ext;
if (fmt === 'md') {
content = '# Claude Conversation Export\n_Exported: ' + new Date().toISOString() + '_\n\n---\n\n';
messages.forEach(m => { content += '## ' + (m.role === 'human' ? 'Human' : 'Assistant') + '\n\n' + m.text + '\n\n---\n\n'; });
mime = 'text/markdown'; ext = 'md';
} else if (fmt === 'json') {
content = JSON.stringify({
exported: new Date().toISOString(),
messages: messages.map(m => ({ role: m.role, content: m.text }))
}, null, 2);
mime = 'application/json'; ext = 'json';
} else {
content = 'Claude Chat Export ' +
'' +
'Claude Conversation Export
Exported: ' + new Date().toISOString() + '
';
messages.forEach(m => {
content += '' + (m.role === 'human' ? 'Human' : 'Assistant') + '' +
'' + m.text.replace(//g, '>').replace(/\n/g, '
') + '';
});
content += '';
mime = 'text/html'; ext = 'html';
}
const blob = new Blob([content], { type: mime });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = 'claude-chat-' + datestamp + '.' + ext;
document.body.appendChild(a); a.click(); document.body.removeChild(a);
URL.revokeObjectURL(url);
overlay.remove();
showToast('Chat exported as ' + ext.toUpperCase() + '!', 2000, 'success');
});
});
},
destroy() {}
};
// =====================================================================
// CONTROL PANEL UI
// =====================================================================
const ControlPanel = {
_panel: null,
_visible: false,
_usageData: null,
_claudeSettings: null,
_refreshTimer: null,
_turnCursor: 0,
FEATURES: [
{ key: 'enabled_monkeys_in_a_barrel', name: 'Code Execution', desc: 'Virtual code environment', exclusive: 'enabled_artifacts_attachments' },
{ key: 'enabled_artifacts_attachments', name: 'Repl Tool', desc: 'Additional Artifacts features', exclusive: 'enabled_monkeys_in_a_barrel' },
{ key: 'enabled_saffron', name: 'Memory', desc: 'Cross-conversation memory' },
{ key: 'enabled_saffron_search', name: 'Search Chats', desc: 'Chat history search' },
{ key: 'enabled_sourdough', name: 'Projects', desc: 'Project memory' },
],
USAGE_PLANS: {
pro: { name: 'Pro', multiplier: 1 },
max5: { name: 'Max 5x', multiplier: 5 },
max20: { name: 'Max 20x', multiplier: 20 },
},
LOCAL_USAGE_BASELINE: { session: 20, fiveHour: 45, sevenDay: 225 },
show() {
if (!this._panel) return;
this._panel.classList.remove(PREFIX + '-panel-hidden');
this._panel.style.pointerEvents = 'auto';
this._visible = true;
},
hide(force = false) {
if (!this._panel || (Settings.get('panelPinned') && !force)) return;
this._panel.classList.add(PREFIX + '-panel-hidden');
this._visible = false;
},
toggle() {
if (!this._panel) return;
if (this._panel.classList.contains(PREFIX + '-panel-hidden')) this.show();
else this.hide(true);
},
async build() {
if (this._panel) return;
this._createStyles();
this._panel = document.createElement('div');
this._panel.id = PREFIX + '-panel';
this._panel.className = PREFIX + '-panel-hidden';
setHTML(this._panel, this._getHTML());
document.body.appendChild(this._panel);
this._applyPanelChrome();
this._bindEvents();
this._loadData();
// Auto-refresh usage every 60s
this._refreshTimer = setInterval(() => this._loadData(), 60000);
},
_createStyles() {
injectCSS(PREFIX + '-panel-css', `
/* Hover trigger strip - invisible 6px strip on right edge */
#${PREFIX}-hover-strip {
position: fixed; top: 0; right: 0; width: 6px; height: 100vh;
z-index: 99998; cursor: pointer;
}
#${PREFIX}-hover-strip::after {
content: ''; position: absolute; top: 50%; right: 0;
transform: translateY(-50%); width: 3px; height: 60px;
background: rgba(88,166,255,0.2); border-radius: 2px;
transition: opacity 0.3s, background 0.3s;
}
#${PREFIX}-hover-strip:hover::after { background: rgba(88,166,255,0.5); width: 4px; }
/* Panel container */
#${PREFIX}-panel {
position: fixed; top: 0; right: 0; width: var(--cue-panel-width, 320px); height: 100vh;
min-width: 280px; max-width: min(640px, 90vw);
background: #0d0d14; border-left: 1px solid #2a2a3a;
z-index: 99999; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
color: #c8c8d8; font-size: 11px; overflow-y: auto; overflow-x: hidden;
transition: transform 0.3s cubic-bezier(0.4,0,0.2,1);
box-shadow: -4px 0 30px rgba(0,0,0,0.5);
display: flex; flex-direction: column;
}
#${PREFIX}-panel.${PREFIX}-panel-hidden { transform: translateX(100%); pointer-events: none; }
#${PREFIX}-panel.${PREFIX}-panel-pinned { transform: translateX(0); pointer-events: auto; }
#${PREFIX}-panel::-webkit-scrollbar { width: 4px; }
#${PREFIX}-panel::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 2px; }
#${PREFIX}-panel-resize {
position: absolute; top: 0; left: -4px; width: 8px; height: 100%;
cursor: ew-resize; z-index: 1;
}
#${PREFIX}-panel-resize:hover { background: rgba(88,166,255,0.14); }
/* Header - minimal */
.${PREFIX}-hdr {
display: flex; align-items: center; justify-content: space-between;
padding: 4px 8px; background: rgba(255,255,255,0.03);
border-bottom: 1px solid #2a2a3a; flex-shrink: 0;
}
.${PREFIX}-hdr h2 { margin: 0; font-size: 12px; font-weight: 600; color: #e8e8f0; }
.${PREFIX}-hdr-ver { font-size: 9px; color: #555; margin-left: 4px; }
.${PREFIX}-hdr-actions { display: flex; align-items: center; gap: 2px; }
.${PREFIX}-icon-btn,
.${PREFIX}-close {
background: none; border: none; color: #555; width: 20px; height: 20px;
cursor: pointer; font-size: 14px; display: flex; align-items: center;
justify-content: center; border-radius: 4px;
}
.${PREFIX}-icon-btn { font-size: 11px; font-weight: 700; }
.${PREFIX}-icon-btn.active { color: #58a6ff; background: rgba(88,166,255,0.1); }
.${PREFIX}-icon-btn:hover { color: #c8c8d8; background: rgba(255,255,255,0.06); }
.${PREFIX}-close:hover { color: #f88; background: rgba(255,80,80,0.1); }
/* Sections - ultra compact */
.${PREFIX}-section { padding: 2px 8px; border-bottom: 1px solid rgba(255,255,255,0.04); }
.${PREFIX}-section-title {
font-size: 9px; font-weight: 700; text-transform: uppercase;
letter-spacing: 1px; color: #58a6ff; margin: 2px 0 1px;
}
/* Rows */
.${PREFIX}-row {
display: flex; align-items: center; justify-content: space-between;
padding: 1px 0; min-height: 18px;
}
.${PREFIX}-row-label { color: #a8a8b8; font-size: 11px; }
/* Toggle switch - compact */
.${PREFIX}-toggle {
position: relative; width: 28px; height: 14px; cursor: pointer; display: inline-block; flex-shrink: 0;
}
.${PREFIX}-toggle input { opacity: 0; width: 0; height: 0; position: absolute; }
.${PREFIX}-toggle-track {
position: absolute; inset: 0; background: #2a2a3a; border-radius: 7px;
transition: background 0.2s;
}
.${PREFIX}-toggle input:checked + .${PREFIX}-toggle-track { background: #3fb950; }
.${PREFIX}-toggle-thumb {
position: absolute; top: 2px; left: 2px; width: 10px; height: 10px;
background: #d0d0d0; border-radius: 50%; transition: transform 0.2s;
}
.${PREFIX}-toggle input:checked ~ .${PREFIX}-toggle-thumb { transform: translateX(14px); background: #fff; }
/* Slider - compact */
.${PREFIX}-slider {
height: 3px; -webkit-appearance: none; appearance: none;
background: #2a2a3a; border-radius: 2px; outline: none; margin: 0;
}
.${PREFIX}-slider::-webkit-slider-thumb {
-webkit-appearance: none; width: 12px; height: 12px;
background: #58a6ff; border-radius: 50%; cursor: pointer;
}
/* Select - compact */
.${PREFIX}-select {
background: #1a1a2a; color: #c8c8d8; border: 1px solid #2a2a3a;
border-radius: 4px; padding: 1px 4px; font-size: 10px; cursor: pointer; outline: none;
}
/* Usage bars - compact */
.${PREFIX}-usage-bar {
height: 5px; background: #1a1a2a; border-radius: 3px;
overflow: hidden; margin: 2px 0 1px;
}
.${PREFIX}-usage-fill {
height: 100%; border-radius: 3px; transition: width 0.5s;
background: linear-gradient(90deg, #3fb950, #58a6ff);
}
.${PREFIX}-usage-fill.warn { background: linear-gradient(90deg, #d29922, #e8a020); }
.${PREFIX}-usage-fill.danger { background: linear-gradient(90deg, #f85149, #ff6b6b); }
.${PREFIX}-usage-pct { font-size: 10px; color: #888; }
.${PREFIX}-usage-local { color: #666; font-size: 9px; }
/* Feature toggle rows - compact */
.${PREFIX}-feat-row {
display: flex; align-items: center; justify-content: space-between;
padding: 1px 0;
}
.${PREFIX}-feat-name { font-size: 11px; color: #c8c8d8; }
.${PREFIX}-feat-desc { display: none; }
.${PREFIX}-feat-btn {
padding: 1px 8px; border-radius: 4px; border: 1px solid #2a2a3a;
cursor: pointer; font-size: 9px; font-weight: 600; transition: all 0.2s;
min-width: 32px; text-align: center;
}
.${PREFIX}-feat-btn.on { background: rgba(63,185,80,0.15); color: #3fb950; border-color: rgba(63,185,80,0.3); }
.${PREFIX}-feat-btn.off { background: rgba(255,255,255,0.04); color: #888; }
.${PREFIX}-feat-btn:hover { opacity: 0.8; }
/* Context health */
.${PREFIX}-ctx-fill {
height: 100%; border-radius: 3px; transition: width 0.5s, background 0.3s;
}
.${PREFIX}-ctx-fill.good { background: linear-gradient(90deg, #3fb950, #2ea043); }
.${PREFIX}-ctx-fill.warn { background: linear-gradient(90deg, #d29922, #e8a020); }
.${PREFIX}-ctx-fill.critical { background: linear-gradient(90deg, #f85149, #ff6b6b); }
/* Status dot */
.${PREFIX}-status-dot {
width: 6px; height: 6px; border-radius: 50%; display: inline-block; margin-right: 4px;
}
.${PREFIX}-status-dot.idle { background: #555; }
.${PREFIX}-status-dot.generating { background: #d29922; animation: ${PREFIX}-pulse 1.2s infinite; }
.${PREFIX}-status-dot.complete { background: #3fb950; }
.${PREFIX}-status-dot.stuck { background: #f85149; }
.${PREFIX}-status-dot.truncated { background: #f85149; }
@keyframes ${PREFIX}-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
/* Prompt buttons - compact */
.${PREFIX}-prompt-btn {
display: inline-flex; align-items: center;
padding: 2px 6px; border-radius: 4px; border: 1px solid #2a2a3a;
background: rgba(255,255,255,0.03); color: #c8c8d8;
cursor: pointer; font-size: 10px; transition: all 0.2s; margin: 1px;
}
.${PREFIX}-prompt-btn:hover { background: rgba(88,166,255,0.1); border-color: #58a6ff40; color: #fff; }
.${PREFIX}-prompt-cat { display: none; }
.${PREFIX}-tool-btn {
padding: 3px 8px; border-radius: 4px; border: 1px solid #2a2a3a;
background: rgba(255,255,255,0.03); color: #c8c8d8;
cursor: pointer; font-size: 10px; transition: all 0.2s;
}
.${PREFIX}-tool-btn:hover { background: rgba(88,166,255,0.1); border-color: #58a6ff40; color: #fff; }
.${PREFIX}-tool-btn.primary { color: #58a6ff; }
.${PREFIX}-tool-btn.warn { color: #d29922; }
.${PREFIX}-tool-btn.success { color: #3fb950; }
.${PREFIX}-search-input {
width: 100%; box-sizing: border-box; background: #1a1a2a; color: #c8c8d8;
border: 1px solid #2a2a3a; border-radius: 4px; padding: 4px 6px;
font-size: 10px; outline: none; margin: 3px 0;
}
.${PREFIX}-search-input:focus { border-color: #58a6ff; }
.${PREFIX}-search-result {
display: flex; justify-content: space-between; align-items: center; gap: 4px;
padding: 3px 0; border-top: 1px solid rgba(255,255,255,0.04);
}
.${PREFIX}-search-title {
min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
font-size: 10px; color: #c8c8d8;
}
.${PREFIX}-search-date { color: #555; font-size: 9px; flex-shrink: 0; }
/* Prompt editor modal */
.${PREFIX}-modal-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.6);
z-index: 100000; display: flex; align-items: center; justify-content: center;
backdrop-filter: blur(4px);
}
.${PREFIX}-modal {
background: #0d0d14; border: 1px solid #2a2a3a; border-radius: 12px;
padding: 16px; width: 500px; max-width: 90vw; max-height: 80vh;
overflow-y: auto; box-shadow: 0 20px 60px rgba(0,0,0,0.6);
}
.${PREFIX}-modal h3 { margin: 0 0 12px; color: #e8e8f0; font-size: 14px; }
.${PREFIX}-modal input, .${PREFIX}-modal textarea {
width: 100%; background: #1a1a2a; color: #c8c8d8; border: 1px solid #2a2a3a;
border-radius: 6px; padding: 8px 10px; font-size: 12px;
font-family: inherit; outline: none; box-sizing: border-box;
}
.${PREFIX}-modal input:focus, .${PREFIX}-modal textarea:focus { border-color: #58a6ff; }
.${PREFIX}-modal textarea { min-height: 180px; resize: vertical; margin-top: 8px; }
.${PREFIX}-modal-actions { display: flex; gap: 6px; justify-content: flex-end; margin-top: 12px; }
.${PREFIX}-modal-btn {
padding: 6px 14px; border-radius: 6px; border: 1px solid #2a2a3a;
cursor: pointer; font-size: 12px; font-weight: 500; transition: all 0.2s;
}
.${PREFIX}-modal-btn.primary { background: #58a6ff; color: #000; border-color: #58a6ff; }
.${PREFIX}-modal-btn.primary:hover { background: #79b8ff; }
.${PREFIX}-modal-btn.secondary { background: transparent; color: #888; }
.${PREFIX}-modal-btn.secondary:hover { color: #ccc; background: rgba(255,255,255,0.05); }
.${PREFIX}-modal-btn.danger { background: transparent; color: #f85149; border-color: rgba(248,81,73,0.3); }
.${PREFIX}-modal-btn.danger:hover { background: rgba(248,81,73,0.1); }
/* Settings grid for two-column layout */
.${PREFIX}-settings-grid {
display: grid; grid-template-columns: 1fr 1fr; gap: 0 6px;
}
/* Turn navigator */
.${PREFIX}-turn-item {
display: flex; align-items: center; gap: 4px;
padding: 1px 4px; cursor: pointer; border-radius: 3px;
font-size: 10px; color: #888; transition: background 0.15s;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.${PREFIX}-turn-item:hover { background: rgba(88,166,255,0.08); color: #c8c8d8; }
.${PREFIX}-turn-item .turn-role {
font-size: 9px; font-weight: 600; min-width: 14px; text-align: center;
}
.${PREFIX}-turn-item .turn-role.human { color: #58a6ff; }
.${PREFIX}-turn-item .turn-role.ai { color: #bc8cff; }
.${PREFIX}-turn-item .turn-preview { overflow: hidden; text-overflow: ellipsis; flex: 1; }
.${PREFIX}-turn-fork {
border: 0; background: transparent; color: #555; cursor: pointer;
font-size: 10px; padding: 0 2px; border-radius: 3px;
}
.${PREFIX}-turn-fork:hover { color: #58a6ff; background: rgba(88,166,255,0.08); }
`);
},
_getHTML() {
return `
CUE v${VERSION}
ResponseIdle
Last--
Cost$0.000
Usage Plan
Loading usage...
Context0%
0 tok
0 turns
0/min
Loading features...
${this._makeToggle('themeEnabled', 'Theme')}
${this._makeToggle('fontOverride', 'Sans Fonts')}
${this._makeToggle('wideMode', 'Wide')}
${this._makeToggle('coloredButtons', 'Btn Colors')}
${this._makeToggle('coloredBoldItalic', 'Bold/Ital')}
${this._makeToggle('smoothAnimations', 'Animate')}
${this._makeToggle('customScrollbar', 'Scrollbar')}
${this._makeToggle('pasteFix', 'Paste Fix')}
${this._makeToggle('autoScroll', 'Auto-Scroll')}
${this._makeToggle('autoApprove', 'Auto-Approve')}
${this._makeToggle('contextTracker', 'Ctx Track')}
${this._makeToggle('responseMonitor', 'Resp Mon')}
${this._makeToggle('notifySound', 'Sound')}
${this._makeToggle('notifyFlash', 'Tab Flash')}
${this._makeToggle('codeFold', 'Code Fold')}
${this._makeToggle('copyTurn', 'Copy Turn')}
${this._makeToggle('snippetTrigger', 'Snippets')}
${this._makeToggle('conversationSearch', 'Chat Search')}
${this._makeToggle('forkConversation', 'Fork')}
${this._makeToggle('voiceDictation', 'Voice')}
${this._makeToggle('focusMode', 'Focus')}
${this._makeToggle('domTrimmer', 'DOM Trim')}
Theme
Density
Width: ${Settings.get('chatWidthPct')}%
Keep: ${Settings.get('domKeepVisible')} msgs
Panel: ${Settings.get('panelWidth')}px
Conversation Search
Tools
Voice idle
Turn Navigator
Error Log
Clear
Export
Import
Reset All
`;
},
_makeToggle(key, label) {
const checked = Settings.get(key) ? 'checked' : '';
return `
${label}
`;
},
_bindEvents() {
// Close button
$(`#${PREFIX}-panel-close`).addEventListener('click', () => {
if (Settings.get('panelPinned')) Settings.set('panelPinned', false);
this.hide(true);
});
$(`#${PREFIX}-panel-pin`).addEventListener('click', () => Settings.toggle('panelPinned'));
this._bindPanelResize();
// Toggle switches
this._panel.querySelectorAll(`input[data-setting]`).forEach(cb => {
cb.addEventListener('change', () => Settings.set(cb.dataset.setting, cb.checked));
});
// Theme variant select
const themeSelect = $(`#${PREFIX}-set-themeVariant`);
themeSelect.value = Settings.get('themeVariant');
themeSelect.addEventListener('change', () => Settings.set('themeVariant', themeSelect.value));
// Density mode select
const densitySelect = $(`#${PREFIX}-set-densityMode`);
densitySelect.value = Settings.get('densityMode');
densitySelect.addEventListener('change', () => Settings.set('densityMode', densitySelect.value));
// Usage plan select
const usagePlanSelect = $(`#${PREFIX}-set-usagePlan`);
usagePlanSelect.value = Settings.get('usagePlan');
usagePlanSelect.addEventListener('change', () => {
Settings.set('usagePlan', usagePlanSelect.value);
this._renderUsage(this._usageData);
});
// Width slider
const widthSlider = $(`#${PREFIX}-set-chatWidthPct`);
widthSlider.addEventListener('input', () => {
$(`#${PREFIX}-width-val`).textContent = widthSlider.value;
Settings.set('chatWidthPct', parseInt(widthSlider.value));
});
// DOM trimmer slider
const trimSlider = $(`#${PREFIX}-set-domKeepVisible`);
trimSlider.addEventListener('input', () => {
$(`#${PREFIX}-trim-val`).textContent = trimSlider.value;
Settings.set('domKeepVisible', parseInt(trimSlider.value));
});
// Panel width slider
const panelWidthSlider = $(`#${PREFIX}-set-panelWidth`);
panelWidthSlider.addEventListener('input', () => {
const width = parseInt(panelWidthSlider.value);
$(`#${PREFIX}-panel-width-val`).textContent = width;
Settings.set('panelWidth', width);
});
// Reset settings
$(`#${PREFIX}-reset-settings`).addEventListener('click', () => {
Settings.reset();
showToast('Settings reset', 1200, 'success');
setTimeout(() => location.reload(), 600);
});
// Export config
$(`#${PREFIX}-export-config`).addEventListener('click', () => {
const config = {};
Object.keys(Settings._defaults).forEach(k => { config[k] = Settings.get(k); });
config._prompts = PromptModule.prompts.map(p => ({ id: p.id, label: p.label, prompt: p.prompt, cat: p.cat }));
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = 'cue-config-' + new Date().toISOString().slice(0, 10) + '.json';
document.body.appendChild(a); a.click(); document.body.removeChild(a);
URL.revokeObjectURL(url);
showToast('Config exported!', 2000, 'success');
});
// Import config
$(`#${PREFIX}-import-config`).addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file'; input.accept = '.json';
input.addEventListener('change', () => {
const file = input.files[0]; if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const config = JSON.parse(e.target.result);
let imported = 0;
Object.keys(Settings._defaults).forEach(k => {
if (k in config) { Settings.set(k, config[k]); imported++; }
});
if (config._prompts && Array.isArray(config._prompts)) {
config._prompts.forEach(p => {
const existing = PromptModule.prompts.find(x => x.id === p.id);
if (existing) { existing.label = p.label; existing.prompt = p.prompt; existing.cat = p.cat; }
else { PromptModule.prompts.push(p); }
});
PromptModule.save();
}
showToast(`Imported ${imported} settings`, 2500, 'success');
setTimeout(() => location.reload(), 1000);
} catch (err) { showToast('Invalid config file: ' + err.message, 3000, 'error'); }
};
reader.readAsText(file);
});
input.click();
});
// Add prompt button
$(`#${PREFIX}-prompt-add`).addEventListener('click', () => this._showPromptEditor());
// Conversation search
const searchInput = $(`#${PREFIX}-search-query`);
searchInput.addEventListener('input', () => this._renderConversationSearch(searchInput.value));
$(`#${PREFIX}-search-refresh`).addEventListener('click', async () => {
await ConversationSearchModule.refresh();
this._renderConversationSearch(searchInput.value);
});
// Tools
$(`#${PREFIX}-voice-toggle`).addEventListener('click', () => VoiceDictationModule.toggle());
$(`#${PREFIX}-fork-latest`).addEventListener('click', () => ForkConversationModule.forkAt(null));
$(`#${PREFIX}-copy-last-code`).addEventListener('click', () => PanelToolsModule._copyLastCode());
$(`#${PREFIX}-copy-last-response`).addEventListener('click', () => PanelToolsModule._copyLastResponse());
$(`#${PREFIX}-export-chat`).addEventListener('click', () => PanelToolsModule._showExportMenu());
$(`#${PREFIX}-turn-prev`).addEventListener('click', () => this._jumpTurn(-1));
$(`#${PREFIX}-turn-next`).addEventListener('click', () => this._jumpTurn(1));
// Clear error log
$(`#${PREFIX}-clear-errors`).addEventListener('click', () => {
ErrorLogModule.clear();
this._renderErrorLog();
});
// Listen for updates
EventBus.on('response:status', (s) => this._updateStatus(s));
EventBus.on('response:complete', (d) => this._updateLastResponse(d));
EventBus.on('context:updated', (h) => { this._updateContext(h); this._updateTurnNav(); });
EventBus.on('stream:messageLimit', (ml) => this._updateUsageFromStream(ml));
EventBus.on('usage:localUpdated', () => this._renderUsage(this._usageData));
EventBus.on('errorlog:updated', () => this._renderErrorLog());
EventBus.on('cost:updated', (d) => this._updateCost(d));
EventBus.on('cache:hit', () => this._updateCache('hit'));
EventBus.on('cache:miss', () => this._updateCache('miss'));
EventBus.on('cache:timer', (d) => this._updateCacheTimer(d));
EventBus.on('cache:expired', () => this._updateCache('expired'));
EventBus.on('conversationSearch:updated', () => this._renderConversationSearch(searchInput.value));
EventBus.on('conversationSearch:loading', (loading) => this._setSearchLoading(loading));
EventBus.on('voice:status', (status) => this._updateVoiceStatus(status));
EventBus.on('setting:panelPinned', () => this._applyPanelChrome());
EventBus.on('setting:panelWidth', () => this._applyPanelChrome());
this._renderPrompts();
this._renderUsage(this._usageData);
this._renderConversationSearch('');
this._renderErrorLog();
this._updateTurnNav();
},
_applyPanelChrome() {
if (!this._panel) return;
const width = Math.max(280, Math.min(640, parseInt(Settings.get('panelWidth')) || 320));
this._panel.style.setProperty('--cue-panel-width', width + 'px');
const widthLabel = $(`#${PREFIX}-panel-width-val`);
if (widthLabel) widthLabel.textContent = width;
const widthSlider = $(`#${PREFIX}-set-panelWidth`);
if (widthSlider) widthSlider.value = width;
const pinned = Settings.get('panelPinned');
this._panel.classList.toggle(PREFIX + '-panel-pinned', pinned);
const pin = $(`#${PREFIX}-panel-pin`);
if (pin) {
pin.classList.toggle('active', pinned);
pin.textContent = pinned ? 'U' : 'P';
pin.title = pinned ? 'Unpin panel' : 'Pin panel';
}
if (pinned) this.show();
},
_bindPanelResize() {
const handle = $(`#${PREFIX}-panel-resize`);
if (!handle) return;
let dragging = false;
let nextWidth = Settings.get('panelWidth');
const onMove = (e) => {
if (!dragging) return;
nextWidth = Math.max(280, Math.min(640, window.innerWidth - e.clientX));
this._panel.style.setProperty('--cue-panel-width', nextWidth + 'px');
const label = $(`#${PREFIX}-panel-width-val`);
if (label) label.textContent = nextWidth;
const slider = $(`#${PREFIX}-set-panelWidth`);
if (slider) slider.value = nextWidth;
};
const onUp = () => {
if (!dragging) return;
dragging = false;
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
Settings.set('panelWidth', Math.round(nextWidth));
};
handle.addEventListener('mousedown', (e) => {
dragging = true;
nextWidth = Settings.get('panelWidth');
e.preventDefault();
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
},
async _loadData() {
// Load usage from API
const usage = await ClaudeAPI.getUsage();
if (usage) {
this._usageData = usage;
this._renderUsage(usage);
}
// Load Claude features
const settings = await ClaudeAPI.getSettings();
if (settings) {
this._claudeSettings = settings;
this._renderFeatures(settings);
}
},
_renderUsage(data) {
const container = $(`#${PREFIX}-usage-content`);
if (!container) return;
if (!Settings.get('usageMonitor')) {
setHTML(container, 'Usage monitor disabled');
return;
}
const plan = this.USAGE_PLANS[Settings.get('usagePlan')] || this.USAGE_PLANS.pro;
const local = UsageTrackerModule.getStats();
let html = `
${plan.name} thresholds${local.totalCached} cached sends
`;
const addBar = (label, info, source = 'server') => {
if (!info) return;
let util = info.utilization ?? info.used_percentage ?? info.percent ?? 0;
if (typeof util === 'string') util = parseFloat(util);
const pct = Math.max(0, Math.min(100, Math.round(util <= 1 ? util * 100 : util)));
const cls = pct > 80 ? 'danger' : pct > 60 ? 'warn' : '';
const reset = info.resets_at ? this._fmtReset(info.resets_at) : '';
html += `
${label}${pct}%${reset ? ' ' + reset : ''}
${source !== 'server' ? `${source}` : ''}
`;
};
const localPct = (count, key) => {
const limit = (this.LOCAL_USAGE_BASELINE[key] || 1) * plan.multiplier;
return Math.min(1, count / limit);
};
addBar('Tab Session', { utilization: localPct(local.session, 'session') }, `${local.session} sends this tab`);
if (data) {
addBar('5h Window', data.five_hour || data.fiveHour || data['5h']);
addBar('7d Window', data.seven_day || data.sevenDay || data['7d']);
addBar('Opus Weekly', data.seven_day_opus || data.opus || data.opus_weekly);
} else {
addBar('5h Window', { utilization: localPct(local.fiveHour, 'fiveHour') }, `${local.fiveHour} local sends`);
addBar('7d Window', { utilization: localPct(local.sevenDay, 'sevenDay') }, `${local.sevenDay} local sends`);
}
setHTML(container, html);
},
_updateUsageFromStream(ml) {
const container = $(`#${PREFIX}-usage-content`);
if (!container) return;
const windows = ml.windows;
if (!windows) return;
const plan = this.USAGE_PLANS[Settings.get('usagePlan')] || this.USAGE_PLANS.pro;
const local = UsageTrackerModule.getStats();
let html = `
${plan.name} thresholds${local.totalCached} cached sends
`;
const addBar = (label, info) => {
if (!info) return;
const pct = Math.round((info.utilization || 0) * 100);
const cls = pct > 80 ? 'danger' : pct > 60 ? 'warn' : '';
const reset = info.resets_at ? this._fmtResetUnix(info.resets_at) : '';
html += `
${label}${pct}%${reset ? ' ' + reset : ''}
`;
};
const localLimit = this.LOCAL_USAGE_BASELINE.session * plan.multiplier;
const localPct = Math.min(100, Math.round((local.session / localLimit) * 100));
const localCls = localPct > 80 ? 'danger' : localPct > 60 ? 'warn' : '';
html += `
Tab Session${local.session} sends
`;
addBar('5h Window', windows['5h'] || windows.five_hour);
addBar('7d Window', windows['7d'] || windows.seven_day);
if (html) setHTML(container, html);
const session = windows['5h'] || windows.five_hour;
if (session) this._updateGearBadge(Math.round((session.utilization || 0) * 100));
},
_renderFeatures(settings) {
const container = $(`#${PREFIX}-features-content`);
if (!container) return;
setHTML(container, this.FEATURES.map(f => {
const on = settings[f.key] === true;
return `
${f.name}
`;
}).join(''));
container.querySelectorAll(`.${PREFIX}-feat-btn`).forEach(btn => {
btn.addEventListener('click', async () => {
const key = btn.dataset.feat;
const isOn = btn.classList.contains('on');
btn.textContent = '...';
btn.disabled = true;
const excl = btn.dataset.exclusive || null;
const result = await ClaudeAPI.toggleFeature(key, !isOn, excl);
if (result) {
btn.classList.toggle('on', !isOn);
btn.classList.toggle('off', isOn);
btn.textContent = !isOn ? 'ON' : 'OFF';
// Update exclusive partner
if (excl && !isOn) {
const partnerBtn = container.querySelector(`[data-feat="${excl}"]`);
if (partnerBtn) {
partnerBtn.classList.remove('on'); partnerBtn.classList.add('off');
partnerBtn.textContent = 'OFF';
}
}
} else {
btn.textContent = isOn ? 'ON' : 'OFF';
}
btn.disabled = false;
});
});
},
_renderConversationSearch(query = '') {
const meta = $(`#${PREFIX}-search-meta`);
const resultsEl = $(`#${PREFIX}-search-results`);
if (!meta || !resultsEl) return;
if (!Settings.get('conversationSearch')) {
meta.textContent = 'Search disabled';
setHTML(resultsEl, '');
return;
}
const results = ConversationSearchModule.search(query, 8);
const indexed = ConversationSearchModule.lastIndexed
? new Date(ConversationSearchModule.lastIndexed).toLocaleString('en', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })
: 'never';
meta.textContent = `${ConversationSearchModule.items.length} cached - indexed ${indexed}`;
if (!results.length) {
setHTML(resultsEl, `${query ? 'No matches' : 'No cached conversations'}`);
return;
}
setHTML(resultsEl, results.map(item => {
const date = item.updated ? new Date(item.updated).toLocaleDateString('en', { month: 'short', day: 'numeric' }) : '';
return `
${esc(item.title)}
${esc(date)}
`;
}).join(''));
resultsEl.querySelectorAll('.' + PREFIX + '-search-open').forEach(btn => {
btn.addEventListener('click', () => ConversationSearchModule.open(btn.dataset.chatId));
});
},
_setSearchLoading(loading) {
const btn = $(`#${PREFIX}-search-refresh`);
if (!btn) return;
btn.disabled = !!loading;
btn.textContent = loading ? 'Indexing' : 'Index';
},
_updateVoiceStatus(status) {
const el = $(`#${PREFIX}-voice-status`);
const btn = $(`#${PREFIX}-voice-toggle`);
if (!el || !btn) return;
if (status === 'listening') {
el.textContent = 'Voice listening';
el.style.color = '#3fb950';
btn.classList.add('success');
} else if (status === 'error') {
el.textContent = 'Voice error';
el.style.color = '#f85149';
btn.classList.remove('success');
} else {
el.textContent = VoiceDictationModule.isSupported() ? 'Voice idle' : 'Voice unavailable';
el.style.color = '#555';
btn.classList.remove('success');
}
},
_renderPrompts() {
const container = $(`#${PREFIX}-prompt-list`);
if (!container) return;
setHTML(container, '');
PromptModule.CATEGORIES.forEach(cat => {
const prompts = PromptModule.prompts.filter(p => p.cat === cat.id && p.prompt);
if (prompts.length === 0) return;
const catEl = document.createElement('div');
catEl.className = PREFIX + '-prompt-cat';
catEl.style.color = cat.color;
catEl.textContent = cat.label;
container.appendChild(catEl);
prompts.forEach(p => {
const btn = document.createElement('button');
btn.className = PREFIX + '-prompt-btn';
setHTML(btn, `${esc(p.label)}`);
btn.title = p.prompt.substring(0, 100) + '...';
btn.addEventListener('click', () => { PromptModule.send(p.prompt); this.toggle(); });
btn.addEventListener('contextmenu', (e) => { e.preventDefault(); this._showPromptEditor(p); });
container.appendChild(btn);
});
});
},
_showPromptEditor(existing = null) {
const overlay = document.createElement('div');
overlay.className = PREFIX + '-modal-overlay';
const modal = document.createElement('div');
modal.className = PREFIX + '-modal';
const history = existing ? PromptModule.getHistory(existing.id) : [];
const historyHTML = history.length > 0 ? `
Version history (${history.length})
${history.map((h, i) => {
const date = new Date(h.time).toLocaleDateString('en', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
const preview = (h.prompt || '').substring(0, 50).replace(/\n/g, ' ');
return `
`;
}).join('')}
` : '';
setHTML(modal, `
${existing ? 'Edit' : 'Add'} Prompt
${historyHTML}
${existing && !existing.id.startsWith('spec') && !existing.id.startsWith('arch') ? `` : ''}
`);
overlay.appendChild(modal);
document.body.appendChild(overlay);
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
$(`#${PREFIX}-pe-cancel`, modal).addEventListener('click', () => overlay.remove());
$(`#${PREFIX}-pe-save`, modal).addEventListener('click', () => {
const label = $(`#${PREFIX}-pe-label`, modal).value.trim();
const text = $(`#${PREFIX}-pe-text`, modal).value;
const cat = $(`#${PREFIX}-pe-cat`, modal).value;
if (!label) { showToast('Label is required', 2000, 'warn'); return; }
if (existing) { PromptModule.update(existing.id, label, text); existing.cat = cat; PromptModule.save(); }
else { PromptModule.add(label, text, cat); }
this._renderPrompts();
overlay.remove();
showToast(`Prompt "${label}" saved!`, 2000, 'success');
});
const delBtn = $(`#${PREFIX}-pe-delete`, modal);
if (delBtn) delBtn.addEventListener('click', () => {
PromptModule.remove(existing.id);
this._renderPrompts();
overlay.remove();
showToast('Prompt deleted', 2000, 'info');
});
// Rollback buttons
modal.querySelectorAll('.' + PREFIX + '-rollback-btn').forEach(btn => {
btn.addEventListener('click', () => {
const idx = parseInt(btn.dataset.idx);
if (existing && PromptModule.rollback(existing.id, idx)) {
this._renderPrompts();
overlay.remove();
showToast('Prompt restored from history', 2000, 'success');
}
});
});
},
_updateStatus(status) {
const el = $(`#${PREFIX}-status`);
if (!el) return;
const labels = {
idle: ['idle', 'Idle'],
generating: ['generating', 'Generating...'],
complete: ['complete', 'Complete'],
stuck: ['stuck', 'Stuck!'],
truncated: ['truncated', 'Truncated']
};
const [cls, txt] = labels[status] || labels.idle;
setHTML(el, `${txt}`);
},
_updateLastResponse(d) {
const el = $(`#${PREFIX}-last-resp`);
if (el) el.textContent = `${fmtDur(d.duration)} / ${fmtNum(d.words)} words`;
},
_updateContext(h) {
const pctEl = $(`#${PREFIX}-ctx-pct`);
const barEl = $(`#${PREFIX}-ctx-bar`);
const tokEl = $(`#${PREFIX}-ctx-tokens`);
const turnEl = $(`#${PREFIX}-ctx-turns`);
const burnEl = $(`#${PREFIX}-ctx-burn`);
const advEl = $(`#${PREFIX}-ctx-advice`);
if (!pctEl) return;
const pct = Math.round(h.fill * 100);
pctEl.textContent = pct + '%';
pctEl.style.color = { good: '#3fb950', warn: '#d29922', critical: '#f85149' }[h.level];
barEl.style.width = pct + '%';
barEl.className = `${PREFIX}-ctx-fill ${h.level}`;
tokEl.textContent = fmtNum(h.tokens);
turnEl.textContent = h.turns;
burnEl.textContent = fmtNum(h.burnRate);
advEl.textContent = h.advice;
advEl.style.color = { good: '#3fb950', warn: '#d29922', critical: '#f85149' }[h.level];
},
_updateCache(status) {
const el = $(`#${PREFIX}-cache-indicator`);
if (!el) return;
if (status === 'hit') {
el.style.color = '#3fb950';
el.textContent = 'Cache: HIT';
} else if (status === 'miss') {
el.style.color = '#555';
el.textContent = 'Cache: miss';
} else if (status === 'expired') {
el.style.color = '#d29922';
el.textContent = 'Cache: expired';
}
},
_updateCacheTimer(d) {
const el = $(`#${PREFIX}-cache-indicator`);
if (!el) return;
const mins = Math.floor(d.remaining / 60);
const secs = Math.floor(d.remaining % 60);
el.style.color = d.remaining < 60 ? '#d29922' : '#3fb950';
el.textContent = `Cache: HIT (${mins}:${String(secs).padStart(2, '0')} left)`;
},
_updateCost(d) {
const el = $(`#${PREFIX}-cost-display`);
if (!el) return;
const total = d.sessionCost;
const fmt = total < 0.01 ? '$' + total.toFixed(4) : '$' + total.toFixed(3);
el.textContent = fmt + ' (' + d.model + ')';
},
_renderErrorLog() {
const el = $(`#${PREFIX}-error-log`);
if (el) setHTML(el, ErrorLogModule.getHTML());
},
_updateTurnNav() {
const container = $(`#${PREFIX}-turn-nav`);
if (!container) return;
const main = document.querySelector('main');
if (!main) { setHTML(container, 'No conversation'); return; }
const groups = $$(SEL.msgGroup, main);
const messages = getConversationMessages();
if (groups.length === 0 || messages.length === 0) { setHTML(container, 'No messages'); return; }
let html = '';
messages.forEach((m) => {
const preview = m.text.substring(0, 40).replace(/\n/g, ' ');
const role = m.role === 'human' ? 'H' : 'A';
const roleClass = m.role === 'human' ? 'human' : 'ai';
html += `` +
`${role}` +
`${esc(preview || '...')}` +
``;
});
setHTML(container, html);
container.querySelectorAll('.' + PREFIX + '-turn-item').forEach(item => {
item.addEventListener('click', () => {
const idx = parseInt(item.dataset.turnIdx);
const target = groups[idx];
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
});
container.querySelectorAll('.' + PREFIX + '-turn-fork').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
ForkConversationModule.forkAt(parseInt(btn.dataset.forkIdx));
});
});
},
_jumpTurn(delta) {
const groups = $$(SEL.msgGroup, document.querySelector('main') || document);
if (!groups.length) return;
this._turnCursor = Math.max(0, Math.min(groups.length - 1, this._turnCursor + delta));
groups[this._turnCursor].scrollIntoView({ behavior: 'smooth', block: 'start' });
},
_updateGearBadge(pct) {
// Update hover strip indicator color based on usage
const strip = $(`#${PREFIX}-hover-strip`);
if (!strip) return;
const color = pct > 80 ? 'rgba(248,81,73,0.6)' : pct > 60 ? 'rgba(210,153,34,0.5)' : 'rgba(88,166,255,0.2)';
strip.style.setProperty('--strip-color', color);
},
_fmtReset(iso) {
if (!iso) return '';
const d = new Date(iso), diff = d - new Date(), m = Math.floor(diff / 60000);
if (m < 1) return 'soon';
if (m < 60) return `in ${m}m`;
const h = Math.floor(m / 60);
return h < 24 ? `in ${h}h` : `in ${Math.floor(h / 24)}d`;
},
_fmtResetUnix(ts) {
if (!ts) return '';
return this._fmtReset(new Date(ts * 1000).toISOString());
},
buildGearButton() {
if ($(`#${PREFIX}-hover-strip`)) return;
const strip = document.createElement('div');
strip.id = PREFIX + '-hover-strip';
document.body.appendChild(strip);
let hideTimeout = null;
const showPanel = () => {
clearTimeout(hideTimeout);
this.show();
};
const scheduleHide = () => {
if (Settings.get('panelPinned')) return;
clearTimeout(hideTimeout);
hideTimeout = setTimeout(() => {
if (Settings.get('panelPinned')) return;
if (this._panel && !this._panel.matches(':hover') && !strip.matches(':hover')) {
this.hide();
}
}, 400);
};
strip.addEventListener('mouseenter', showPanel);
strip.addEventListener('mouseleave', scheduleHide);
if (this._panel) {
this._panel.addEventListener('mouseenter', () => clearTimeout(hideTimeout));
this._panel.addEventListener('mouseleave', scheduleHide);
}
},
destroy() { clearInterval(this._refreshTimer); }
};
// =====================================================================
// INITIALIZATION
// =====================================================================
const ALL_MODULES = [
ThemeModule, FocusModule, DensityModule, LayoutModule, VisualModule, PasteFixModule,
AutoScrollModule, AutoApproveModule, UsageTrackerModule, ContextModule, CacheModule, CostModule,
ResponseModule, DomTrimmerModule, CodeFoldModule, CopyTurnModule,
ErrorLogModule, RetitleModule, ConversationSearchModule, VoiceDictationModule,
ForkConversationModule, SnippetModule, PromptModule, PanelToolsModule
];
// Safe module initializer — isolates each module so one failure
// doesn't take down the rest (graceful degradation).
function safeInit(mod) {
try {
mod.init();
} catch (e) {
console.error(LOG_TAG, `Module "${mod.id}" failed to init:`, e);
EventBus.emit('module:error', { module: mod.id, error: e });
}
}
async function init() {
console.log(`%c${LOG_TAG} Claude Ultimate Enhancer v${VERSION} initializing...`, 'color:#58a6ff;font-weight:bold;font-size:14px');
// Install fetch interceptor early (before any fetches)
StreamMonitor.install();
// Error log must init first to capture other module errors
safeInit(ErrorLogModule);
// Init modules that work at document-start
safeInit(ThemeModule);
safeInit(FocusModule);
safeInit(DensityModule);
safeInit(LayoutModule);
safeInit(PasteFixModule);
safeInit(UsageTrackerModule);
// Wait for body to be available
const waitBody = () => new Promise(r => {
if (document.body) return r();
const obs = new MutationObserver(() => { if (document.body) { obs.disconnect(); r(); } });
obs.observe(document.documentElement, { childList: true });
});
await waitBody();
// Init remaining modules with graceful degradation
safeInit(VisualModule);
safeInit(AutoScrollModule);
safeInit(AutoApproveModule);
safeInit(ContextModule);
safeInit(CacheModule);
safeInit(CostModule);
safeInit(ResponseModule);
safeInit(DomTrimmerModule);
safeInit(CodeFoldModule);
safeInit(CopyTurnModule);
safeInit(RetitleModule);
safeInit(ConversationSearchModule);
safeInit(VoiceDictationModule);
safeInit(ForkConversationModule);
safeInit(SnippetModule);
safeInit(PromptModule);
safeInit(PanelToolsModule);
// Build control panel
ControlPanel.build();
ControlPanel.buildGearButton();
// URL change detection for SPA navigation
let lastUrl = location.href;
setInterval(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
ContextModule.data = { turns: 0, userMsgs: 0, assistantMsgs: 0, estimatedTokens: 0, chatStartTime: null, sseUtilization: null, history: [] };
ResponseModule.status = 'idle';
ResponseModule.genStartTime = null;
ResponseModule.lastDuration = 0;
ResponseModule.lastWords = 0;
ResponseModule.lastChars = 0;
ResponseModule._stopTimer();
EventBus.emit('response:status', 'idle');
EventBus.emit('navigation', location.href);
}
}, 2000);
console.log(`%c${LOG_TAG} Ready! Hover right edge to open the panel`, 'color:#3fb950;font-weight:bold');
}
// Start
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();