feat: proper in-page reveal mode + privacy note
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
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
+210
-37
@@ -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();
|
||||
};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user