From 7271027c0280ad8701c8b8f8f12a6ba852e05445 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 26 Mar 2026 04:16:44 +0000 Subject: [PATCH] feat: underline revealed text in AI responses Revealed values are now wrapped in with a blue underline and subtle blue background, making it obvious which parts are your real data vs what the AI said. Hover shows tooltip "Substituted value: [fake]" so you can see what the AI actually received. Un-reveal properly removes the spans and normalizes text nodes. https://claude.ai/code/session_01Dvgwe7XMoSxnWXkih8p1Cw --- src/content/content.css | 5 ++- src/content/content.js | 89 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 85 insertions(+), 9 deletions(-) diff --git a/src/content/content.css b/src/content/content.css index 1d2da16..b5db103 100644 --- a/src/content/content.css +++ b/src/content/content.css @@ -16,9 +16,10 @@ /* Revealed text styling (when reveal mode shows real values in responses) */ .ss-revealed { background: rgba(59, 130, 246, 0.1); - border-bottom: 1.5px dashed rgba(59, 130, 246, 0.5); + border-bottom: 2px solid rgba(59, 130, 246, 0.6); border-radius: 2px; - padding: 0 1px; + padding: 0 2px; + cursor: help; } /* Floating reveal mode indicator */ diff --git a/src/content/content.js b/src/content/content.js index 7c3e0ef..c8eedda 100644 --- a/src/content/content.js +++ b/src/content/content.js @@ -630,37 +630,109 @@ function revealInElement(el) { if (SKIP_REVEAL_TAGS.has(el.tagName)) return; - // Skip our own badge if (el.classList?.contains('ss-reveal-badge')) return; + if (el.classList?.contains('ss-revealed')) return; // skip our own spans const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, { acceptNode(node) { const parent = node.parentElement; if (parent && SKIP_REVEAL_TAGS.has(parent.tagName)) return NodeFilter.FILTER_REJECT; if (parent?.classList?.contains('ss-reveal-badge')) return NodeFilter.FILTER_REJECT; + if (parent?.classList?.contains('ss-revealed')) return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_ACCEPT; } }); + // Collect nodes first (modifying DOM during walk breaks the walker) + const nodes = []; let textNode; while ((textNode = walker.nextNode())) { - const text = textNode.textContent; + nodes.push(textNode); + } + + if (!_revealPairsCache) _revealPairsCache = buildRevealPairs(); + const pairs = _revealPairsCache; + + for (const node of nodes) { + const text = node.textContent; if (!text || text.length < MIN_STRING_LENGTH) continue; - if (!originalTexts.has(textNode)) { - originalTexts.set(textNode, text); + // Save original + if (!originalTexts.has(node)) { + originalTexts.set(node, text); } - const revealed = revealText(text); - if (revealed !== text) { - textNode.textContent = revealed; + // Find all substituted values and their positions + const segments = []; + let remaining = text; + let offset = 0; + + for (const p of pairs) { + const escaped = esc(p.from); + const regex = new RegExp(escaped, p.caseSensitive ? 'gi' : 'gi'); + let match; + while ((match = regex.exec(text)) !== null) { + segments.push({ + start: match.index, + end: match.index + match[0].length, + original: match[0], + revealed: p.to, + }); + } } + + if (segments.length === 0) continue; + + // Sort by position, deduplicate overlaps + segments.sort((a, b) => a.start - b.start); + const deduped = []; + let lastEnd = -1; + for (const s of segments) { + if (s.start >= lastEnd) { + deduped.push(s); + lastEnd = s.end; + } + } + + // Build fragment: text + highlighted spans + const frag = document.createDocumentFragment(); + let pos = 0; + + for (const s of deduped) { + // Text before this match + if (s.start > pos) { + frag.appendChild(document.createTextNode(text.slice(pos, s.start))); + } + // Highlighted revealed value + const span = document.createElement('span'); + span.className = 'ss-revealed'; + span.textContent = s.revealed; + span.title = 'Substituted value: ' + s.original; + frag.appendChild(span); + pos = s.end; + } + + // Remaining text after last match + if (pos < text.length) { + frag.appendChild(document.createTextNode(text.slice(pos))); + } + + // Replace the text node with the fragment + node.parentNode.replaceChild(frag, node); } } function unrevealInElement(el) { if (SKIP_REVEAL_TAGS.has(el.tagName)) return; + // Remove all ss-revealed spans, restoring original text + const spans = el.querySelectorAll('.ss-revealed'); + for (const span of spans) { + const text = document.createTextNode(span.title?.replace('Substituted value: ', '') || span.textContent); + span.parentNode.replaceChild(text, span); + } + + // Also restore any text nodes that were modified without spans const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT); let textNode; while ((textNode = walker.nextNode())) { @@ -669,6 +741,9 @@ textNode.textContent = original; } } + + // Normalize adjacent text nodes + el.normalize(); } // Elements to skip when revealing (inputs, scripts, styles, extension UI)