From 045ee4848b97d5c15894ff863d8f51f29f9c7878 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 26 Mar 2026 00:51:04 +0000 Subject: [PATCH] feat: proper in-page reveal mode + privacy note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reveal mode (eye icon) now works as intended: - Toggle ON: all existing responses on the page get fake→real substitution applied immediately (paths, names, emails, etc.) - Streaming responses are revealed in real-time as they arrive - Toggle OFF: original text is restored from saved state - Covers code blocks, artifacts, pre tags, and all response containers across all supported services - Blue floating badge shows "Reveal Mode — showing real data" when active so user knows what they're seeing The workflow is now: type /home/jsmith/... → Claude sees /home/ademo/... → Claude responds with /home/ademo/... → reveal mode shows /home/jsmith/... → user copies real path. Also adds privacy note in popup footer: data stays in local browser storage, no servers, no tracking, no analytics. https://claude.ai/code/session_01Dvgwe7XMoSxnWXkih8p1Cw --- src/content/content.css | 25 ++++ src/content/content.js | 247 ++++++++++++++++++++++++++++++++++------ src/popup/popup.css | 8 ++ src/popup/popup.html | 5 + 4 files changed, 248 insertions(+), 37 deletions(-) diff --git a/src/content/content.css b/src/content/content.css index c07ebfc..1d2da16 100644 --- a/src/content/content.css +++ b/src/content/content.css @@ -20,3 +20,28 @@ border-radius: 2px; padding: 0 1px; } + +/* Floating reveal mode indicator */ +.ss-reveal-badge { + position: fixed; + bottom: 16px; + right: 16px; + background: #1d4ed8; + color: #fff; + padding: 6px 14px; + border-radius: 20px; + font-size: 12px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-weight: 500; + z-index: 999999; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + pointer-events: none; + opacity: 0; + transform: translateY(10px); + transition: opacity 0.2s, transform 0.2s; +} + +.ss-reveal-badge.visible { + opacity: 1; + transform: translateY(0); +} diff --git a/src/content/content.js b/src/content/content.js index 1f9a1bb..f9561ad 100644 --- a/src/content/content.js +++ b/src/content/content.js @@ -505,58 +505,193 @@ }; // ============================================================ - // Response Observer (reveal mode) + // Response Reveal — swaps fake data back to real in the page // ============================================================ + + // All response/output selectors across supported services + const RESPONSE_SELECTORS = [ + // Claude + '[data-is-streaming]', '.font-claude-message', + // Claude artifacts / code + '.code-block__code', '.artifact-content', 'pre code', 'pre', + // ChatGPT + '[data-message-author-role="assistant"]', '.markdown', + // Grok + '[class*="message-bubble"]', '[class*="response"]', + // Gemini + '.model-response-text', '.response-content', 'message-content', + // Generic / OpenWebUI + '.prose', '[class*="Message"]', '[class*="assistant"]', + // Code and artifacts everywhere + 'code', '.hljs', '.highlight', + ].join(', '); + + // Build reverse mapping pairs from identity + explicit mappings + function buildRevealPairs() { + const pairs = []; + + // Explicit mappings (substitute → real) + for (const m of mappings) { + if (!m.enabled || !m.substitute || !m.real) continue; + pairs.push({ from: m.substitute, to: m.real, caseSensitive: m.caseSensitive }); + } + + // Smart identity pairs (substitute → real) + if (identity) { + for (const e of (identity.emails || [])) { + if (e.substitute && e.real) pairs.push({ from: e.substitute, to: e.real }); + } + if (identity.catchAllEmail) { + // Can't reverse a catch-all to a specific email, but we mark it + // so the user sees it was a substitution + } + for (const n of (identity.names || [])) { + if (n.substitute && n.real) pairs.push({ from: n.substitute, to: n.real }); + } + for (const u of (identity.usernames || [])) { + if (u.substitute && u.real) pairs.push({ from: u.substitute, to: u.real }); + } + for (const h of (identity.hostnames || [])) { + if (h.substitute && h.real) pairs.push({ from: h.substitute, to: h.real }); + } + for (const p of (identity.phones || [])) { + if (p.substitute && p.real) pairs.push({ from: p.substitute, to: p.real }); + } + } + + // Sort longer matches first + pairs.sort((a, b) => b.from.length - a.from.length); + return pairs; + } + + function revealText(text) { + const pairs = buildRevealPairs(); + let result = text; + for (const p of pairs) { + const escaped = esc(p.from); + const regex = new RegExp(escaped, p.caseSensitive ? 'g' : 'gi'); + result = result.replace(regex, p.to); + } + return result; + } + + // Store originals so we can un-reveal when toggled off + const originalTexts = new WeakMap(); + + function revealInElement(el) { + const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT); + let textNode; + while ((textNode = walker.nextNode())) { + const text = textNode.textContent; + if (!text || text.trim().length === 0) continue; + + // Save original if not already saved + if (!originalTexts.has(textNode)) { + originalTexts.set(textNode, text); + } + + const revealed = revealText(text); + if (revealed !== text) { + textNode.textContent = revealed; + } + } + } + + function unrevealInElement(el) { + const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT); + let textNode; + while ((textNode = walker.nextNode())) { + const original = originalTexts.get(textNode); + if (original && textNode.textContent !== original) { + textNode.textContent = original; + } + } + } + + // Reveal ALL existing responses on the page + function revealAllResponses() { + const elements = document.querySelectorAll(RESPONSE_SELECTORS); + for (const el of elements) { + revealInElement(el); + } + } + + // Un-reveal ALL responses (restore originals) + function unrevealAllResponses() { + const elements = document.querySelectorAll(RESPONSE_SELECTORS); + for (const el of elements) { + unrevealInElement(el); + } + } + + // Watch for new content (streaming responses, new messages) function observeResponses() { const observer = new MutationObserver((mutations) => { if (!settings.revealMode || !hasSubstitutions()) return; for (const mutation of mutations) { + // Handle new nodes for (const node of mutation.addedNodes) { if (node.nodeType !== Node.ELEMENT_NODE) continue; - // Response container selectors for all supported services - const RESPONSE_SELECTORS = [ - // Claude - '[data-is-streaming]', '.font-claude-message', - // ChatGPT - '[data-message-author-role="assistant"]', '.markdown', - // Grok - '[class*="message-bubble"]', '[class*="response"]', - // Gemini - '.model-response-text', '.response-content', 'message-content', - // Generic - '.prose', '[class*="Message"]', '[class*="assistant"]', - ].join(', '); - const responseEls = node.querySelectorAll ? node.querySelectorAll(RESPONSE_SELECTORS) : []; - for (const el of responseEls) revealInElement(el); if (node.matches?.(RESPONSE_SELECTORS)) { revealInElement(node); } } + + // Handle text changes in existing nodes (streaming) + if (mutation.type === 'characterData' && settings.revealMode) { + const parentEl = mutation.target.parentElement; + if (parentEl?.closest?.(RESPONSE_SELECTORS)) { + const text = mutation.target.textContent; + const revealed = revealText(text); + if (revealed !== text) { + // Save original before overwriting + if (!originalTexts.has(mutation.target)) { + originalTexts.set(mutation.target, text); + } + mutation.target.textContent = revealed; + } + } + } } }); - observer.observe(document.body, { childList: true, subtree: true }); + observer.observe(document.body, { + childList: true, + subtree: true, + characterData: true, + }); } - function revealInElement(el) { - const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT); - let textNode; - while ((textNode = walker.nextNode())) { - const original = textNode.textContent; - const revealed = reveal(original, mappings); - if (revealed !== original) { - textNode.textContent = revealed; - } + // React to reveal mode toggle + let prevRevealMode = settings.revealMode; + function checkRevealToggle() { + if (settings.revealMode && !prevRevealMode) { + // Just turned ON — reveal everything existing + revealAllResponses(); + } else if (!settings.revealMode && prevRevealMode) { + // Just turned OFF — restore originals + unrevealAllResponses(); } + prevRevealMode = settings.revealMode; } + // Hook into config updates to detect reveal toggle + const origMessageHandler = window.addEventListener; + window.addEventListener('message', (event) => { + if (event.source !== window) return; + if (event.data?.type === 'ss:config-updated') { + // Settings were updated — check if reveal mode changed + setTimeout(checkRevealToggle, 50); + } + }); + // ============================================================ // Shadow DOM Traversal // ============================================================ @@ -590,22 +725,60 @@ } }, true); + // ============================================================ + // Reveal Mode Badge + // ============================================================ + let revealBadge = null; + + function ensureRevealBadge() { + if (revealBadge) return revealBadge; + revealBadge = document.createElement('div'); + revealBadge.className = 'ss-reveal-badge'; + revealBadge.textContent = 'Reveal Mode — showing real data'; + document.body.appendChild(revealBadge); + return revealBadge; + } + + function updateRevealBadge() { + const badge = ensureRevealBadge(); + badge.classList.toggle('visible', !!settings.revealMode); + } + // ============================================================ // Boot // ============================================================ - if (document.body) { + function boot() { observeResponses(); - } else { - document.addEventListener('DOMContentLoaded', observeResponses); + traverseShadowRoots(); + updateRevealBadge(); + + // If reveal mode was already on at page load, reveal everything + if (settings.revealMode && hasSubstitutions()) { + // Wait for page content to render + setTimeout(revealAllResponses, 1000); + setTimeout(revealAllResponses, 3000); + } + + const smartCount = (identity.emails || []).length + + (identity.names || []).length + + (identity.usernames || []).length + + (identity.phones || []).length; + + console.log( + `[Silent Send] Active on ${location.hostname} — ${mappings.length} explicit mapping(s), ${smartCount} smart pattern(s)` + ); } - traverseShadowRoots(); - const smartCount = (identity.emails || []).length + - (identity.names || []).length + - (identity.usernames || []).length + - (identity.phones || []).length; + if (document.body) { + boot(); + } else { + document.addEventListener('DOMContentLoaded', boot); + } - console.log( - `[Silent Send] Active on ${location.hostname} — ${mappings.length} explicit mapping(s), ${smartCount} smart pattern(s)` - ); + // Also update badge when settings change + const origCheckReveal = checkRevealToggle; + checkRevealToggle = function () { + origCheckReveal(); + updateRevealBadge(); + }; })(); diff --git a/src/popup/popup.css b/src/popup/popup.css index a0f5203..583838f 100644 --- a/src/popup/popup.css +++ b/src/popup/popup.css @@ -526,6 +526,14 @@ body { text-align: center; } +.privacy-note { + font-size: 10px; + color: #9ca3af; + line-height: 1.4; + margin-bottom: 6px; + padding: 0 4px; +} + .footer a { font-size: 11px; color: #6b7280; diff --git a/src/popup/popup.html b/src/popup/popup.html index 8444a10..2f8cfb7 100644 --- a/src/popup/popup.html +++ b/src/popup/popup.html @@ -172,6 +172,11 @@