feat: custom domains for OpenWebUI + fix README issues

- Add custom domain support in Options page so users can add
  self-hosted AI services (e.g. https://ai.myserver.com)
- Background worker dynamically injects content scripts on
  custom domains using scripting.executeScript
- Add optional_host_permissions so Chrome can grant per-domain access
- Rewrite README: add clone step to Firefox instructions, clarify
  what "credentials" means in step 3, add Windows commands alongside
  Mac/Linux for every terminal step
- Bump version to 0.2.0

https://claude.ai/code/session_01Dvgwe7XMoSxnWXkih8p1Cw
This commit is contained in:
Claude
2026-03-26 00:35:21 +00:00
parent 1fe813337b
commit 60b810b1b9
8 changed files with 286 additions and 118 deletions
+70 -13
View File
@@ -2,6 +2,7 @@
* Silent Send - Background Service Worker
*
* Manages badge count, coordinates between popup and content scripts.
* Injects content scripts on custom domains dynamically.
* Uses `api` alias for cross-browser compatibility (Chrome + Firefox).
*/
@@ -11,6 +12,18 @@ import api from '../lib/browser-polyfill.js';
// Track substitution counts per tab
const tabCounts = new Map();
// Built-in URL patterns
const BUILTIN_URL_PATTERNS = [
'https://claude.ai/*',
'https://chatgpt.com/*',
'https://chat.openai.com/*',
'https://grok.x.ai/*',
'https://x.com/i/grok*',
'https://gemini.google.com/*',
'http://localhost/*',
'http://127.0.0.1/*',
];
// --- Badge Management ---
function updateBadge(tabId) {
@@ -22,11 +35,16 @@ function updateBadge(tabId) {
}
// Reset count when tab navigates
api.tabs.onUpdated.addListener((tabId, changeInfo) => {
api.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
if (changeInfo.status === 'loading') {
tabCounts.set(tabId, 0);
updateBadge(tabId);
}
// Inject content script on custom domains when page loads
if (changeInfo.status === 'complete' && tab.url) {
await injectOnCustomDomain(tabId, tab.url);
}
});
// Cleanup when tab closes
@@ -34,6 +52,47 @@ api.tabs.onRemoved.addListener((tabId) => {
tabCounts.delete(tabId);
});
// --- Dynamic injection for custom domains ---
async function injectOnCustomDomain(tabId, tabUrl) {
const settings = await Storage.getSettings();
const customDomains = settings.customDomains || [];
if (customDomains.length === 0) return;
const matches = customDomains.some((domain) => tabUrl.startsWith(domain));
if (!matches) return;
// Check if already injected (avoid double-injection)
try {
const results = await api.scripting.executeScript({
target: { tabId },
func: () => !!window.__silentSendInjected,
});
if (results?.[0]?.result) return;
} catch (e) {
// Permission denied — user hasn't granted access to this domain
return;
}
// Inject CSS
try {
await api.scripting.insertCSS({
target: { tabId },
files: ['src/content/content.css'],
});
} catch (e) { /* non-fatal */ }
// Inject content script
try {
await api.scripting.executeScript({
target: { tabId },
files: ['src/content/injector.js'],
});
} catch (e) {
console.warn('[Silent Send] Failed to inject on custom domain:', e);
}
}
// --- Message Handling ---
api.runtime.onMessage.addListener((message, sender, sendResponse) => {
@@ -88,18 +147,16 @@ const messageHandlers = {
async 'update:settings'(message) {
await Storage.saveSettings(message.settings);
// Broadcast to content scripts on all supported sites
const SUPPORTED_URLS = [
'https://claude.ai/*',
'https://chatgpt.com/*',
'https://chat.openai.com/*',
'https://grok.x.ai/*',
'https://x.com/i/grok*',
'https://gemini.google.com/*',
'http://localhost/*',
'http://127.0.0.1/*',
];
for (const urlPattern of SUPPORTED_URLS) {
// Build list of all URL patterns (built-in + custom)
const allPatterns = [...BUILTIN_URL_PATTERNS];
const customDomains = message.settings.customDomains || [];
for (const domain of customDomains) {
allPatterns.push(domain + '/*');
}
// Broadcast to content scripts
for (const urlPattern of allPatterns) {
const tabs = await api.tabs.query({ url: urlPattern }).catch(() => []);
for (const tab of tabs) {
api.tabs.sendMessage(tab.id, {