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.
-
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 },