Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "webbrain",
"version": "24.2.0",
"version": "24.2.1",
"description": "Open-source AI browser agent — chat with pages, automate tasks, multi-provider LLM support.",
"private": true,
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion src/chrome/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# WebBrain Chrome/Edge Extension — Architecture

> Version 24.2.0 · Manifest V3 · Service Worker background
> Version 24.2.1 · Manifest V3 · Service Worker background

## High-Level Overview

Expand Down
2 changes: 1 addition & 1 deletion src/chrome/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "WebBrain",
"version": "24.2.0",
"version": "24.2.1",
"description": "Open-source AI browser agent — chat with pages, automate tasks, multi-provider LLM support.",
"permissions": [
"sidePanel",
Expand Down
15 changes: 14 additions & 1 deletion src/chrome/src/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import {
createConfigExport,
parseConfigImport,
} from './config-transfer.js';
import { installDownloadDirectoryRouting } from './download-directory.js';

/**
* WebBrain Service Worker (Background Script)
Expand All @@ -71,6 +72,7 @@ const providerManager = new ProviderManager();
const agent = new Agent(providerManager);
const userMemoryStore = createUserMemoryStore(chrome.storage.local);
const profileSync = new ProfileSyncManager(chrome.storage.local);
installDownloadDirectoryRouting(chrome);
const scheduler = new ScheduledJobManager({
api: chrome,
agent,
Expand Down Expand Up @@ -731,8 +733,19 @@ async function loadPlanReviewSettings() {
const planBeforeActReady = loadPlanBeforeAct();
const planReviewReady = loadPlanReviewSettings();

function showFirstInstallGuide(details) {
if (details?.reason !== 'install') return;
chrome.tabs.create({
url: chrome.runtime.getURL('src/ui/install.html'),
active: true,
}).catch((error) => {
console.warn('[WebBrain] Could not open the first-install pinning guide:', error);
});
}

// Initialize on install
chrome.runtime.onInstalled.addListener(async () => {
chrome.runtime.onInstalled.addListener(async (details) => {
showFirstInstallGuide(details);
createContextMenus();
await providerManager.load();
await loadMaxSteps();
Expand Down
2 changes: 2 additions & 0 deletions src/chrome/src/config-transfer.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const DEFAULT_CONFIG_SETTINGS = Object.freeze({
planBeforeAct: true,
planReviewMode: 'confidence',
planReviewConfidenceThreshold: 75,
downloadDirectory: '',
notifySound: true,
completionConfetti: true,
tracingEnabled: false,
Expand Down Expand Up @@ -109,6 +110,7 @@ const STRING_KEYS = new Set([
'autoScreenshot',
'planBeforeActMode',
'planReviewMode',
'downloadDirectory',
'providerFilter',
'activeProvider',
'profileText',
Expand Down
73 changes: 73 additions & 0 deletions src/chrome/src/download-directory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
export const DOWNLOAD_DIRECTORY_STORAGE_KEY = 'downloadDirectory';

/**
* Browser download paths are always relative to the browser/OS Downloads
* directory. Keep the stored preference in that portable form so an empty
* value naturally means "use the system default".
*/
export function normalizeDownloadDirectory(value) {
const raw = String(value ?? '').trim().replace(/\\/g, '/');
if (!raw) return '';
if (raw.startsWith('/') || raw.startsWith('~') || /^[a-zA-Z]:\//.test(raw)) return '';

const parts = raw.split('/').map((part) => part.trim());
if (parts.some((part) => !part || part === '.' || part === '..')) return '';
if (parts.some((part) => /[\u0000-\u001f\u007f<>:"|?*]/.test(part))) return '';
return parts.join('/');
}

export function filenameInDownloadDirectory(directory, filename) {
const normalizedDirectory = normalizeDownloadDirectory(directory);
if (!normalizedDirectory) return '';

let basename = String(filename ?? '')
.replace(/\\/g, '/')
.split('/')
.pop()
.replace(/[\u0000-\u001f\u007f<>:"|?*]/g, '_')
.trim();
basename = basename.replace(/^\.+|\.+$/g, '').trim();
if (!basename) return '';
if (/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(basename)) {
basename = `_${basename}`;
}
return `${normalizedDirectory}/${basename}`;
}

export function createDownloadDirectoryListener(api) {
let directory = '';
let changedBeforeReady = false;
const ready = Promise.resolve(api.storage.local.get(DOWNLOAD_DIRECTORY_STORAGE_KEY))
.then((stored) => {
if (!changedBeforeReady) {
directory = normalizeDownloadDirectory(stored?.[DOWNLOAD_DIRECTORY_STORAGE_KEY]);
}
})
.catch(() => {});

api.storage?.onChanged?.addListener?.((changes, area) => {
if (area !== 'local' || !changes[DOWNLOAD_DIRECTORY_STORAGE_KEY]) return;
changedBeforeReady = true;
directory = normalizeDownloadDirectory(changes[DOWNLOAD_DIRECTORY_STORAGE_KEY].newValue);
});

return function routeWebBrainDownload(downloadItem, suggest) {
if (downloadItem?.byExtensionId !== api.runtime.id) {
suggest();
return undefined;
}

ready.then(() => {
const filename = filenameInDownloadDirectory(directory, downloadItem.filename);
suggest(filename ? { filename } : undefined);
});
return true;
};
}

export function installDownloadDirectoryRouting(api) {
if (!api?.downloads?.onDeterminingFilename?.addListener) return null;
const listener = createDownloadDirectoryListener(api);
api.downloads.onDeterminingFilename.addListener(listener);
return listener;
}
12 changes: 7 additions & 5 deletions src/chrome/src/ui/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ function detect() {
const saved = localStorage.getItem(LS_KEY);
if (saved && DICTS[saved]) return saved;
} catch { /* storage denied */ }
const nav = (navigator.language || 'en').slice(0, 2).toLowerCase();
const nav = (globalThis.navigator?.language || 'en').slice(0, 2).toLowerCase();
return DICTS[nav] ? nav : 'en';
}

Expand Down Expand Up @@ -118,8 +118,10 @@ try {
} catch { /* ignore */ }

// Apply on first load. If the DOM isn't ready yet, wait for it.
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => applyDOMTranslations(document));
} else {
applyDOMTranslations(document);
if (typeof document !== 'undefined') {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => applyDOMTranslations(document));
} else {
applyDOMTranslations(document);
}
}
Loading