feat: add reveal paste-back tool + fix smart detection bail

Test tab now has two modes:
- Strip (real → fake): paste text with real data, see what gets sent
- Reveal (fake → real): paste AI output with fake data, get back
  real data with a "Copy to Clipboard" button

Also fixes:
- content.js smartSubstitute bailing when identity.enabled was
  undefined (defaulted enabled to all-true instead of returning)
- Test tab now reloads identity from storage on tab switch so
  changes saved in the Identity tab take effect immediately
- Shows yellow warning when identity fields are missing

https://claude.ai/code/session_01Dvgwe7XMoSxnWXkih8p1Cw
This commit is contained in:
Claude
2026-03-26 00:44:35 +00:00
parent 60b810b1b9
commit 7176cf6509
4 changed files with 227 additions and 37 deletions
+3 -1
View File
@@ -95,7 +95,9 @@
]);
function smartSubstitute(text, id) {
if (!id || !id.enabled) return { text, replacements: [] };
if (!id) return { text, replacements: [] };
// Default enabled to all-true if not set
if (!id.enabled) id.enabled = { emails: true, names: true, usernames: true, phones: true, paths: true };
const replacements = [];
let result = text;
+42
View File
@@ -408,6 +408,48 @@ body {
color: #059669;
}
/* Test tab mode toggle */
.test-mode-toggle {
display: flex;
background: #f3f4f6;
border-radius: 6px;
padding: 2px;
margin-bottom: 10px;
}
.test-mode-btn {
flex: 1;
padding: 6px 0;
border: none;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
cursor: pointer;
background: transparent;
color: #6b7280;
transition: all 0.15s;
}
.test-mode-btn.active {
background: #fff;
color: #111;
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
.test-identity-status {
margin-top: 8px;
padding: 6px 10px;
background: #fef3c7;
border-radius: 6px;
font-size: 11px;
color: #92400e;
display: none;
}
.test-identity-status.visible {
display: block;
}
/* Test tab */
.help-text {
font-size: 12px;
+26 -6
View File
@@ -141,13 +141,33 @@
<!-- Test Tab -->
<section class="tab-content" id="tab-test">
<p class="help-text">Type text containing your real values to see what would be sent.</p>
<textarea id="testInput" class="textarea" placeholder="Try: My name is John Smith, email john@gmail.com, logged in as jsmith@macbook-pro" rows="4"></textarea>
<div class="diff-view" id="diffView">
<div class="diff-label">What gets sent:</div>
<div class="diff-output" id="diffOutput"></div>
<div class="test-mode-toggle">
<button class="test-mode-btn active" data-mode="strip">Strip (real &rarr; fake)</button>
<button class="test-mode-btn" data-mode="reveal">Reveal (fake &rarr; real)</button>
</div>
<div class="diff-stats" id="diffStats"></div>
<div id="stripMode">
<p class="help-text">Paste text with your real info. See what gets sent.</p>
<textarea id="testInput" class="textarea" placeholder="Try: My name is John Smith, email john@gmail.com, logged in as jsmith@macbook-pro" rows="4"></textarea>
<div class="diff-view" id="diffView">
<div class="diff-label">What gets sent:</div>
<div class="diff-output" id="diffOutput"></div>
</div>
<div class="diff-stats" id="diffStats"></div>
</div>
<div id="revealMode" style="display:none">
<p class="help-text">Paste text from AI responses (with fake data). Get back real data to copy.</p>
<textarea id="revealInput" class="textarea" placeholder="Paste AI output here, e.g.: Hello Alex Demo, I see your project at /home/ademo/..." rows="4"></textarea>
<div class="diff-view" id="revealView">
<div class="diff-label">Your real data:</div>
<div class="diff-output" id="revealOutput"></div>
</div>
<button class="btn btn-primary" id="btnCopyRevealed" style="width:100%;margin-top:8px">Copy to Clipboard</button>
<div class="diff-stats" id="revealStats"></div>
</div>
<div class="test-identity-status" id="identityStatus"></div>
</section>
<!-- Footer -->
+156 -30
View File
@@ -34,6 +34,13 @@ document.addEventListener('DOMContentLoaded', async () => {
$(`#tab-${tab.dataset.tab}`).classList.add('active');
if (tab.dataset.tab === 'activity') renderActivity();
if (tab.dataset.tab === 'test') {
// Reload identity from storage in case it was just saved
Storage.getIdentity().then((id) => {
identity = id;
updateIdentityStatus();
});
}
});
});
@@ -76,8 +83,21 @@ document.addEventListener('DOMContentLoaded', async () => {
renderActivity();
});
// Test tab - live diff
// Test tab - live diff + reveal
$('#testInput').addEventListener('input', renderTestDiff);
$('#revealInput').addEventListener('input', renderRevealDiff);
$('#btnCopyRevealed').addEventListener('click', copyRevealedText);
// Test mode toggle (strip vs reveal)
$$('.test-mode-btn').forEach((btn) => {
btn.addEventListener('click', () => {
$$('.test-mode-btn').forEach((b) => b.classList.remove('active'));
btn.classList.add('active');
const mode = btn.dataset.mode;
$('#stripMode').style.display = mode === 'strip' ? 'block' : 'none';
$('#revealMode').style.display = mode === 'reveal' ? 'block' : 'none';
});
});
// Options link
$('#btnOptions').addEventListener('click', (e) => {
@@ -297,8 +317,7 @@ async function renderActivity() {
.join('');
}
// --- Test Diff ---
// Runs both smart patterns AND explicit mappings, shows combined result
// --- Test Diff (Strip: real → fake) ---
function renderTestDiff() {
const input = $('#testInput').value;
const output = $('#diffOutput');
@@ -310,39 +329,27 @@ function renderTestDiff() {
return;
}
// Smart patterns first (broader catches), then explicit mappings (specific overrides)
const smartResult = SmartPatterns.substitute(input, identity);
const explicitResult = SubstitutionEngine.substitute(smartResult.text, mappings);
const allReplacements = [...smartResult.replacements, ...explicitResult.replacements];
const finalText = explicitResult.text;
// Simple diff: highlight differences
if (finalText === input) {
output.textContent = input;
stats.textContent = 'No substitutions detected';
return;
}
// Build a visual diff by running smart patterns on original to find positions
// For display, we re-run on the original to get positions
const smartPositions = findReplacementPositions(input, identity, mappings);
if (smartPositions.length === 0) {
output.textContent = finalText;
} else {
// Build highlighted output from the final text
// Simpler approach: show the final text with replaced values highlighted
let html = escapeHtml(finalText);
for (const r of allReplacements) {
const escapedReplaced = escapeHtml(r.replaced);
html = html.replace(
escapedReplaced,
`<span class="sub-highlight" title="Was: ${escapeHtml(r.original)} [${r.pattern || r.category}]">${escapedReplaced}</span>`
);
}
output.innerHTML = html;
let html = escapeHtml(finalText);
for (const r of allReplacements) {
const escapedReplaced = escapeHtml(r.replaced);
html = html.replace(
escapedReplaced,
`<span class="sub-highlight" title="Was: ${escapeHtml(r.original)} [${r.pattern || r.category}]">${escapedReplaced}</span>`
);
}
output.innerHTML = html;
const smartCount = smartResult.replacements.length;
const explicitCount = explicitResult.replacements.length;
@@ -352,13 +359,132 @@ function renderTestDiff() {
stats.textContent = `${allReplacements.length} substitution${allReplacements.length !== 1 ? 's' : ''} (${parts.join(', ')})`;
}
function findReplacementPositions(text, ident, maps) {
const positions = [];
const r1 = SmartPatterns.substitute(text, ident);
positions.push(...r1.replacements);
const r2 = SubstitutionEngine.substitute(r1.text, maps);
positions.push(...r2.replacements);
return positions;
// --- Reveal Diff (fake → real) ---
function renderRevealDiff() {
const input = $('#revealInput').value;
const output = $('#revealOutput');
const stats = $('#revealStats');
if (!input) {
output.innerHTML = '';
stats.textContent = '';
return;
}
// Reverse: substitute → real using SmartPatterns reveal + explicit reveal
let result = input;
let totalCount = 0;
// Reverse explicit mappings (substitute → real)
const explicitRevealed = SubstitutionEngine.reveal(result, mappings);
// Count explicit reveals
for (const m of mappings) {
if (!m.enabled || !m.substitute) continue;
const regex = new RegExp(escapeRegex(m.substitute), m.caseSensitive ? 'g' : 'gi');
const matches = result.match(regex);
if (matches) totalCount += matches.length;
}
result = explicitRevealed;
// Reverse smart patterns (all identity substitutes → real)
const allSubs = gatherSmartSubstitutePairs(identity);
for (const pair of allSubs) {
const regex = new RegExp(escapeRegex(pair.substitute), 'gi');
const matches = result.match(regex);
if (matches) totalCount += matches.length;
result = result.replace(regex, pair.real);
}
if (result === input) {
output.textContent = input;
stats.textContent = 'No substituted values found to reveal';
return;
}
// Highlight revealed values
let html = escapeHtml(result);
for (const pair of [...allSubs, ...mappings.filter(m => m.enabled)]) {
const real = pair.real;
if (!real) continue;
const escapedReal = escapeHtml(real);
html = html.replace(
new RegExp(escapeRegex(escapedReal), 'gi'),
`<span class="sub-highlight" title="Was: ${escapeHtml(pair.substitute)}" style="background:#dbeafe;color:#1d4ed8">${escapedReal}</span>`
);
}
output.innerHTML = html;
stats.textContent = `${totalCount} value${totalCount !== 1 ? 's' : ''} revealed`;
}
// Gather all substitute → real pairs from identity for reveal
function gatherSmartSubstitutePairs(id) {
const pairs = [];
for (const e of (id.emails || [])) {
if (e.substitute && e.real) pairs.push(e);
}
if (id.catchAllEmail) {
pairs.push({ substitute: id.catchAllEmail, real: '[catch-all]' });
}
for (const n of (id.names || [])) {
if (n.substitute && n.real) pairs.push(n);
}
for (const u of (id.usernames || [])) {
if (u.substitute && u.real) pairs.push(u);
}
for (const h of (id.hostnames || [])) {
if (h.substitute && h.real) pairs.push(h);
}
for (const p of (id.phones || [])) {
if (p.substitute && p.real) pairs.push(p);
}
return pairs;
}
async function copyRevealedText() {
const output = $('#revealOutput');
const text = output.textContent;
if (!text) return;
try {
await navigator.clipboard.writeText(text);
const btn = $('#btnCopyRevealed');
btn.textContent = 'Copied!';
btn.style.background = '#059669';
setTimeout(() => {
btn.textContent = 'Copy to Clipboard';
btn.style.background = '';
}, 1500);
} catch (e) {
// Fallback
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
}
}
// --- Identity Status ---
function updateIdentityStatus() {
const el = $('#identityStatus');
const missing = [];
if (!(identity.names || []).some(n => n.type === 'first')) missing.push('first name');
if (!(identity.names || []).some(n => n.type === 'last')) missing.push('last name');
if ((identity.emails || []).length === 0 && !identity.catchAllEmail) missing.push('email');
if ((identity.usernames || []).length === 0) missing.push('username');
if (missing.length > 0) {
el.textContent = `Identity missing: ${missing.join(', ')}. Go to the Identity tab to set up.`;
el.classList.add('visible');
} else {
el.classList.remove('visible');
}
}
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// --- Status Dot ---