From ae451f73df14e3d6085aca8f58ae68d647a5e053 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 26 Mar 2026 21:59:28 +0000 Subject: [PATCH 1/3] fix: reveal mode was destroying chat input text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reveal mode's text replacement was walking ALL text nodes on the page including contenteditable elements (the chat input box on Claude.ai, ChatGPT, etc.), overwriting the user's typed text with garbled revealed content. Fixed by adding contenteditable checks to: - revealInElement() — returns early if element is contenteditable - unrevealInElement() — same - highlightMatches() — TreeWalker filter rejects contenteditable nodes - MutationObserver — skips addedNodes and characterData mutations inside contenteditable elements All four code paths now use parent.closest('[contenteditable="true"]') to detect and skip chat input areas. https://claude.ai/code/session_01SWSwDfMVij53bCTNSCLMwn --- src/content/content.js | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/content/content.js b/src/content/content.js index b8c7cd4..5b4fc7b 100644 --- a/src/content/content.js +++ b/src/content/content.js @@ -1318,12 +1318,16 @@ function revealInElement(el) { if (SKIP_REVEAL_TAGS.has(el.tagName)) return; if (el.classList?.contains('ss-reveal-badge')) return; + // Never touch contenteditable elements (chat input boxes) + if (el.isContentEditable) return; 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?.closest?.('.ss-autodetect-warning, .ss-presend-warning, .ss-reveal-badge')) return NodeFilter.FILTER_REJECT; + // Skip contenteditable areas (chat input) + if (parent?.closest?.('[contenteditable="true"]')) return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_ACCEPT; } }); @@ -1346,8 +1350,15 @@ function unrevealInElement(el) { if (SKIP_REVEAL_TAGS.has(el.tagName)) return; + if (el.isContentEditable) return; - const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT); + const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, { + acceptNode(node) { + const parent = node.parentElement; + if (parent?.closest?.('[contenteditable="true"]')) return NodeFilter.FILTER_REJECT; + return NodeFilter.FILTER_ACCEPT; + } + }); let textNode; while ((textNode = walker.nextNode())) { const original = originalTexts.get(textNode); @@ -1374,6 +1385,7 @@ const parent = node.parentElement; if (parent && SKIP_REVEAL_TAGS.has(parent.tagName)) return NodeFilter.FILTER_REJECT; if (parent?.closest?.('.ss-autodetect-warning, .ss-presend-warning, .ss-reveal-badge')) return NodeFilter.FILTER_REJECT; + if (parent?.closest?.('[contenteditable="true"]')) return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_ACCEPT; } }); @@ -1454,10 +1466,16 @@ // Only do text replacement in reveal mode if (settings.revealMode) { if (node.nodeType === Node.ELEMENT_NODE) { - if (!SKIP_REVEAL_TAGS.has(node.tagName)) { + // Skip contenteditable (chat input) and skipped tags + if (!SKIP_REVEAL_TAGS.has(node.tagName) && + !node.isContentEditable && + !node.closest?.('[contenteditable="true"]')) { revealInElement(node); } } else if (node.nodeType === Node.TEXT_NODE) { + // Skip text nodes inside contenteditable + const parent = node.parentElement; + if (parent?.closest?.('[contenteditable="true"]')) continue; const text = node.textContent; if (text && text.length >= MIN_STRING_LENGTH) { if (!originalTexts.has(node)) { @@ -1479,6 +1497,8 @@ const text = mutation.target.textContent; if (text && text.length >= MIN_STRING_LENGTH) { const parent = mutation.target.parentElement; + // Skip contenteditable (chat input) + if (parent?.closest?.('[contenteditable="true"]')) continue; if (parent && !SKIP_REVEAL_TAGS.has(parent.tagName)) { if (!originalTexts.has(mutation.target)) { originalTexts.set(mutation.target, text); From c25be011f7c815f4da46a6ea85214685d2f3a122 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 26 Mar 2026 22:18:11 +0000 Subject: [PATCH 2/3] fix: proper noun detection too aggressive + add ignore button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Proper noun heuristic: - Changed from matching any single capitalized word to requiring TWO OR MORE consecutive capitalized words (e.g. "Acme Corp") - Single capitalized words at sentence starts were causing massive false positives — every sentence starts with a capital letter - Minimum 5 characters total and 2 proper words required Ignore button: - Each PPI warning item now has an X (ignore) button alongside the + (add mapping) button - Ignored values are persisted to storage (ss_ignored_ppi) so they stay dismissed across page reloads - Ignored values are skipped in both pattern detection and proper noun detection - Clicking ignore removes the item from the warning and re-scans https://claude.ai/code/session_01SWSwDfMVij53bCTNSCLMwn --- src/content/content.css | 18 ++++++++++ src/content/content.js | 76 ++++++++++++++++++++++++++++++----------- src/lib/auto-detect.js | 14 ++++---- 3 files changed, 81 insertions(+), 27 deletions(-) diff --git a/src/content/content.css b/src/content/content.css index 077a022..be1beda 100644 --- a/src/content/content.css +++ b/src/content/content.css @@ -220,6 +220,24 @@ .ss-ps-add:hover { background: rgba(74, 222, 128, 0.15); } .ss-ps-add:disabled { border-color: #333; cursor: default; } +.ss-ps-ignore { + background: none; + border: 1px solid #6b7280; + border-radius: 4px; + color: #9ca3af; + font-size: 11px; + width: 24px; + height: 24px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + padding: 0; +} + +.ss-ps-ignore:hover { background: rgba(156, 163, 175, 0.15); color: #e5e7eb; } + /* Floating reveal mode indicator */ .ss-reveal-badge { position: fixed; diff --git a/src/content/content.js b/src/content/content.js index 5b4fc7b..d1df9e9 100644 --- a/src/content/content.js +++ b/src/content/content.js @@ -400,50 +400,44 @@ /** * Detect proper nouns (potential names, company names, project names) * that aren't configured in identity. Uses capitalization heuristics: - * - Capitalized words not at the start of a sentence - * - Multi-word capitalized sequences (e.g. "Acme Corp", "Project Atlas") + * - ONLY multi-word capitalized sequences (e.g. "Acme Corp", "Project Atlas") + * - Single capitalized words are too noisy — every sentence starts with one * - Filters out common English words and programming terms */ function detectProperNouns(text, configured) { const findings = []; - // Match capitalized words that aren't at the very start of the text - // and aren't after a period/newline (sentence start) - const re = /(?:^|[.!?\n]\s*)?([A-Z][a-z]{2,}(?:\s+[A-Z][a-z]{2,})*)/g; + // Only match TWO OR MORE consecutive capitalized words + // Single capitalized words cause too many false positives + const re = /\b([A-Z][a-z]{2,}(?:\s+[A-Z][a-z]{2,})+)\b/g; let m; while ((m = re.exec(text)) !== null) { const fullMatch = m[1]; if (!fullMatch) continue; - // Check if this is at the start of a sentence - const before = text.slice(Math.max(0, m.index - 2), m.index); - const isSentenceStart = m.index === 0 || /[.!?\n]\s*$/.test(before); - - // Split into individual words and check each + // Split into individual words and filter common ones const words = fullMatch.split(/\s+/); const properWords = words.filter(w => w.length >= 3 && !COMMON_CAPITALIZED.has(w.toLowerCase()) && - !configured.has(w.toLowerCase()) + !configured.has(w.toLowerCase()) && + !ignoredValues.has(w.toLowerCase()) ); - if (properWords.length === 0) continue; + if (properWords.length < 2) continue; // need at least 2 proper words - // Single capitalized word at sentence start = likely not a proper noun - if (isSentenceStart && properWords.length === 1 && words.length === 1) continue; - - // Multi-word capitalized sequence is likely a proper noun - // Single capitalized word mid-sentence is likely a proper noun const value = properWords.join(' '); - if (value.length >= 3 && !configured.has(value.toLowerCase())) { + if (value.length >= 5 && !configured.has(value.toLowerCase()) && !ignoredValues.has(value.toLowerCase())) { findings.push({ name: 'Possible Name/Org', value, - hint: 'Capitalized word — could be a name, company, or project', + hint: 'Capitalized phrase — could be a name, company, or project', category: 'name', }); } } + } + } // Deduplicate const seen = new Set(); @@ -477,6 +471,7 @@ while ((m = pat.re.exec(text)) !== null) { const val = m[0]; if (configured.has(val.toLowerCase())) continue; + if (ignoredValues.has(val.toLowerCase())) continue; if (pat.skip && pat.skip.test(val)) continue; findings.push({ name: pat.name, value: val, hint: pat.hint, category: pat.cat }); } @@ -630,6 +625,30 @@ // ============================================================ const sessionSubstitutions = new Map(); + // Values the user has explicitly ignored via the "Ignore" button. + // Persisted to storage so they stay dismissed across page reloads. + const ignoredValues = new Set(); + + // Load ignored values from storage + (async () => { + const stored = await getStorageData('ss_ignored_ppi'); + if (Array.isArray(stored)) { + for (const v of stored) ignoredValues.add(v.toLowerCase()); + } + })(); + + function addIgnoredValue(value) { + ignoredValues.add(value.toLowerCase()); + // Persist + getStorageData('ss_ignored_ppi').then(stored => { + const list = Array.isArray(stored) ? stored : []; + if (!list.includes(value.toLowerCase())) { + list.push(value.toLowerCase()); + setStorageData('ss_ignored_ppi', list); + } + }); + } + // ============================================================ // Notify content script of substitutions (for badge + logging) // ============================================================ @@ -1632,6 +1651,7 @@ ${settings.autoAddDetected !== false ? `` : ''} + `; }).join(''); @@ -1694,6 +1714,24 @@ btn.disabled = true; }); }); + + // Ignore buttons + preSendWarningEl.querySelectorAll('.ss-ps-ignore').forEach(btn => { + btn.addEventListener('click', () => { + const value = decodeURIComponent(btn.dataset.value); + addIgnoredValue(value); + + // Remove this item's row + const row = btn.closest('.ss-ps-item'); + if (row) row.remove(); + + // Re-scan to update warning + if (inputEl) { + if (inputScanTimer) clearTimeout(inputScanTimer); + inputScanTimer = setTimeout(() => scanInputForPPI(inputEl), 150); + } + }); + }); } // Replace all occurrences of `real` with `fake` in an input or contenteditable element diff --git a/src/lib/auto-detect.js b/src/lib/auto-detect.js index e1af629..35a9eab 100644 --- a/src/lib/auto-detect.js +++ b/src/lib/auto-detect.js @@ -218,16 +218,15 @@ const AutoDetect = { */ _detectProperNouns(text, configured) { const findings = []; - const re = /(?:^|[.!?\n]\s*)?([A-Z][a-z]{2,}(?:\s+[A-Z][a-z]{2,})*)/g; + // Only match TWO OR MORE consecutive capitalized words + // Single capitalized words cause too many false positives (sentence starts) + const re = /\b([A-Z][a-z]{2,}(?:\s+[A-Z][a-z]{2,})+)\b/g; let m; while ((m = re.exec(text)) !== null) { const fullMatch = m[1]; if (!fullMatch) continue; - const before = text.slice(Math.max(0, m.index - 2), m.index); - const isSentenceStart = m.index === 0 || /[.!?\n]\s*$/.test(before); - const words = fullMatch.split(/\s+/); const properWords = words.filter(w => w.length >= 3 && @@ -235,15 +234,14 @@ const AutoDetect = { !configured.has(w.toLowerCase()) ); - if (properWords.length === 0) continue; - if (isSentenceStart && properWords.length === 1 && words.length === 1) continue; + if (properWords.length < 2) continue; // need at least 2 proper words const value = properWords.join(' '); - if (value.length >= 3 && !configured.has(value.toLowerCase())) { + if (value.length >= 5 && !configured.has(value.toLowerCase())) { findings.push({ name: 'Possible Name/Org', value, - hint: 'Capitalized word — could be a name, company, or project', + hint: 'Capitalized phrase — could be a name, company, or project', category: 'name', }); } From c5239609c14d2a6ef424b29e91d193990b316be6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 26 Mar 2026 22:34:30 +0000 Subject: [PATCH 3/3] fix: ignore button now says 'ignore' instead of X icon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The X icon looked like a dismiss/close button (temporary). Changed to a small 'IGNORE' text link that clearly communicates the action is permanent — the value will never be flagged again. https://claude.ai/code/session_01SWSwDfMVij53bCTNSCLMwn --- src/content/content.css | 18 +++++++----------- src/content/content.js | 2 +- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/content/content.css b/src/content/content.css index be1beda..8f287d9 100644 --- a/src/content/content.css +++ b/src/content/content.css @@ -222,21 +222,17 @@ .ss-ps-ignore { background: none; - border: 1px solid #6b7280; - border-radius: 4px; - color: #9ca3af; - font-size: 11px; - width: 24px; - height: 24px; + border: none; + color: #6b7280; + font-size: 9px; cursor: pointer; - display: flex; - align-items: center; - justify-content: center; flex-shrink: 0; - padding: 0; + padding: 2px 4px; + text-transform: uppercase; + letter-spacing: 0.5px; } -.ss-ps-ignore:hover { background: rgba(156, 163, 175, 0.15); color: #e5e7eb; } +.ss-ps-ignore:hover { color: #e5e7eb; text-decoration: underline; } /* Floating reveal mode indicator */ .ss-reveal-badge { diff --git a/src/content/content.js b/src/content/content.js index d1df9e9..35b4915 100644 --- a/src/content/content.js +++ b/src/content/content.js @@ -1651,7 +1651,7 @@ ${settings.autoAddDetected !== false ? `` : ''} - + `; }).join('');