feat: underline revealed text in AI responses

Revealed values are now wrapped in <span class="ss-revealed">
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
This commit is contained in:
Claude
2026-03-26 04:16:44 +00:00
parent 684413f5fe
commit 7271027c02
2 changed files with 85 additions and 9 deletions
+3 -2
View File
@@ -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 */
+82 -7
View File
@@ -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)