From 1995208941016207d42d406389299d250a433f2c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 26 Mar 2026 03:08:46 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20v0.3.0=20=E2=80=94=20multiple=20entries?= =?UTF-8?q?=20per=20field,=20README=20setup=20guide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Identity fields now support multiple entries per type: - Add unlimited names (first, last, middle, nickname), emails, usernames, hostnames, and phone numbers per profile - "+ Add" button on each section, "x" to remove rows - Names have a type selector (1st/Last/Mid/Nick) README now includes: - First-time setup walkthrough (step by step) - Icon color legend (gray/black/blue/red) - Keyboard shortcuts table - Note that extension does nothing until configured Also bumps version to 0.3.0 for Firefox re-signing. https://claude.ai/code/session_01Dvgwe7XMoSxnWXkih8p1Cw --- README.md | 37 ++++++- manifest.firefox.json | 2 +- manifest.json | 2 +- package.json | 2 +- src/options/options.html | 2 +- src/popup/popup.css | 50 ++++++++- src/popup/popup.html | 52 +++------- src/popup/popup.js | 213 +++++++++++++++++++++++++++------------ 8 files changed, 253 insertions(+), 107 deletions(-) diff --git a/README.md b/README.md index 17995e2..a2b58fc 100644 --- a/README.md +++ b/README.md @@ -60,12 +60,47 @@ A browser extension (Chrome + Firefox) that intercepts personal information and | `123-45-6789` | `[REDACTED-SSN]` | | `4111 1111 1111 1111` | `[REDACTED-CARD]` | +## First-time setup + +After installing the extension, **it does nothing until you configure it**. The icon will be gray to remind you. + +1. Click the Silent Send icon in your toolbar +2. You'll see the **Identity** tab with a red "Setup required" banner +3. Fill in your real info and fake substitutes: + - **Names** — add as many as needed (first, last, nicknames). Click "+ Add" for more rows. + - **Emails** — your real email → fake email. Set a catch-all for unknown addresses. + - **Usernames** — your system username → fake username + - **Hostnames** — your computer name → fake computer name + - **Phones** — your phone number → fake number +4. Click **Save Identity** +5. The icon turns **black** — you're now protected + +You can create multiple profiles (Personal, Work, Spouse) using the dropdown at the top. Each can be toggled on/off independently. + +### Icon colors + +| Icon color | Meaning | +|-----------|---------| +| **Gray** | Not configured — does nothing until you set up your identity | +| **Black** | Active and protecting | +| **Blue** | Reveal mode on — showing your real data in AI responses | +| **Red** | Manually disabled | + +### Keyboard shortcuts + +| Shortcut | Action | +|----------|--------| +| `Alt+Shift+R` | Toggle reveal mode | +| `Alt+Shift+S` | Toggle Silent Send on/off | + +Remap in Chrome: `chrome://extensions/shortcuts` | Firefox: `about:addons` → gear → Manage Extension Shortcuts + ## How to verify it's working - **Badge count** on the extension icon shows substitutions per page - **Activity tab** in the popup shows a timestamped log of every substitution - **Test tab** in the popup lets you type text and see the before/after diff live -- **Reveal mode** (eye icon) toggles showing real vs substitute data in responses +- **Reveal mode** (eye icon or `Alt+Shift+R`) shows your real data in AI responses for easy copy/paste - **Browser DevTools** → Console shows `[Silent Send] Substituted N value(s)` messages ## Installation diff --git a/manifest.firefox.json b/manifest.firefox.json index d9eb096..56872a9 100644 --- a/manifest.firefox.json +++ b/manifest.firefox.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Silent Send", - "version": "0.2.0", + "version": "0.3.0", "description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.", "browser_specific_settings": { "gecko": { diff --git a/manifest.json b/manifest.json index c66096a..ba379d1 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Silent Send", - "version": "0.2.0", + "version": "0.3.0", "description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.", "permissions": [ "storage", diff --git a/package.json b/package.json index e5a8bd1..50da784 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "silent-send", - "version": "0.2.0", + "version": "0.3.0", "private": true, "description": "Browser extension that substitutes personal data before sending to AI services", "scripts": { diff --git a/src/options/options.html b/src/options/options.html index c0f600a..ccdc299 100644 --- a/src/options/options.html +++ b/src/options/options.html @@ -136,7 +136,7 @@ diff --git a/src/popup/popup.css b/src/popup/popup.css index 9c1bc09..df8f32c 100644 --- a/src/popup/popup.css +++ b/src/popup/popup.css @@ -310,13 +310,61 @@ body { margin-bottom: 4px; } +.id-label-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 6px; +} + .id-label { font-size: 11px; font-weight: 600; color: #374151; text-transform: uppercase; letter-spacing: 0.5px; - margin-bottom: 6px; +} + +.btn-add { + background: none; + border: 1px solid #d1d5db; + border-radius: 4px; + padding: 2px 8px; + font-size: 10px; + font-weight: 500; + color: #6b7280; + cursor: pointer; +} + +.btn-add:hover { background: #f3f4f6; color: #111; } + +.id-entry-row { + display: flex; + align-items: center; + gap: 4px; + margin-bottom: 3px; +} + +.id-entry-row .input-sm { flex: 1; } + +.id-entry-row .btn-remove { + background: none; + border: none; + color: #d1d5db; + cursor: pointer; + font-size: 16px; + padding: 0 2px; + line-height: 1; +} + +.id-entry-row .btn-remove:hover { color: #dc2626; } + +.id-type-label { + font-size: 9px; + color: #9ca3af; + text-transform: uppercase; + min-width: 28px; + text-align: center; } .id-row { diff --git a/src/popup/popup.html b/src/popup/popup.html index 4c36a69..264b1fb 100644 --- a/src/popup/popup.html +++ b/src/popup/popup.html @@ -60,55 +60,35 @@

Each profile is a person/identity. All active profiles are protected simultaneously.

-
Names
-
- - - -
-
- - - -
+
Names
+
Catches: first last, last first, possessives, case variants
-
Email
+
Emails
+
- - - -
-
- +
Catches: any *@gmail.com, *@yahoo.com, *@outlook.com, etc.
-
Username / Computer
-
- - - -
-
- - - -
-
Catches: jsmith@macbook-pro, /home/jsmith, ~jsmith, C:\Users\jsmith
+
Usernames
+
+
Catches: user@host, /home/user, ~user, C:\Users\user
-
Phone
-
- - - -
+
Hostnames
+
+
Catches: hostname in user@hostname and standalone
+
+ +
+
Phones
+
Catches: all formats — (555) 123-4567, 555-123-4567, 555.123.4567
diff --git a/src/popup/popup.js b/src/popup/popup.js index 6edc5c6..ab1908d 100644 --- a/src/popup/popup.js +++ b/src/popup/popup.js @@ -191,81 +191,164 @@ function renderProfileSelector() { $('#profileActive').checked = profile?.active ?? true; } -// --- Identity --- +// --- Identity (dynamic multi-entry) --- + +// Field configs for rendering +const FIELD_CONFIGS = { + names: { + container: 'idNames', + placeholderReal: 'Real name', + placeholderSub: 'Fake name', + typeOptions: [{ value: 'first', label: '1st' }, { value: 'last', label: 'Last' }, { value: 'middle', label: 'Mid' }, { value: 'nick', label: 'Nick' }], + defaultType: 'first', + }, + emails: { + container: 'idEmails', + placeholderReal: 'you@gmail.com', + placeholderSub: 'fake@example.com', + }, + usernames: { + container: 'idUsernames', + placeholderReal: 'jsmith', + placeholderSub: 'ademo', + }, + hostnames: { + container: 'idHostnames', + placeholderReal: 'macbook-pro', + placeholderSub: 'mycomputer', + }, + phones: { + container: 'idPhones', + placeholderReal: '(555) 123-4567', + placeholderSub: '(555) 000-0000', + }, +}; + +function renderFieldList(fieldName, items) { + const config = FIELD_CONFIGS[fieldName]; + const container = $(`#${config.container}`); + if (!container) return; + + if (!items || items.length === 0) { + // Show one empty row + items = [{ real: '', substitute: '', type: config.defaultType || '' }]; + } + + container.innerHTML = items.map((item, i) => { + let typeHtml = ''; + if (config.typeOptions) { + typeHtml = ``; + } + return `
+ ${typeHtml} + + + + +
`; + }).join(''); + + // Bind remove buttons + container.querySelectorAll('.btn-remove').forEach(btn => { + btn.addEventListener('click', () => { + const row = btn.closest('.id-entry-row'); + if (container.querySelectorAll('.id-entry-row').length > 1) { + row.remove(); + } else { + // Clear instead of remove if it's the last row + row.querySelectorAll('input').forEach(inp => inp.value = ''); + } + }); + }); +} + function loadIdentityForm() { const profile = profiles.find(p => p.id === currentProfileId); if (!profile) return; - const first = (profile.names || []).find(n => n.type === 'first'); - const last = (profile.names || []).find(n => n.type === 'last'); - const email = (profile.emails || [])[0]; - const user = (profile.usernames || [])[0]; - const host = (profile.hostnames || [])[0]; - const phone = (profile.phones || [])[0]; - - $('#idFirstReal').value = first?.real || ''; - $('#idFirstSub').value = first?.substitute || ''; - $('#idLastReal').value = last?.real || ''; - $('#idLastSub').value = last?.substitute || ''; - $('#idEmailReal').value = email?.real || ''; - $('#idEmailSub').value = email?.substitute || ''; + renderFieldList('names', profile.names || []); + renderFieldList('emails', profile.emails || []); + renderFieldList('usernames', profile.usernames || []); + renderFieldList('hostnames', profile.hostnames || []); + renderFieldList('phones', profile.phones || []); $('#idCatchAllEmail').value = profile.catchAllEmail || ''; - $('#idUserReal').value = user?.real || ''; - $('#idUserSub').value = user?.substitute || ''; - $('#idHostReal').value = host?.real || ''; - $('#idHostSub').value = host?.substitute || ''; - $('#idPhoneReal').value = phone?.real || ''; - $('#idPhoneSub').value = phone?.substitute || ''; $('#profileActive').checked = profile.active ?? true; + + // Bind add buttons + $$('.btn-add').forEach(btn => { + // Remove old listeners by cloning + const newBtn = btn.cloneNode(true); + btn.replaceWith(newBtn); + newBtn.addEventListener('click', () => { + const field = newBtn.dataset.field; + const config = FIELD_CONFIGS[field]; + const container = $(`#${config.container}`); + const count = container.querySelectorAll('.id-entry-row').length; + const tempDiv = document.createElement('div'); + let typeHtml = ''; + if (config.typeOptions) { + // Alternate: if first row is 'first', next should be 'last', etc. + const defaultType = count % 2 === 0 ? 'first' : 'last'; + typeHtml = ``; + } + tempDiv.innerHTML = `
+ ${typeHtml} + + + + +
`; + const row = tempDiv.firstElementChild; + container.appendChild(row); + row.querySelector('.btn-remove').addEventListener('click', () => { + if (container.querySelectorAll('.id-entry-row').length > 1) row.remove(); + else row.querySelectorAll('input').forEach(inp => inp.value = ''); + }); + row.querySelector('.id-real').focus(); + }); + }); +} + +function escapeAttr(str) { + return str.replace(/&/g, '&').replace(/"/g, '"').replace(/ { + const real = row.querySelector('.id-real')?.value.trim() || ''; + const sub = row.querySelector('.id-sub')?.value.trim() || ''; + if (real && sub) { + const entry = { real, substitute: sub }; + const typeSelect = row.querySelector('.id-type-select'); + if (typeSelect) entry.type = typeSelect.value; + entries.push(entry); + } + }); + + return entries; } async function saveIdentity() { - const names = []; - const firstReal = $('#idFirstReal').value.trim(); - const firstSub = $('#idFirstSub').value.trim(); - if (firstReal && firstSub) { - names.push({ real: firstReal, substitute: firstSub, type: 'first' }); - } - const lastReal = $('#idLastReal').value.trim(); - const lastSub = $('#idLastSub').value.trim(); - if (lastReal && lastSub) { - names.push({ real: lastReal, substitute: lastSub, type: 'last' }); - } - - const emails = []; - const emailReal = $('#idEmailReal').value.trim(); - const emailSub = $('#idEmailSub').value.trim(); - if (emailReal && emailSub) { - emails.push({ real: emailReal, substitute: emailSub }); - } - - const usernames = []; - const userReal = $('#idUserReal').value.trim(); - const userSub = $('#idUserSub').value.trim(); - if (userReal && userSub) { - usernames.push({ real: userReal, substitute: userSub }); - } - - const hostnames = []; - const hostReal = $('#idHostReal').value.trim(); - const hostSub = $('#idHostSub').value.trim(); - if (hostReal && hostSub) { - hostnames.push({ real: hostReal, substitute: hostSub }); - } - - const phones = []; - const phoneReal = $('#idPhoneReal').value.trim(); - const phoneSub = $('#idPhoneSub').value.trim(); - if (phoneReal && phoneSub) { - phones.push({ real: phoneReal, substitute: phoneSub }); - } - const profileData = { - names, - emails, - usernames, - hostnames, - phones, + names: readFieldEntries('names'), + emails: readFieldEntries('emails'), + usernames: readFieldEntries('usernames'), + hostnames: readFieldEntries('hostnames'), + phones: readFieldEntries('phones'), catchAllEmail: $('#idCatchAllEmail').value.trim(), emailDomains: [], enabled: { emails: true, names: true, usernames: true, phones: true, paths: true },