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;
|
border-radius: 2px;
|
||||||
padding: 0 1px;
|
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);
|
||||||
|
}
|
||||||
|
|||||||
+197
-24
@@ -505,57 +505,192 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Response Observer (reveal mode)
|
// Response Reveal — swaps fake data back to real in the page
|
||||||
// ============================================================
|
// ============================================================
|
||||||
function observeResponses() {
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
if (!settings.revealMode || !hasSubstitutions()) return;
|
|
||||||
|
|
||||||
for (const mutation of mutations) {
|
// All response/output selectors across supported services
|
||||||
for (const node of mutation.addedNodes) {
|
|
||||||
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
|
||||||
|
|
||||||
// Response container selectors for all supported services
|
|
||||||
const RESPONSE_SELECTORS = [
|
const RESPONSE_SELECTORS = [
|
||||||
// Claude
|
// Claude
|
||||||
'[data-is-streaming]', '.font-claude-message',
|
'[data-is-streaming]', '.font-claude-message',
|
||||||
|
// Claude artifacts / code
|
||||||
|
'.code-block__code', '.artifact-content', 'pre code', 'pre',
|
||||||
// ChatGPT
|
// ChatGPT
|
||||||
'[data-message-author-role="assistant"]', '.markdown',
|
'[data-message-author-role="assistant"]', '.markdown',
|
||||||
// Grok
|
// Grok
|
||||||
'[class*="message-bubble"]', '[class*="response"]',
|
'[class*="message-bubble"]', '[class*="response"]',
|
||||||
// Gemini
|
// Gemini
|
||||||
'.model-response-text', '.response-content', 'message-content',
|
'.model-response-text', '.response-content', 'message-content',
|
||||||
// Generic
|
// Generic / OpenWebUI
|
||||||
'.prose', '[class*="Message"]', '[class*="assistant"]',
|
'.prose', '[class*="Message"]', '[class*="assistant"]',
|
||||||
|
// Code and artifacts everywhere
|
||||||
|
'code', '.hljs', '.highlight',
|
||||||
].join(', ');
|
].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;
|
||||||
|
|
||||||
const responseEls = node.querySelectorAll
|
const responseEls = node.querySelectorAll
|
||||||
? node.querySelectorAll(RESPONSE_SELECTORS)
|
? node.querySelectorAll(RESPONSE_SELECTORS)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
for (const el of responseEls) revealInElement(el);
|
for (const el of responseEls) revealInElement(el);
|
||||||
|
|
||||||
if (node.matches?.(RESPONSE_SELECTORS)) {
|
if (node.matches?.(RESPONSE_SELECTORS)) {
|
||||||
revealInElement(node);
|
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) {
|
// React to reveal mode toggle
|
||||||
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
let prevRevealMode = settings.revealMode;
|
||||||
let textNode;
|
function checkRevealToggle() {
|
||||||
while ((textNode = walker.nextNode())) {
|
if (settings.revealMode && !prevRevealMode) {
|
||||||
const original = textNode.textContent;
|
// Just turned ON — reveal everything existing
|
||||||
const revealed = reveal(original, mappings);
|
revealAllResponses();
|
||||||
if (revealed !== original) {
|
} else if (!settings.revealMode && prevRevealMode) {
|
||||||
textNode.textContent = revealed;
|
// 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
|
// Shadow DOM Traversal
|
||||||
@@ -590,15 +725,39 @@
|
|||||||
}
|
}
|
||||||
}, true);
|
}, 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
|
// Boot
|
||||||
// ============================================================
|
// ============================================================
|
||||||
if (document.body) {
|
function boot() {
|
||||||
observeResponses();
|
observeResponses();
|
||||||
} else {
|
|
||||||
document.addEventListener('DOMContentLoaded', observeResponses);
|
|
||||||
}
|
|
||||||
traverseShadowRoots();
|
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 +
|
const smartCount = (identity.emails || []).length +
|
||||||
(identity.names || []).length +
|
(identity.names || []).length +
|
||||||
@@ -608,4 +767,18 @@
|
|||||||
console.log(
|
console.log(
|
||||||
`[Silent Send] Active on ${location.hostname} — ${mappings.length} explicit mapping(s), ${smartCount} smart pattern(s)`
|
`[Silent Send] Active on ${location.hostname} — ${mappings.length} explicit mapping(s), ${smartCount} smart pattern(s)`
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.body) {
|
||||||
|
boot();
|
||||||
|
} else {
|
||||||
|
document.addEventListener('DOMContentLoaded', boot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also update badge when settings change
|
||||||
|
const origCheckReveal = checkRevealToggle;
|
||||||
|
checkRevealToggle = function () {
|
||||||
|
origCheckReveal();
|
||||||
|
updateRevealBadge();
|
||||||
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -526,6 +526,14 @@ body {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.privacy-note {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #9ca3af;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.footer a {
|
.footer a {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
|
|||||||
@@ -172,6 +172,11 @@
|
|||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
|
<div class="privacy-note">
|
||||||
|
Your data never leaves your browser. No servers, no tracking, no analytics.
|
||||||
|
Identity data is stored in local browser storage only — as secure as your
|
||||||
|
browser's saved passwords. Nothing is transmitted anywhere.
|
||||||
|
</div>
|
||||||
<a href="#" id="btnOptions">Options</a>
|
<a href="#" id="btnOptions">Options</a>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user