From c51b1d2cb6f1e3eb0dd63fc7d8cbb65404961c4a Mon Sep 17 00:00:00 2001 From: Emre Sokullu Date: Fri, 17 Jul 2026 09:14:21 +0300 Subject: [PATCH 1/7] Make download directory configurable --- src/chrome/src/background.js | 2 + src/chrome/src/config-transfer.js | 2 + src/chrome/src/download-directory.js | 69 +++++++++ src/chrome/src/ui/locales/ar.js | 4 + src/chrome/src/ui/locales/en.js | 4 + src/chrome/src/ui/locales/es.js | 4 + src/chrome/src/ui/locales/fr.js | 4 + src/chrome/src/ui/locales/he.js | 4 + src/chrome/src/ui/locales/id.js | 4 + src/chrome/src/ui/locales/ja.js | 4 + src/chrome/src/ui/locales/ko.js | 4 + src/chrome/src/ui/locales/ms.js | 4 + src/chrome/src/ui/locales/pl.js | 4 + src/chrome/src/ui/locales/ru.js | 4 + src/chrome/src/ui/locales/th.js | 4 + src/chrome/src/ui/locales/tl.js | 4 + src/chrome/src/ui/locales/tr.js | 4 + src/chrome/src/ui/locales/uk.js | 4 + src/chrome/src/ui/locales/zh.js | 4 + src/chrome/src/ui/settings.html | 25 +++ src/chrome/src/ui/settings.js | 27 +++- src/firefox/src/agent/agent.js | 2 + src/firefox/src/config-transfer.js | 2 + src/firefox/src/download-directory.js | 62 ++++++++ src/firefox/src/network/network-tools.js | 19 ++- src/firefox/src/run-capture.js | 3 + src/firefox/src/ui/locales/ar.js | 4 + src/firefox/src/ui/locales/en.js | 4 + src/firefox/src/ui/locales/es.js | 4 + src/firefox/src/ui/locales/fr.js | 4 + src/firefox/src/ui/locales/he.js | 4 + src/firefox/src/ui/locales/id.js | 4 + src/firefox/src/ui/locales/ja.js | 4 + src/firefox/src/ui/locales/ko.js | 4 + src/firefox/src/ui/locales/ms.js | 4 + src/firefox/src/ui/locales/pl.js | 4 + src/firefox/src/ui/locales/ru.js | 4 + src/firefox/src/ui/locales/th.js | 4 + src/firefox/src/ui/locales/tl.js | 4 + src/firefox/src/ui/locales/tr.js | 4 + src/firefox/src/ui/locales/uk.js | 4 + src/firefox/src/ui/locales/zh.js | 4 + src/firefox/src/ui/settings.html | 25 +++ src/firefox/src/ui/settings.js | 27 +++- test/run.js | 184 +++++++++++++++++++++++ 45 files changed, 570 insertions(+), 7 deletions(-) create mode 100644 src/chrome/src/download-directory.js create mode 100644 src/firefox/src/download-directory.js diff --git a/src/chrome/src/background.js b/src/chrome/src/background.js index 3d5fa22f..e0ef16a1 100644 --- a/src/chrome/src/background.js +++ b/src/chrome/src/background.js @@ -61,6 +61,7 @@ import { createConfigExport, parseConfigImport, } from './config-transfer.js'; +import { installDownloadDirectoryRouting } from './download-directory.js'; /** * WebBrain Service Worker (Background Script) @@ -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, diff --git a/src/chrome/src/config-transfer.js b/src/chrome/src/config-transfer.js index 29ba9002..5d351803 100644 --- a/src/chrome/src/config-transfer.js +++ b/src/chrome/src/config-transfer.js @@ -36,6 +36,7 @@ export const DEFAULT_CONFIG_SETTINGS = Object.freeze({ planBeforeAct: true, planReviewMode: 'confidence', planReviewConfidenceThreshold: 75, + downloadDirectory: '', notifySound: true, completionConfetti: true, tracingEnabled: false, @@ -109,6 +110,7 @@ const STRING_KEYS = new Set([ 'autoScreenshot', 'planBeforeActMode', 'planReviewMode', + 'downloadDirectory', 'providerFilter', 'activeProvider', 'profileText', diff --git a/src/chrome/src/download-directory.js b/src/chrome/src/download-directory.js new file mode 100644 index 00000000..911b4670 --- /dev/null +++ b/src/chrome/src/download-directory.js @@ -0,0 +1,69 @@ +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 ''; + + const basename = String(filename ?? '') + .replace(/\\/g, '/') + .split('/') + .pop() + .replace(/[\u0000-\u001f\u007f]/g, '') + .trim(); + if (!basename || basename === '.' || basename === '..') return ''; + 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; +} diff --git a/src/chrome/src/ui/locales/ar.js b/src/chrome/src/ui/locales/ar.js index 4220912a..842ec54f 100644 --- a/src/chrome/src/ui/locales/ar.js +++ b/src/chrome/src/ui/locales/ar.js @@ -370,6 +370,10 @@ export default { "st.display.theme.system": "النظام", "st.display.theme.light": "فاتح", "st.display.theme.dark": "داكن", + "st.display.download_directory.label": "دليل التنزيل", + "st.display.download_directory.desc": "مجلد فرعي اختياري داخل مجلد التنزيلات في المتصفح أو النظام. اتركه فارغًا لاستخدام الإعداد الافتراضي للنظام.", + "st.display.download_directory.placeholder": "الإعداد الافتراضي للنظام", + "st.display.download_directory.error": "استخدم مجلدًا نسبيًا مثل WebBrain أو Work/WebBrain. لا يُسمح بالمسارات المطلقة أو “..”.", "st.display.strict_secret.label": "المعالجة الصارمة للأسرار", "st.display.strict_secret.desc": "رفض اقتباس بيانات الاعتماد (كلمات المرور، مفاتيح API، الرموز، رموز OTP) في الملخّصات أو نص المساعد — حتى عندما تطلبها صراحةً. مفيد إذا كنت تشارك ملفات التتبّع أو تشارك شاشتك بانتظام. مُعطّل افتراضيًا: يعمل WebBrain داخل متصفّحك الخاص، لذا يُظهر لك الوكيل افتراضيًا القيم التي تطلبها ويُبقي ملخّصات `done` مرتّبة فقط.", "st.display.request_timeout.label": "مهلة طلب LLM", diff --git a/src/chrome/src/ui/locales/en.js b/src/chrome/src/ui/locales/en.js index 5d8840c4..9469385d 100644 --- a/src/chrome/src/ui/locales/en.js +++ b/src/chrome/src/ui/locales/en.js @@ -392,6 +392,10 @@ export default { 'st.display.theme.system': 'System', 'st.display.theme.light': 'Light', 'st.display.theme.dark': 'Dark', + 'st.display.download_directory.label': 'Download directory', + 'st.display.download_directory.desc': 'Optional subfolder inside your browser or system Downloads directory. Leave blank to use the system default.', + 'st.display.download_directory.placeholder': 'System default', + 'st.display.download_directory.error': 'Use a relative folder such as WebBrain or Work/WebBrain. Absolute paths and “..” are not allowed.', 'st.display.search.placeholder': 'Search General settings', 'st.display.search.empty': 'No General settings match.', 'st.display.advanced': 'Advanced', diff --git a/src/chrome/src/ui/locales/es.js b/src/chrome/src/ui/locales/es.js index f03ab878..a2c2b131 100644 --- a/src/chrome/src/ui/locales/es.js +++ b/src/chrome/src/ui/locales/es.js @@ -370,6 +370,10 @@ export default { "st.display.theme.system": "Sistema", "st.display.theme.light": "Claro", "st.display.theme.dark": "Oscuro", + "st.display.download_directory.label": "Directorio de descargas", + "st.display.download_directory.desc": "Subcarpeta opcional dentro del directorio de descargas del navegador o del sistema. Déjala en blanco para usar el valor predeterminado del sistema.", + "st.display.download_directory.placeholder": "Predeterminado del sistema", + "st.display.download_directory.error": "Usa una carpeta relativa como WebBrain o Work/WebBrain. No se permiten rutas absolutas ni “..”.", "st.display.strict_secret.label": "Manejo estricto de secretos", "st.display.strict_secret.desc": "Se niega a citar credenciales (contraseñas, claves de API, tokens, OTPs) en resúmenes o texto del asistente, incluso cuando se las pidas explícitamente. Útil si compartes a menudo archivos de traza o tu pantalla. Desactivado por defecto: WebBrain se ejecuta en tu propio navegador, así que por defecto el agente te muestra los valores que pides y solo mantiene ordenados los resúmenes de `done`.", "st.display.request_timeout.label": "Tiempo de espera de la petición al LLM", diff --git a/src/chrome/src/ui/locales/fr.js b/src/chrome/src/ui/locales/fr.js index 2105a810..8ebe2aaa 100644 --- a/src/chrome/src/ui/locales/fr.js +++ b/src/chrome/src/ui/locales/fr.js @@ -370,6 +370,10 @@ export default { "st.display.theme.system": "Système", "st.display.theme.light": "Clair", "st.display.theme.dark": "Sombre", + "st.display.download_directory.label": "Dossier de téléchargement", + "st.display.download_directory.desc": "Sous-dossier facultatif dans le dossier Téléchargements du navigateur ou du système. Laissez vide pour utiliser la valeur par défaut du système.", + "st.display.download_directory.placeholder": "Valeur par défaut du système", + "st.display.download_directory.error": "Utilisez un dossier relatif comme WebBrain ou Work/WebBrain. Les chemins absolus et « .. » ne sont pas autorisés.", "st.display.strict_secret.label": "Gestion stricte des secrets", "st.display.strict_secret.desc": "Refuse de citer les identifiants (mots de passe, clés d'API, jetons, OTPs) dans les résumés ou le texte de l'assistant — même lorsque vous le demandez explicitement. Utile si vous partagez régulièrement des fichiers de trace ou votre écran. Désactivé par défaut : webbrain s'exécute dans votre propre navigateur, donc par défaut l'agent vous montre les valeurs que vous demandez et se contente de garder les résumés `done` propres.", "st.display.request_timeout.label": "Délai d'attente des requêtes LLM", diff --git a/src/chrome/src/ui/locales/he.js b/src/chrome/src/ui/locales/he.js index 068b0238..c3e1da3f 100644 --- a/src/chrome/src/ui/locales/he.js +++ b/src/chrome/src/ui/locales/he.js @@ -365,6 +365,10 @@ export default { "st.display.theme.system": "מערכת", "st.display.theme.light": "אוֹר", "st.display.theme.dark": "כֵּהֶה", + "st.display.download_directory.label": "תיקיית הורדות", + "st.display.download_directory.desc": "תיקיית משנה אופציונלית בתוך תיקיית ההורדות של הדפדפן או המערכת. השאירו ריק כדי להשתמש בברירת המחדל של המערכת.", + "st.display.download_directory.placeholder": "ברירת המחדל של המערכת", + "st.display.download_directory.error": "השתמשו בתיקייה יחסית כמו WebBrain או Work/WebBrain. נתיבים מוחלטים ו-”..“ אינם מותרים.", "st.display.search.placeholder": "חפש בהגדרות כלליות", "st.display.search.empty": "אין הגדרות כלליות תואמות.", "st.display.advanced": "מִתקַדֵם", diff --git a/src/chrome/src/ui/locales/id.js b/src/chrome/src/ui/locales/id.js index 10230896..159e0b3d 100644 --- a/src/chrome/src/ui/locales/id.js +++ b/src/chrome/src/ui/locales/id.js @@ -370,6 +370,10 @@ export default { "st.display.theme.system": "Sistem", "st.display.theme.light": "Terang", "st.display.theme.dark": "Gelap", + "st.display.download_directory.label": "Direktori unduhan", + "st.display.download_directory.desc": "Subfolder opsional di dalam direktori Unduhan browser atau sistem. Biarkan kosong untuk menggunakan pengaturan bawaan sistem.", + "st.display.download_directory.placeholder": "Bawaan sistem", + "st.display.download_directory.error": "Gunakan folder relatif seperti WebBrain atau Work/WebBrain. Jalur absolut dan “..” tidak diizinkan.", "st.display.strict_secret.label": "Penanganan rahasia ketat", "st.display.strict_secret.desc": "Menolak mengutip kredensial (kata sandi, kunci API, token, OTP) dalam ringkasan atau teks asisten — bahkan saat Anda memintanya secara eksplisit. Berguna jika Anda kerap berbagi berkas jejak atau berbagi layar. Nonaktif secara bawaan: webbrain berjalan di peramban Anda sendiri, jadi secara bawaan agen menampilkan nilai yang Anda minta dan hanya menjaga ringkasan `done` tetap rapi.", "st.display.request_timeout.label": "Batas waktu permintaan LLM", diff --git a/src/chrome/src/ui/locales/ja.js b/src/chrome/src/ui/locales/ja.js index d678b0f8..c9dea88c 100644 --- a/src/chrome/src/ui/locales/ja.js +++ b/src/chrome/src/ui/locales/ja.js @@ -370,6 +370,10 @@ export default { "st.display.theme.system": "システム", "st.display.theme.light": "ライト", "st.display.theme.dark": "ダーク", + "st.display.download_directory.label": "ダウンロード先", + "st.display.download_directory.desc": "ブラウザーまたはシステムのダウンロードフォルダー内に作成する任意のサブフォルダーです。空欄の場合はシステムの既定値を使用します。", + "st.display.download_directory.placeholder": "システムの既定値", + "st.display.download_directory.error": "WebBrain や Work/WebBrain のような相対フォルダーを指定してください。絶対パスと「..」は使用できません。", "st.display.strict_secret.label": "資格情報の厳格な取り扱い", "st.display.strict_secret.desc": "要約やアシスタントの文章の中で資格情報(パスワード、API キー、トークン、OTP)を引用することを、たとえ明示的に求めた場合でも拒否します。トレースファイルを共有したり画面を共有したりすることが多い場合に便利です。デフォルトはオフ: WebBrain はあなた自身のブラウザ内で動くため、既定ではエージェントは求めた値を表示し、`done` の要約だけをすっきりと保ちます。", "st.display.request_timeout.label": "LLM リクエストのタイムアウト", diff --git a/src/chrome/src/ui/locales/ko.js b/src/chrome/src/ui/locales/ko.js index 4758ed62..579613e9 100644 --- a/src/chrome/src/ui/locales/ko.js +++ b/src/chrome/src/ui/locales/ko.js @@ -370,6 +370,10 @@ export default { "st.display.theme.system": "시스템", "st.display.theme.light": "라이트", "st.display.theme.dark": "다크", + "st.display.download_directory.label": "다운로드 디렉터리", + "st.display.download_directory.desc": "브라우저 또는 시스템 다운로드 디렉터리 안의 선택적 하위 폴더입니다. 비워 두면 시스템 기본값을 사용합니다.", + "st.display.download_directory.placeholder": "시스템 기본값", + "st.display.download_directory.error": "WebBrain 또는 Work/WebBrain 같은 상대 폴더를 사용하세요. 절대 경로와 “..”는 허용되지 않습니다.", "st.display.strict_secret.label": "엄격한 비밀 정보 처리", "st.display.strict_secret.desc": "요약이나 어시스턴트 텍스트에 자격 증명(비밀번호, API 키, 토큰, OTP)을 인용하기를 거부합니다 — 명시적으로 요청하더라도 마찬가지입니다. 트레이스 파일을 자주 공유하거나 화면을 공유한다면 유용합니다. 기본값은 꺼짐: WebBrain은 사용자 본인의 브라우저에서 실행되므로, 기본적으로 에이전트는 요청한 값을 보여 주고 `done` 요약만 깔끔하게 유지합니다.", "st.display.request_timeout.label": "LLM 요청 시간 제한", diff --git a/src/chrome/src/ui/locales/ms.js b/src/chrome/src/ui/locales/ms.js index c3b7fd3c..82c31421 100644 --- a/src/chrome/src/ui/locales/ms.js +++ b/src/chrome/src/ui/locales/ms.js @@ -370,6 +370,10 @@ export default { "st.display.theme.system": "Sistem", "st.display.theme.light": "Cerah", "st.display.theme.dark": "Gelap", + "st.display.download_directory.label": "Direktori muat turun", + "st.display.download_directory.desc": "Subfolder pilihan dalam direktori Muat Turun pelayar atau sistem. Biarkan kosong untuk menggunakan lalai sistem.", + "st.display.download_directory.placeholder": "Lalai sistem", + "st.display.download_directory.error": "Gunakan folder relatif seperti WebBrain atau Work/WebBrain. Laluan mutlak dan “..” tidak dibenarkan.", "st.display.strict_secret.label": "Pengendalian rahsia ketat", "st.display.strict_secret.desc": "Enggan memetik kelayakan (kata laluan, kunci API, token, OTP) dalam ringkasan atau teks pembantu — walaupun apabila anda secara jelas memintanya. Berguna jika anda kerap berkongsi fail jejak atau berkongsi skrin. Dimatikan secara lalai: webbrain berjalan dalam pelayar anda sendiri, jadi secara lalai ejen menunjukkan kepada anda nilai yang anda minta dan hanya memastikan ringkasan `done` kemas.", "st.display.request_timeout.label": "Tamat masa permintaan LLM", diff --git a/src/chrome/src/ui/locales/pl.js b/src/chrome/src/ui/locales/pl.js index 00335318..403ed73d 100644 --- a/src/chrome/src/ui/locales/pl.js +++ b/src/chrome/src/ui/locales/pl.js @@ -301,6 +301,10 @@ export default { 'st.display.theme.system': 'Systemowy', 'st.display.theme.light': 'Jasny', 'st.display.theme.dark': 'Ciemny', + 'st.display.download_directory.label': 'Katalog pobierania', + 'st.display.download_directory.desc': 'Opcjonalny podfolder w katalogu Pobrane przeglądarki lub systemu. Pozostaw puste, aby użyć ustawienia domyślnego systemu.', + 'st.display.download_directory.placeholder': 'Domyślne systemu', + 'st.display.download_directory.error': 'Użyj folderu względnego, np. WebBrain lub Work/WebBrain. Ścieżki bezwzględne i „..” są niedozwolone.', 'st.display.verbose.label': 'Tryb szczegółowy', 'st.display.verbose.desc': 'Pokazuj pełne szczegóły wywołań narzędzi, argumenty i surowe wyniki JSON. Domyślnie wyłączony dla czystszego czatu.', 'st.display.selection_shortcut.label': 'Skrót zaznaczenia', diff --git a/src/chrome/src/ui/locales/ru.js b/src/chrome/src/ui/locales/ru.js index 495bad1c..93ea2bc4 100644 --- a/src/chrome/src/ui/locales/ru.js +++ b/src/chrome/src/ui/locales/ru.js @@ -370,6 +370,10 @@ export default { "st.display.theme.system": "Системная", "st.display.theme.light": "Светлая", "st.display.theme.dark": "Тёмная", + "st.display.download_directory.label": "Каталог загрузок", + "st.display.download_directory.desc": "Необязательная подпапка в каталоге загрузок браузера или системы. Оставьте пустым, чтобы использовать системное значение по умолчанию.", + "st.display.download_directory.placeholder": "Системное значение", + "st.display.download_directory.error": "Используйте относительную папку, например WebBrain или Work/WebBrain. Абсолютные пути и «..» не допускаются.", "st.display.strict_secret.label": "Строгая обработка секретов", "st.display.strict_secret.desc": "Отказываться цитировать учётные данные (пароли, API-ключи, токены, OTP) в сводках или тексте ассистента — даже когда вы явно об этом просите. Полезно, если вы регулярно делитесь файлами трасс или демонстрируете экран. По умолчанию выключено: webbrain работает в вашем собственном браузере, поэтому по умолчанию агент показывает запрошенные вами значения и просто держит сводки `done` аккуратными.", "st.display.request_timeout.label": "Тайм-аут запроса к LLM", diff --git a/src/chrome/src/ui/locales/th.js b/src/chrome/src/ui/locales/th.js index 8030e799..e72f6522 100644 --- a/src/chrome/src/ui/locales/th.js +++ b/src/chrome/src/ui/locales/th.js @@ -370,6 +370,10 @@ export default { "st.display.theme.system": "ระบบ", "st.display.theme.light": "สว่าง", "st.display.theme.dark": "มืด", + "st.display.download_directory.label": "ไดเรกทอรีดาวน์โหลด", + "st.display.download_directory.desc": "โฟลเดอร์ย่อยภายในไดเรกทอรีดาวน์โหลดของเบราว์เซอร์หรือระบบ เว้นว่างไว้เพื่อใช้ค่าเริ่มต้นของระบบ", + "st.display.download_directory.placeholder": "ค่าเริ่มต้นของระบบ", + "st.display.download_directory.error": "ใช้โฟลเดอร์แบบสัมพัทธ์ เช่น WebBrain หรือ Work/WebBrain ไม่อนุญาตพาธแบบสัมบูรณ์และ “..”", "st.display.strict_secret.label": "การจัดการความลับอย่างเข้มงวด", "st.display.strict_secret.desc": "ปฏิเสธการอ้างอิงข้อมูลรับรอง (รหัสผ่าน คีย์ API โทเค็น OTP) ในบทสรุปหรือข้อความของผู้ช่วย — แม้คุณจะขอโดยตรง มีประโยชน์หากคุณแชร์ไฟล์ trace หรือแชร์หน้าจอเป็นประจำ ปิดเป็นค่าเริ่มต้น: WebBrain ทำงานในเบราว์เซอร์ของคุณเอง ดังนั้นโดยค่าเริ่มต้นเอเจนต์จะแสดงค่าที่คุณขอ และเพียงทำให้บทสรุป `done` เป็นระเบียบ", "st.display.request_timeout.label": "เวลารอคำขอ LLM", diff --git a/src/chrome/src/ui/locales/tl.js b/src/chrome/src/ui/locales/tl.js index 215d76a4..d1b41449 100644 --- a/src/chrome/src/ui/locales/tl.js +++ b/src/chrome/src/ui/locales/tl.js @@ -370,6 +370,10 @@ export default { "st.display.theme.system": "System", "st.display.theme.light": "Maliwanag", "st.display.theme.dark": "Madilim", + "st.display.download_directory.label": "Direktoryo ng download", + "st.display.download_directory.desc": "Opsyonal na subfolder sa loob ng Downloads directory ng browser o system. Iwang blangko upang gamitin ang default ng system.", + "st.display.download_directory.placeholder": "Default ng system", + "st.display.download_directory.error": "Gumamit ng relative na folder tulad ng WebBrain o Work/WebBrain. Hindi pinapayagan ang absolute path at “..”.", "st.display.strict_secret.label": "Mahigpit na paghawak ng lihim", "st.display.strict_secret.desc": "Tanggihang i-quote ang mga kredensyal (password, API key, token, OTPs) sa mga summary o teksto ng assistant — kahit hayagan mong hilingin ang mga ito. Mainam kung madalas kang magbahagi ng trace files o screen-share. Naka-off bilang default: tumatakbo ang webbrain sa sarili mong browser, kaya bilang default ay ipinapakita sa iyo ng ahente ang mga value na hiniling mo at pinapanatili lang na malinis ang mga `done` na summary.", "st.display.request_timeout.label": "Timeout ng LLM request", diff --git a/src/chrome/src/ui/locales/tr.js b/src/chrome/src/ui/locales/tr.js index 17c72fce..4d8d7387 100644 --- a/src/chrome/src/ui/locales/tr.js +++ b/src/chrome/src/ui/locales/tr.js @@ -409,6 +409,10 @@ export default { "st.display.theme.system": "Sistem", "st.display.theme.light": "Açık", "st.display.theme.dark": "Koyu", + "st.display.download_directory.label": "İndirme dizini", + "st.display.download_directory.desc": "Tarayıcının veya sistemin İndirilenler dizini içinde isteğe bağlı bir alt klasör. Sistem varsayılanını kullanmak için boş bırakın.", + "st.display.download_directory.placeholder": "Sistem varsayılanı", + "st.display.download_directory.error": "WebBrain veya Work/WebBrain gibi göreli bir klasör kullanın. Mutlak yollara ve “..” kullanımına izin verilmez.", "st.display.strict_secret.label": "Katı gizli bilgi işleme", "st.display.strict_secret.desc": "Özellikle istesen bile, özetlerde veya asistan metninde kimlik bilgilerini (parolalar, API anahtarları, jetonlar, OTP'ler) aktarmayı reddet. Düzenli olarak iz dosyaları paylaşıyorsan veya ekran paylaşımı yapıyorsan kullanışlıdır. Varsayılan olarak kapalı: WebBrain kendi tarayıcında çalışır, bu yüzden varsayılan olarak aracı istediğin değerleri sana gösterir ve yalnızca `done` özetlerini derli toplu tutar.", "st.display.request_timeout.label": "LLM istek zaman aşımı", diff --git a/src/chrome/src/ui/locales/uk.js b/src/chrome/src/ui/locales/uk.js index 7ed5cc67..519401da 100644 --- a/src/chrome/src/ui/locales/uk.js +++ b/src/chrome/src/ui/locales/uk.js @@ -370,6 +370,10 @@ export default { "st.display.theme.system": "Системна", "st.display.theme.light": "Світла", "st.display.theme.dark": "Темна", + "st.display.download_directory.label": "Каталог завантажень", + "st.display.download_directory.desc": "Необов’язкова підпапка в каталозі завантажень браузера або системи. Залиште порожнім, щоб використовувати системне значення за замовчуванням.", + "st.display.download_directory.placeholder": "Системне значення", + "st.display.download_directory.error": "Використовуйте відносну папку, наприклад WebBrain або Work/WebBrain. Абсолютні шляхи та «..» заборонені.", "st.display.strict_secret.label": "Суворе поводження з секретами", "st.display.strict_secret.desc": "Відмовлятися наводити облікові дані (паролі, API-ключі, токени, OTP) у підсумках чи тексті асистента — навіть коли ви явно про них просите. Корисно, якщо ви регулярно ділитеся файлами трас або демонструєте екран. Вимкнено за замовчуванням: webbrain працює у вашому власному браузері, тож за замовчуванням агент показує вам значення, про які ви просите, і лише тримає підсумки `done` охайними.", "st.display.request_timeout.label": "Тайм-аут запиту до LLM", diff --git a/src/chrome/src/ui/locales/zh.js b/src/chrome/src/ui/locales/zh.js index 94b491c1..83c5d26f 100644 --- a/src/chrome/src/ui/locales/zh.js +++ b/src/chrome/src/ui/locales/zh.js @@ -370,6 +370,10 @@ export default { "st.display.theme.system": "系统", "st.display.theme.light": "浅色", "st.display.theme.dark": "深色", + "st.display.download_directory.label": "下载目录", + "st.display.download_directory.desc": "浏览器或系统“下载”目录中的可选子文件夹。留空即可使用系统默认值。", + "st.display.download_directory.placeholder": "系统默认值", + "st.display.download_directory.error": "请使用 WebBrain 或 Work/WebBrain 这样的相对文件夹。不允许使用绝对路径或“..”。", "st.display.strict_secret.label": "严格的机密处理", "st.display.strict_secret.desc": "拒绝在总结或助手文本中引用凭据(密码、API 密钥、令牌、OTP)—— 即使你明确要求也是如此。如果你经常分享轨迹文件或共享屏幕,这会很有用。默认关闭:webbrain 运行在你自己的浏览器中,因此默认情况下代理会向你展示你索取的值,只是把 `done` 总结保持整洁。", "st.display.request_timeout.label": "LLM 请求超时", diff --git a/src/chrome/src/ui/settings.html b/src/chrome/src/ui/settings.html index fbe40a2e..8b747e97 100644 --- a/src/chrome/src/ui/settings.html +++ b/src/chrome/src/ui/settings.html @@ -697,6 +697,23 @@ color: var(--text2); line-height: 1.4; } + .setting-input { + flex: 0 1 260px; + min-width: 120px; + max-width: 42%; + background: var(--bg); + color: var(--text); + border: 1px solid var(--border); + border-radius: 6px; + padding: 7px 10px; + font: inherit; + font-size: 12px; + outline: none; + } + .setting-input:focus { + border-color: var(--accent); + box-shadow: 0 0 0 2px rgba(108,99,255,0.22); + } .skill-source { max-width: 340px; overflow: hidden; @@ -1031,6 +1048,14 @@

+
+
+
+
+
+ +
diff --git a/src/chrome/src/ui/settings.js b/src/chrome/src/ui/settings.js index 4e49a0f8..f29f2b01 100644 --- a/src/chrome/src/ui/settings.js +++ b/src/chrome/src/ui/settings.js @@ -32,6 +32,10 @@ import { parseProviderExtraBodyJson, shouldUseOpenAIResponsesApi, } from '../providers/provider-compatibility.js'; +import { + DOWNLOAD_DIRECTORY_STORAGE_KEY, + normalizeDownloadDirectory, +} from '../download-directory.js'; // Version shown in the subtitle. Kept here so it only needs one update per // release; the subtitle string itself is translated. @@ -145,6 +149,7 @@ const btnClearCaptcha = document.getElementById('btn-clear-captcha'); const captchaTestResult = document.getElementById('test-captcha'); const languageSelect = document.getElementById('select-language'); const themeSelect = document.getElementById('select-theme'); +const downloadDirectoryInput = document.getElementById('input-download-directory'); const subtitleEl = document.getElementById('subtitle'); // --- Appearance / theme --- @@ -378,7 +383,7 @@ async function init() { chrome.storage.local.remove(['authToken', 'authEmail', 'authDefaultModel']).catch(() => {}); // Load display settings - const stored = await chrome.storage.local.get(['verboseMode', 'selectionShortcutEnabled', 'helpImproveWebBrain', 'screenshotFallback', 'maxAgentSteps', 'autoScreenshot', 'useSiteAdapters', 'voiceInputEnabled', 'apiMutationObserverEnabled', 'planBeforeActMode', 'planBeforeAct', 'planReviewMode', 'planReviewConfidenceThreshold', 'notifySound', 'completionConfetti', 'tracingEnabled', 'strictSecretMode', 'agentAllowLocalNetwork', 'scheduledTasksEnabled', 'scheduledRequireConsequentialConfirmation', 'providerFilter', 'requestTimeoutMs', 'clarifyTimeoutSec', 'clarifyTimeoutSemanticsV2', 'costAllowanceSessionUsd', 'costAllowanceTotalUsd', 'cloudCostSpentUsd', 'screenshotRedaction']); + const stored = await chrome.storage.local.get(['verboseMode', 'selectionShortcutEnabled', 'helpImproveWebBrain', 'screenshotFallback', 'maxAgentSteps', 'autoScreenshot', 'useSiteAdapters', 'voiceInputEnabled', 'apiMutationObserverEnabled', 'planBeforeActMode', 'planBeforeAct', 'planReviewMode', 'planReviewConfidenceThreshold', DOWNLOAD_DIRECTORY_STORAGE_KEY, 'notifySound', 'completionConfetti', 'tracingEnabled', 'strictSecretMode', 'agentAllowLocalNetwork', 'scheduledTasksEnabled', 'scheduledRequireConsequentialConfirmation', 'providerFilter', 'requestTimeoutMs', 'clarifyTimeoutSec', 'clarifyTimeoutSemanticsV2', 'costAllowanceSessionUsd', 'costAllowanceTotalUsd', 'cloudCostSpentUsd', 'screenshotRedaction']); if (typeof stored.providerFilter === 'string' && ['all','local','cloud','router'].includes(stored.providerFilter)) { providerFilter = stored.providerFilter; } @@ -436,6 +441,9 @@ async function init() { planReviewConfidenceRange.value = normalizePlanReviewConfidenceThreshold(stored); updatePlanReviewConfidenceUI(); } + if (downloadDirectoryInput) { + downloadDirectoryInput.value = normalizeDownloadDirectory(stored[DOWNLOAD_DIRECTORY_STORAGE_KEY]); + } notifySoundToggle.checked = stored.notifySound ?? true; // on by default completionConfettiToggle.checked = stored.completionConfetti ?? true; // on by default tracingToggle.checked = stored.tracingEnabled === true; // off by default @@ -947,6 +955,23 @@ if (globalThis.chrome?.storage?.onChanged) { // --- Display Settings --- +downloadDirectoryInput?.addEventListener('input', () => { + downloadDirectoryInput.setCustomValidity(''); +}); + +downloadDirectoryInput?.addEventListener('change', async () => { + const raw = String(downloadDirectoryInput.value || '').trim(); + const directory = normalizeDownloadDirectory(raw); + if (raw && !directory) { + downloadDirectoryInput.setCustomValidity(t('st.display.download_directory.error')); + downloadDirectoryInput.reportValidity(); + return; + } + downloadDirectoryInput.setCustomValidity(''); + downloadDirectoryInput.value = directory; + await chrome.storage.local.set({ [DOWNLOAD_DIRECTORY_STORAGE_KEY]: directory }).catch(() => {}); +}); + verboseToggle.addEventListener('change', async () => { await chrome.storage.local.set({ verboseMode: verboseToggle.checked }).catch(() => {}); }); diff --git a/src/firefox/src/agent/agent.js b/src/firefox/src/agent/agent.js index 2594951e..63b4ac3a 100644 --- a/src/firefox/src/agent/agent.js +++ b/src/firefox/src/agent/agent.js @@ -48,6 +48,7 @@ import { USER_MEMORY_DEFAULT_MAX_PROMPT_CHARS, formatUserMemoryPrompt, normalize import { mergeRedactionFrameRegions, mapRegionsToImage, pixelateDataUrl } from './screenshot-redaction.js'; import { buildTrustedRuntimeContext, stripTrustedRuntimeContext } from './runtime-context.js'; import { firefoxHostPermissionFailure, firefoxRestrictedDomainFailure } from '../firefox-restricted-domains.js'; +import { filenameInConfiguredDownloadDirectory } from '../download-directory.js'; const DEFAULT_CLOUD_COST_ALLOWANCE_USD = 10; // Product default: auto-approve plans at 75% confidence to reduce review stops. @@ -3404,6 +3405,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d filename = filename.split('/').pop().split('\\').pop(); filename = filename.replace(/\.(jpe?g|webp)$/i, '.png'); if (!/\.png$/i.test(filename)) filename += '.png'; + filename = await filenameInConfiguredDownloadDirectory(browser, filename, crop.dataUrl); const downloadId = await browser.downloads.download({ url: crop.dataUrl, filename, saveAs: false }); return { diff --git a/src/firefox/src/config-transfer.js b/src/firefox/src/config-transfer.js index 29ba9002..5d351803 100644 --- a/src/firefox/src/config-transfer.js +++ b/src/firefox/src/config-transfer.js @@ -36,6 +36,7 @@ export const DEFAULT_CONFIG_SETTINGS = Object.freeze({ planBeforeAct: true, planReviewMode: 'confidence', planReviewConfidenceThreshold: 75, + downloadDirectory: '', notifySound: true, completionConfetti: true, tracingEnabled: false, @@ -109,6 +110,7 @@ const STRING_KEYS = new Set([ 'autoScreenshot', 'planBeforeActMode', 'planReviewMode', + 'downloadDirectory', 'providerFilter', 'activeProvider', 'profileText', diff --git a/src/firefox/src/download-directory.js b/src/firefox/src/download-directory.js new file mode 100644 index 00000000..9ccecfe9 --- /dev/null +++ b/src/firefox/src/download-directory.js @@ -0,0 +1,62 @@ +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 ''; + + const basename = String(filename ?? '') + .replace(/\\/g, '/') + .split('/') + .pop() + .replace(/[\u0000-\u001f\u007f]/g, '') + .trim(); + if (!basename || basename === '.' || basename === '..') return ''; + return `${normalizedDirectory}/${basename}`; +} + +function filenameFromDownloadUrl(value) { + try { + const url = new URL(String(value || '')); + if (url.protocol !== 'http:' && url.protocol !== 'https:') return ''; + return decodeURIComponent(url.pathname.split('/').filter(Boolean).pop() || ''); + } catch { + return ''; + } +} + +/** + * Firefox does not expose downloads.onDeterminingFilename. Its download call + * sites therefore resolve the configured relative path before starting each + * download. Missing storage keeps tests and older runtimes on the original + * filename, while a configured directory also supplies a URL-derived fallback + * for downloads that did not request a name. + */ +export async function filenameInConfiguredDownloadDirectory(api, filename, url = '') { + const original = filename || undefined; + if (!api?.storage?.local?.get) return original; + try { + const stored = await api.storage.local.get(DOWNLOAD_DIRECTORY_STORAGE_KEY); + const directory = normalizeDownloadDirectory(stored?.[DOWNLOAD_DIRECTORY_STORAGE_KEY]); + if (!directory) return original; + const candidate = original || filenameFromDownloadUrl(url) || 'download'; + return filenameInDownloadDirectory(directory, candidate) || `${directory}/download`; + } catch { + return original; + } +} diff --git a/src/firefox/src/network/network-tools.js b/src/firefox/src/network/network-tools.js index 7262c62e..50e0604b 100644 --- a/src/firefox/src/network/network-tools.js +++ b/src/firefox/src/network/network-tools.js @@ -1,4 +1,5 @@ import { firefoxRestrictedDomainFailure } from '../firefox-restricted-domains.js'; +import { filenameInConfiguredDownloadDirectory } from '../download-directory.js'; /** * Network & download tools for the WebBrain agent (Firefox). @@ -908,7 +909,8 @@ async function downloadSkillFile(url, filename, waitMs = 60000) { || filenameFromContentDisposition(file.contentDisposition); const opts = { url: staged ? staged.localUrl : file.dataUrl, conflictAction: 'uniquify' }; const safeName = safeDownloadFilename(filename) || responseFilename || defaultSkillDownloadFilename(contentType); - if (safeName) opts.filename = safeName; + const configuredName = await filenameInConfiguredDownloadDirectory(browser, safeName, url); + if (configuredName) opts.filename = configuredName; let downloadId; try { downloadId = await browser.downloads.download(opts); @@ -2097,9 +2099,10 @@ export async function downloadResourceFromPage(tabId, args = {}) { }; } + const downloadFilename = await filenameInConfiguredDownloadDirectory(browser, filename, r.url); const downloadId = await browser.downloads.download({ url: r.url, - filename: filename || undefined, + filename: downloadFilename, conflictAction: 'uniquify', }); return { @@ -2153,6 +2156,7 @@ export async function downloadFiles(args = {}) { return { success: false, error: `Too many URLs (max ${DOWNLOAD_BATCH_MAX})` }; } + const singleFilename = (urls.length === 1 && args.filename) ? args.filename : null; const results = new Array(urls.length); let idx = 0; @@ -2162,9 +2166,14 @@ export async function downloadFiles(args = {}) { if (i >= urls.length) return; const url = urls[i]; try { - const downloadId = await browser.downloads.download({ - url, conflictAction: 'uniquify', - }); + const filename = await filenameInConfiguredDownloadDirectory( + browser, + i === 0 ? singleFilename : null, + url, + ); + const opts = { url, conflictAction: 'uniquify' }; + if (filename) opts.filename = filename; + const downloadId = await browser.downloads.download(opts); results[i] = { url, downloadId, success: true }; } catch (e) { results[i] = { url, success: false, error: e.message }; diff --git a/src/firefox/src/run-capture.js b/src/firefox/src/run-capture.js index 87e692e6..8f1c023a 100644 --- a/src/firefox/src/run-capture.js +++ b/src/firefox/src/run-capture.js @@ -1,3 +1,5 @@ +import { filenameInConfiguredDownloadDirectory } from './download-directory.js'; + export const RUN_CAPTURE_START_ERROR_PREFIX = 'Run capture could not start: '; function sanitizeRunCaptureSaveAs(value) { @@ -60,6 +62,7 @@ export async function captureAndSaveRunScreenshot(api, tabId, filename) { throw new Error('The run tab could not be activated for capture.'); } const dataUrl = await api.tabs.captureVisibleTab(tab.windowId, { format: 'png' }); + filename = await filenameInConfiguredDownloadDirectory(api, filename, dataUrl); const downloadId = await api.downloads.download({ url: dataUrl, filename, diff --git a/src/firefox/src/ui/locales/ar.js b/src/firefox/src/ui/locales/ar.js index c3a58736..2baf2044 100644 --- a/src/firefox/src/ui/locales/ar.js +++ b/src/firefox/src/ui/locales/ar.js @@ -361,6 +361,10 @@ export default { "st.display.theme.system": "النظام", "st.display.theme.light": "فاتح", "st.display.theme.dark": "داكن", + "st.display.download_directory.label": "دليل التنزيل", + "st.display.download_directory.desc": "مجلد فرعي اختياري داخل مجلد التنزيلات في المتصفح أو النظام. اتركه فارغًا لاستخدام الإعداد الافتراضي للنظام.", + "st.display.download_directory.placeholder": "الإعداد الافتراضي للنظام", + "st.display.download_directory.error": "استخدم مجلدًا نسبيًا مثل WebBrain أو Work/WebBrain. لا يُسمح بالمسارات المطلقة أو “..”.", "st.display.strict_secret.label": "المعالجة الصارمة للأسرار", "st.display.strict_secret.desc": "رفض اقتباس بيانات الاعتماد (كلمات المرور، مفاتيح API، الرموز، رموز OTP) في الملخّصات أو نص المساعد — حتى عندما تطلبها صراحةً. مفيد إذا كنت تشارك ملفات التتبّع أو تشارك شاشتك بانتظام. مُعطّل افتراضيًا: يعمل WebBrain داخل متصفّحك الخاص، لذا يُظهر لك الوكيل افتراضيًا القيم التي تطلبها ويُبقي ملخّصات `done` مرتّبة فقط.", "st.display.request_timeout.label": "مهلة طلب LLM", diff --git a/src/firefox/src/ui/locales/en.js b/src/firefox/src/ui/locales/en.js index 13e9db4b..72e6ae17 100644 --- a/src/firefox/src/ui/locales/en.js +++ b/src/firefox/src/ui/locales/en.js @@ -390,6 +390,10 @@ export default { 'st.display.theme.system': 'System', 'st.display.theme.light': 'Light', 'st.display.theme.dark': 'Dark', + 'st.display.download_directory.label': 'Download directory', + 'st.display.download_directory.desc': 'Optional subfolder inside your browser or system Downloads directory. Leave blank to use the system default.', + 'st.display.download_directory.placeholder': 'System default', + 'st.display.download_directory.error': 'Use a relative folder such as WebBrain or Work/WebBrain. Absolute paths and “..” are not allowed.', 'st.display.search.placeholder': 'Search General settings', 'st.display.search.empty': 'No General settings match.', 'st.display.advanced': 'Advanced', diff --git a/src/firefox/src/ui/locales/es.js b/src/firefox/src/ui/locales/es.js index 67036a62..89efc430 100644 --- a/src/firefox/src/ui/locales/es.js +++ b/src/firefox/src/ui/locales/es.js @@ -361,6 +361,10 @@ export default { "st.display.theme.system": "Sistema", "st.display.theme.light": "Claro", "st.display.theme.dark": "Oscuro", + "st.display.download_directory.label": "Directorio de descargas", + "st.display.download_directory.desc": "Subcarpeta opcional dentro del directorio de descargas del navegador o del sistema. Déjala en blanco para usar el valor predeterminado del sistema.", + "st.display.download_directory.placeholder": "Predeterminado del sistema", + "st.display.download_directory.error": "Usa una carpeta relativa como WebBrain o Work/WebBrain. No se permiten rutas absolutas ni “..”.", "st.display.strict_secret.label": "Manejo estricto de secretos", "st.display.strict_secret.desc": "Se niega a citar credenciales (contraseñas, claves de API, tokens, OTPs) en resúmenes o texto del asistente, incluso cuando se las pidas explícitamente. Útil si compartes a menudo archivos de traza o tu pantalla. Desactivado por defecto: WebBrain se ejecuta en tu propio navegador, así que por defecto el agente te muestra los valores que pides y solo mantiene ordenados los resúmenes de `done`.", "st.display.request_timeout.label": "Tiempo de espera de la petición al LLM", diff --git a/src/firefox/src/ui/locales/fr.js b/src/firefox/src/ui/locales/fr.js index 74358dbf..90eb99e5 100644 --- a/src/firefox/src/ui/locales/fr.js +++ b/src/firefox/src/ui/locales/fr.js @@ -361,6 +361,10 @@ export default { "st.display.theme.system": "Système", "st.display.theme.light": "Clair", "st.display.theme.dark": "Sombre", + "st.display.download_directory.label": "Dossier de téléchargement", + "st.display.download_directory.desc": "Sous-dossier facultatif dans le dossier Téléchargements du navigateur ou du système. Laissez vide pour utiliser la valeur par défaut du système.", + "st.display.download_directory.placeholder": "Valeur par défaut du système", + "st.display.download_directory.error": "Utilisez un dossier relatif comme WebBrain ou Work/WebBrain. Les chemins absolus et « .. » ne sont pas autorisés.", "st.display.strict_secret.label": "Gestion stricte des secrets", "st.display.strict_secret.desc": "Refuse de citer les identifiants (mots de passe, clés d'API, jetons, OTPs) dans les résumés ou le texte de l'assistant — même lorsque vous le demandez explicitement. Utile si vous partagez régulièrement des fichiers de trace ou votre écran. Désactivé par défaut : webbrain s'exécute dans votre propre navigateur, donc par défaut l'agent vous montre les valeurs que vous demandez et se contente de garder les résumés `done` propres.", "st.display.request_timeout.label": "Délai d'attente des requêtes LLM", diff --git a/src/firefox/src/ui/locales/he.js b/src/firefox/src/ui/locales/he.js index ed793aae..bc0d645b 100644 --- a/src/firefox/src/ui/locales/he.js +++ b/src/firefox/src/ui/locales/he.js @@ -356,6 +356,10 @@ export default { "st.display.theme.system": "מערכת", "st.display.theme.light": "אוֹר", "st.display.theme.dark": "כֵּהֶה", + "st.display.download_directory.label": "תיקיית הורדות", + "st.display.download_directory.desc": "תיקיית משנה אופציונלית בתוך תיקיית ההורדות של הדפדפן או המערכת. השאירו ריק כדי להשתמש בברירת המחדל של המערכת.", + "st.display.download_directory.placeholder": "ברירת המחדל של המערכת", + "st.display.download_directory.error": "השתמשו בתיקייה יחסית כמו WebBrain או Work/WebBrain. נתיבים מוחלטים ו-”..“ אינם מותרים.", "st.display.search.placeholder": "חפש בהגדרות כלליות", "st.display.search.empty": "אין הגדרות כלליות תואמות.", "st.display.advanced": "מִתקַדֵם", diff --git a/src/firefox/src/ui/locales/id.js b/src/firefox/src/ui/locales/id.js index 6b0b686b..4c1b0254 100644 --- a/src/firefox/src/ui/locales/id.js +++ b/src/firefox/src/ui/locales/id.js @@ -361,6 +361,10 @@ export default { "st.display.theme.system": "Sistem", "st.display.theme.light": "Terang", "st.display.theme.dark": "Gelap", + "st.display.download_directory.label": "Direktori unduhan", + "st.display.download_directory.desc": "Subfolder opsional di dalam direktori Unduhan browser atau sistem. Biarkan kosong untuk menggunakan pengaturan bawaan sistem.", + "st.display.download_directory.placeholder": "Bawaan sistem", + "st.display.download_directory.error": "Gunakan folder relatif seperti WebBrain atau Work/WebBrain. Jalur absolut dan “..” tidak diizinkan.", "st.display.strict_secret.label": "Penanganan rahasia ketat", "st.display.strict_secret.desc": "Menolak mengutip kredensial (kata sandi, kunci API, token, OTP) dalam ringkasan atau teks asisten — bahkan saat Anda memintanya secara eksplisit. Berguna jika Anda kerap berbagi berkas jejak atau berbagi layar. Nonaktif secara bawaan: webbrain berjalan di peramban Anda sendiri, jadi secara bawaan agen menampilkan nilai yang Anda minta dan hanya menjaga ringkasan `done` tetap rapi.", "st.display.request_timeout.label": "Batas waktu permintaan LLM", diff --git a/src/firefox/src/ui/locales/ja.js b/src/firefox/src/ui/locales/ja.js index 085fd1f2..4614f0fb 100644 --- a/src/firefox/src/ui/locales/ja.js +++ b/src/firefox/src/ui/locales/ja.js @@ -361,6 +361,10 @@ export default { "st.display.theme.system": "システム", "st.display.theme.light": "ライト", "st.display.theme.dark": "ダーク", + "st.display.download_directory.label": "ダウンロード先", + "st.display.download_directory.desc": "ブラウザーまたはシステムのダウンロードフォルダー内に作成する任意のサブフォルダーです。空欄の場合はシステムの既定値を使用します。", + "st.display.download_directory.placeholder": "システムの既定値", + "st.display.download_directory.error": "WebBrain や Work/WebBrain のような相対フォルダーを指定してください。絶対パスと「..」は使用できません。", "st.display.strict_secret.label": "資格情報の厳格な取り扱い", "st.display.strict_secret.desc": "要約やアシスタントの文章の中で資格情報(パスワード、API キー、トークン、OTP)を引用することを、たとえ明示的に求めた場合でも拒否します。トレースファイルを共有したり画面を共有したりすることが多い場合に便利です。デフォルトはオフ: WebBrain はあなた自身のブラウザ内で動くため、既定ではエージェントは求めた値を表示し、`done` の要約だけをすっきりと保ちます。", "st.display.request_timeout.label": "LLM リクエストのタイムアウト", diff --git a/src/firefox/src/ui/locales/ko.js b/src/firefox/src/ui/locales/ko.js index 768c328c..5fc6c7a8 100644 --- a/src/firefox/src/ui/locales/ko.js +++ b/src/firefox/src/ui/locales/ko.js @@ -361,6 +361,10 @@ export default { "st.display.theme.system": "시스템", "st.display.theme.light": "라이트", "st.display.theme.dark": "다크", + "st.display.download_directory.label": "다운로드 디렉터리", + "st.display.download_directory.desc": "브라우저 또는 시스템 다운로드 디렉터리 안의 선택적 하위 폴더입니다. 비워 두면 시스템 기본값을 사용합니다.", + "st.display.download_directory.placeholder": "시스템 기본값", + "st.display.download_directory.error": "WebBrain 또는 Work/WebBrain 같은 상대 폴더를 사용하세요. 절대 경로와 “..”는 허용되지 않습니다.", "st.display.strict_secret.label": "엄격한 비밀 정보 처리", "st.display.strict_secret.desc": "요약이나 어시스턴트 텍스트에 자격 증명(비밀번호, API 키, 토큰, OTP)을 인용하기를 거부합니다 — 명시적으로 요청하더라도 마찬가지입니다. 트레이스 파일을 자주 공유하거나 화면을 공유한다면 유용합니다. 기본값은 꺼짐: WebBrain은 사용자 본인의 브라우저에서 실행되므로, 기본적으로 에이전트는 요청한 값을 보여 주고 `done` 요약만 깔끔하게 유지합니다.", "st.display.request_timeout.label": "LLM 요청 시간 제한", diff --git a/src/firefox/src/ui/locales/ms.js b/src/firefox/src/ui/locales/ms.js index accf9686..31169e0d 100644 --- a/src/firefox/src/ui/locales/ms.js +++ b/src/firefox/src/ui/locales/ms.js @@ -361,6 +361,10 @@ export default { "st.display.theme.system": "Sistem", "st.display.theme.light": "Cerah", "st.display.theme.dark": "Gelap", + "st.display.download_directory.label": "Direktori muat turun", + "st.display.download_directory.desc": "Subfolder pilihan dalam direktori Muat Turun pelayar atau sistem. Biarkan kosong untuk menggunakan lalai sistem.", + "st.display.download_directory.placeholder": "Lalai sistem", + "st.display.download_directory.error": "Gunakan folder relatif seperti WebBrain atau Work/WebBrain. Laluan mutlak dan “..” tidak dibenarkan.", "st.display.strict_secret.label": "Pengendalian rahsia ketat", "st.display.strict_secret.desc": "Enggan memetik kelayakan (kata laluan, kunci API, token, OTP) dalam ringkasan atau teks pembantu — walaupun apabila anda secara jelas memintanya. Berguna jika anda kerap berkongsi fail jejak atau berkongsi skrin. Dimatikan secara lalai: webbrain berjalan dalam pelayar anda sendiri, jadi secara lalai ejen menunjukkan kepada anda nilai yang anda minta dan hanya memastikan ringkasan `done` kemas.", "st.display.request_timeout.label": "Tamat masa permintaan LLM", diff --git a/src/firefox/src/ui/locales/pl.js b/src/firefox/src/ui/locales/pl.js index c2af3780..5d856c62 100644 --- a/src/firefox/src/ui/locales/pl.js +++ b/src/firefox/src/ui/locales/pl.js @@ -292,6 +292,10 @@ export default { 'st.display.theme.system': 'Systemowy', 'st.display.theme.light': 'Jasny', 'st.display.theme.dark': 'Ciemny', + 'st.display.download_directory.label': 'Katalog pobierania', + 'st.display.download_directory.desc': 'Opcjonalny podfolder w katalogu Pobrane przeglądarki lub systemu. Pozostaw puste, aby użyć ustawienia domyślnego systemu.', + 'st.display.download_directory.placeholder': 'Domyślne systemu', + 'st.display.download_directory.error': 'Użyj folderu względnego, np. WebBrain lub Work/WebBrain. Ścieżki bezwzględne i „..” są niedozwolone.', 'st.display.verbose.label': 'Tryb szczegółowy', 'st.display.verbose.desc': 'Pokazuj pełne szczegóły wywołań narzędzi, argumenty i surowe wyniki JSON. Domyślnie wyłączony dla czystszego czatu.', 'st.display.selection_shortcut.label': 'Skrót zaznaczenia', diff --git a/src/firefox/src/ui/locales/ru.js b/src/firefox/src/ui/locales/ru.js index 36a49c03..f05e41f5 100644 --- a/src/firefox/src/ui/locales/ru.js +++ b/src/firefox/src/ui/locales/ru.js @@ -361,6 +361,10 @@ export default { "st.display.theme.system": "Системная", "st.display.theme.light": "Светлая", "st.display.theme.dark": "Тёмная", + "st.display.download_directory.label": "Каталог загрузок", + "st.display.download_directory.desc": "Необязательная подпапка в каталоге загрузок браузера или системы. Оставьте пустым, чтобы использовать системное значение по умолчанию.", + "st.display.download_directory.placeholder": "Системное значение", + "st.display.download_directory.error": "Используйте относительную папку, например WebBrain или Work/WebBrain. Абсолютные пути и «..» не допускаются.", "st.display.strict_secret.label": "Строгая обработка секретов", "st.display.strict_secret.desc": "Отказываться цитировать учётные данные (пароли, API-ключи, токены, OTP) в сводках или тексте ассистента — даже когда вы явно об этом просите. Полезно, если вы регулярно делитесь файлами трасс или демонстрируете экран. По умолчанию выключено: webbrain работает в вашем собственном браузере, поэтому по умолчанию агент показывает запрошенные вами значения и просто держит сводки `done` аккуратными.", "st.display.request_timeout.label": "Тайм-аут запроса к LLM", diff --git a/src/firefox/src/ui/locales/th.js b/src/firefox/src/ui/locales/th.js index a113030c..7293ba51 100644 --- a/src/firefox/src/ui/locales/th.js +++ b/src/firefox/src/ui/locales/th.js @@ -361,6 +361,10 @@ export default { "st.display.theme.system": "ระบบ", "st.display.theme.light": "สว่าง", "st.display.theme.dark": "มืด", + "st.display.download_directory.label": "ไดเรกทอรีดาวน์โหลด", + "st.display.download_directory.desc": "โฟลเดอร์ย่อยภายในไดเรกทอรีดาวน์โหลดของเบราว์เซอร์หรือระบบ เว้นว่างไว้เพื่อใช้ค่าเริ่มต้นของระบบ", + "st.display.download_directory.placeholder": "ค่าเริ่มต้นของระบบ", + "st.display.download_directory.error": "ใช้โฟลเดอร์แบบสัมพัทธ์ เช่น WebBrain หรือ Work/WebBrain ไม่อนุญาตพาธแบบสัมบูรณ์และ “..”", "st.display.strict_secret.label": "การจัดการความลับอย่างเข้มงวด", "st.display.strict_secret.desc": "ปฏิเสธการอ้างอิงข้อมูลรับรอง (รหัสผ่าน คีย์ API โทเค็น OTP) ในบทสรุปหรือข้อความของผู้ช่วย — แม้คุณจะขอโดยตรง มีประโยชน์หากคุณแชร์ไฟล์ trace หรือแชร์หน้าจอเป็นประจำ ปิดเป็นค่าเริ่มต้น: WebBrain ทำงานในเบราว์เซอร์ของคุณเอง ดังนั้นโดยค่าเริ่มต้นเอเจนต์จะแสดงค่าที่คุณขอ และเพียงทำให้บทสรุป `done` เป็นระเบียบ", "st.display.request_timeout.label": "เวลารอคำขอ LLM", diff --git a/src/firefox/src/ui/locales/tl.js b/src/firefox/src/ui/locales/tl.js index e400070d..5a022ae8 100644 --- a/src/firefox/src/ui/locales/tl.js +++ b/src/firefox/src/ui/locales/tl.js @@ -361,6 +361,10 @@ export default { "st.display.theme.system": "System", "st.display.theme.light": "Maliwanag", "st.display.theme.dark": "Madilim", + "st.display.download_directory.label": "Direktoryo ng download", + "st.display.download_directory.desc": "Opsyonal na subfolder sa loob ng Downloads directory ng browser o system. Iwang blangko upang gamitin ang default ng system.", + "st.display.download_directory.placeholder": "Default ng system", + "st.display.download_directory.error": "Gumamit ng relative na folder tulad ng WebBrain o Work/WebBrain. Hindi pinapayagan ang absolute path at “..”.", "st.display.strict_secret.label": "Mahigpit na paghawak ng lihim", "st.display.strict_secret.desc": "Tanggihang i-quote ang mga kredensyal (password, API key, token, OTPs) sa mga summary o teksto ng assistant — kahit hayagan mong hilingin ang mga ito. Mainam kung madalas kang magbahagi ng trace files o screen-share. Naka-off bilang default: tumatakbo ang webbrain sa sarili mong browser, kaya bilang default ay ipinapakita sa iyo ng ahente ang mga value na hiniling mo at pinapanatili lang na malinis ang mga `done` na summary.", "st.display.request_timeout.label": "Timeout ng LLM request", diff --git a/src/firefox/src/ui/locales/tr.js b/src/firefox/src/ui/locales/tr.js index c688173f..e78773e3 100644 --- a/src/firefox/src/ui/locales/tr.js +++ b/src/firefox/src/ui/locales/tr.js @@ -400,6 +400,10 @@ export default { "st.display.theme.system": "Sistem", "st.display.theme.light": "Açık", "st.display.theme.dark": "Koyu", + "st.display.download_directory.label": "İndirme dizini", + "st.display.download_directory.desc": "Tarayıcının veya sistemin İndirilenler dizini içinde isteğe bağlı bir alt klasör. Sistem varsayılanını kullanmak için boş bırakın.", + "st.display.download_directory.placeholder": "Sistem varsayılanı", + "st.display.download_directory.error": "WebBrain veya Work/WebBrain gibi göreli bir klasör kullanın. Mutlak yollara ve “..” kullanımına izin verilmez.", "st.display.strict_secret.label": "Katı gizli bilgi işleme", "st.display.strict_secret.desc": "Özellikle istesen bile, özetlerde veya asistan metninde kimlik bilgilerini (parolalar, API anahtarları, jetonlar, OTP'ler) aktarmayı reddet. Düzenli olarak iz dosyaları paylaşıyorsan veya ekran paylaşımı yapıyorsan kullanışlıdır. Varsayılan olarak kapalı: WebBrain kendi tarayıcında çalışır, bu yüzden varsayılan olarak aracı istediğin değerleri sana gösterir ve yalnızca `done` özetlerini derli toplu tutar.", "st.display.request_timeout.label": "LLM istek zaman aşımı", diff --git a/src/firefox/src/ui/locales/uk.js b/src/firefox/src/ui/locales/uk.js index 9b590478..19a54b8e 100644 --- a/src/firefox/src/ui/locales/uk.js +++ b/src/firefox/src/ui/locales/uk.js @@ -361,6 +361,10 @@ export default { "st.display.theme.system": "Системна", "st.display.theme.light": "Світла", "st.display.theme.dark": "Темна", + "st.display.download_directory.label": "Каталог завантажень", + "st.display.download_directory.desc": "Необов’язкова підпапка в каталозі завантажень браузера або системи. Залиште порожнім, щоб використовувати системне значення за замовчуванням.", + "st.display.download_directory.placeholder": "Системне значення", + "st.display.download_directory.error": "Використовуйте відносну папку, наприклад WebBrain або Work/WebBrain. Абсолютні шляхи та «..» заборонені.", "st.display.strict_secret.label": "Суворе поводження з секретами", "st.display.strict_secret.desc": "Відмовлятися наводити облікові дані (паролі, API-ключі, токени, OTP) у підсумках чи тексті асистента — навіть коли ви явно про них просите. Корисно, якщо ви регулярно ділитеся файлами трас або демонструєте екран. Вимкнено за замовчуванням: webbrain працює у вашому власному браузері, тож за замовчуванням агент показує вам значення, про які ви просите, і лише тримає підсумки `done` охайними.", "st.display.request_timeout.label": "Тайм-аут запиту до LLM", diff --git a/src/firefox/src/ui/locales/zh.js b/src/firefox/src/ui/locales/zh.js index 9aceb4e9..0d87e31f 100644 --- a/src/firefox/src/ui/locales/zh.js +++ b/src/firefox/src/ui/locales/zh.js @@ -361,6 +361,10 @@ export default { "st.display.theme.system": "系统", "st.display.theme.light": "浅色", "st.display.theme.dark": "深色", + "st.display.download_directory.label": "下载目录", + "st.display.download_directory.desc": "浏览器或系统“下载”目录中的可选子文件夹。留空即可使用系统默认值。", + "st.display.download_directory.placeholder": "系统默认值", + "st.display.download_directory.error": "请使用 WebBrain 或 Work/WebBrain 这样的相对文件夹。不允许使用绝对路径或“..”。", "st.display.strict_secret.label": "严格的机密处理", "st.display.strict_secret.desc": "拒绝在总结或助手文本中引用凭据(密码、API 密钥、令牌、OTP)—— 即使你明确要求也是如此。如果你经常分享轨迹文件或共享屏幕,这会很有用。默认关闭:webbrain 运行在你自己的浏览器中,因此默认情况下代理会向你展示你索取的值,只是把 `done` 总结保持整洁。", "st.display.request_timeout.label": "LLM 请求超时", diff --git a/src/firefox/src/ui/settings.html b/src/firefox/src/ui/settings.html index cc392f05..17d4a215 100644 --- a/src/firefox/src/ui/settings.html +++ b/src/firefox/src/ui/settings.html @@ -691,6 +691,23 @@ color: var(--text2); line-height: 1.4; } + .setting-input { + flex: 0 1 260px; + min-width: 120px; + max-width: 42%; + background: var(--bg); + color: var(--text); + border: 1px solid var(--border); + border-radius: 6px; + padding: 7px 10px; + font: inherit; + font-size: 12px; + outline: none; + } + .setting-input:focus { + border-color: var(--accent); + box-shadow: 0 0 0 2px rgba(108,99,255,0.22); + } .skill-source { max-width: 340px; overflow: hidden; @@ -1022,6 +1039,14 @@

+
+
+
+
+
+ +
diff --git a/src/firefox/src/ui/settings.js b/src/firefox/src/ui/settings.js index c5810e6b..3e73f1d8 100644 --- a/src/firefox/src/ui/settings.js +++ b/src/firefox/src/ui/settings.js @@ -32,6 +32,10 @@ import { parseProviderExtraBodyJson, shouldUseOpenAIResponsesApi, } from '../providers/provider-compatibility.js'; +import { + DOWNLOAD_DIRECTORY_STORAGE_KEY, + normalizeDownloadDirectory, +} from '../download-directory.js'; // Version shown in the subtitle. Kept here so it only needs one update per // release; the subtitle string itself is translated. @@ -145,6 +149,7 @@ const btnClearCaptcha = document.getElementById('btn-clear-captcha'); const captchaTestResult = document.getElementById('test-captcha'); const languageSelect = document.getElementById('select-language'); const themeSelect = document.getElementById('select-theme'); +const downloadDirectoryInput = document.getElementById('input-download-directory'); const subtitleEl = document.getElementById('subtitle'); // --- Appearance / theme --- @@ -375,7 +380,7 @@ async function init() { browser.storage.local.remove(['authToken', 'authEmail', 'authDefaultModel']).catch(() => {}); // Load display settings - const stored = await browser.storage.local.get(['verboseMode', 'selectionShortcutEnabled', 'helpImproveWebBrain', 'screenshotFallback', 'maxAgentSteps', 'autoScreenshot', 'useSiteAdapters', 'voiceInputEnabled', 'apiMutationObserverEnabled', 'planBeforeActMode', 'planBeforeAct', 'planReviewMode', 'planReviewConfidenceThreshold', 'notifySound', 'completionConfetti', 'tracingEnabled', 'strictSecretMode', 'agentAllowLocalNetwork', 'scheduledTasksEnabled', 'scheduledRequireConsequentialConfirmation', 'providerFilter', 'requestTimeoutMs', 'clarifyTimeoutSec', 'clarifyTimeoutSemanticsV2', 'costAllowanceSessionUsd', 'costAllowanceTotalUsd', 'cloudCostSpentUsd', 'screenshotRedaction']); + const stored = await browser.storage.local.get(['verboseMode', 'selectionShortcutEnabled', 'helpImproveWebBrain', 'screenshotFallback', 'maxAgentSteps', 'autoScreenshot', 'useSiteAdapters', 'voiceInputEnabled', 'apiMutationObserverEnabled', 'planBeforeActMode', 'planBeforeAct', 'planReviewMode', 'planReviewConfidenceThreshold', DOWNLOAD_DIRECTORY_STORAGE_KEY, 'notifySound', 'completionConfetti', 'tracingEnabled', 'strictSecretMode', 'agentAllowLocalNetwork', 'scheduledTasksEnabled', 'scheduledRequireConsequentialConfirmation', 'providerFilter', 'requestTimeoutMs', 'clarifyTimeoutSec', 'clarifyTimeoutSemanticsV2', 'costAllowanceSessionUsd', 'costAllowanceTotalUsd', 'cloudCostSpentUsd', 'screenshotRedaction']); if (typeof stored.providerFilter === 'string' && ['all','local','cloud','router'].includes(stored.providerFilter)) { providerFilter = stored.providerFilter; } @@ -428,6 +433,9 @@ async function init() { planReviewConfidenceRange.value = normalizePlanReviewConfidenceThreshold(stored); updatePlanReviewConfidenceUI(); } + if (downloadDirectoryInput) { + downloadDirectoryInput.value = normalizeDownloadDirectory(stored[DOWNLOAD_DIRECTORY_STORAGE_KEY]); + } if (notifySoundToggle) notifySoundToggle.checked = stored.notifySound ?? true; if (completionConfettiToggle) completionConfettiToggle.checked = stored.completionConfetti ?? true; if (tracingToggle) tracingToggle.checked = stored.tracingEnabled === true; @@ -931,6 +939,23 @@ if (globalThis.browser?.storage?.onChanged) { // --- Display Settings --- +downloadDirectoryInput?.addEventListener('input', () => { + downloadDirectoryInput.setCustomValidity(''); +}); + +downloadDirectoryInput?.addEventListener('change', async () => { + const raw = String(downloadDirectoryInput.value || '').trim(); + const directory = normalizeDownloadDirectory(raw); + if (raw && !directory) { + downloadDirectoryInput.setCustomValidity(t('st.display.download_directory.error')); + downloadDirectoryInput.reportValidity(); + return; + } + downloadDirectoryInput.setCustomValidity(''); + downloadDirectoryInput.value = directory; + await browser.storage.local.set({ [DOWNLOAD_DIRECTORY_STORAGE_KEY]: directory }).catch(() => {}); +}); + verboseToggle.addEventListener('change', async () => { await browser.storage.local.set({ verboseMode: verboseToggle.checked }).catch(() => {}); }); diff --git a/test/run.js b/test/run.js index 26354593..52943291 100644 --- a/test/run.js +++ b/test/run.js @@ -204,6 +204,12 @@ const ConfigTransferCh = await import( const ConfigTransferFx = await import( 'file://' + path.join(ROOT, 'src/firefox/src/config-transfer.js').replace(/\\/g, '/') ); +const DownloadDirectoryCh = await import( + 'file://' + path.join(ROOT, 'src/chrome/src/download-directory.js').replace(/\\/g, '/') +); +const DownloadDirectoryFx = await import( + 'file://' + path.join(ROOT, 'src/firefox/src/download-directory.js').replace(/\\/g, '/') +); const { transcribeAudio } = await import( 'file://' + path.join(ROOT, 'src/chrome/src/agent/transcribe.js').replace(/\\/g, '/') ); @@ -2089,6 +2095,7 @@ test('/export --traces is wired in both side panels and backgrounds', () => { test('config transfer exports and restores Settings values including provider keys', () => { const stored = { wbLocale: 'tr', + downloadDirectory: 'Work/WebBrain', strictSecretMode: true, profileEnabled: true, profileText: 'Use the test profile', @@ -2117,6 +2124,7 @@ test('config transfer exports and restores Settings values including provider ke assert.equal(chromeExport.settings.transcriptionModel.apiKey, 'audio-secret'); assert.equal(chromeExport.settings.capsolverApiKey, 'capsolver-secret'); assert.equal(chromeExport.settings.profileText, 'Use the test profile'); + assert.equal(chromeExport.settings.downloadDirectory, 'Work/WebBrain'); assert.equal(chromeExport.settings.wb_user_memory_v1.records.length, 1); assert.equal(chromeExport.settings.providers.webbrain_cloud.deviceGuid, undefined, 'device identity must not be portable'); assert.equal(chromeExport.settings.cloudCostSpentUsd, undefined, 'spend counters are runtime state, not config'); @@ -2127,6 +2135,7 @@ test('config transfer exports and restores Settings values including provider ke const imported = ConfigTransferCh.parseConfigImport(JSON.stringify(chromeExport)); assert.equal(imported.settings.providers.openai.apiKey, 'provider-secret'); assert.equal(imported.settings.activeProvider, 'openai'); + assert.equal(imported.settings.downloadDirectory, 'Work/WebBrain'); assert.equal(imported.settings.themeMode, 'system', 'missing Settings values should restore their product defaults'); assert.equal(Object.keys(imported.settings).length, ConfigTransferCh.CONFIG_STORAGE_KEYS.length); }); @@ -2151,6 +2160,146 @@ test('config transfer validates schema, size, containers, and unknown keys', () assert.deepEqual(parsed.ignoredKeys, ['futureSetting']); }); +test('download directory routing is relative and uses each browser-supported path', async () => { + for (const [label, downloadDirectory] of [ + ['chrome', DownloadDirectoryCh], + ['firefox', DownloadDirectoryFx], + ]) { + assert.equal(downloadDirectory.normalizeDownloadDirectory(''), '', `${label}: blank should use the system default`); + assert.equal(downloadDirectory.normalizeDownloadDirectory(' Work\\WebBrain '), 'Work/WebBrain', `${label}: nested relative directory should normalize`); + assert.equal(downloadDirectory.normalizeDownloadDirectory('/tmp/WebBrain'), '', `${label}: absolute POSIX paths should be rejected`); + assert.equal(downloadDirectory.normalizeDownloadDirectory('C:\\Downloads\\WebBrain'), '', `${label}: absolute Windows paths should be rejected`); + assert.equal(downloadDirectory.normalizeDownloadDirectory('Work/../Other'), '', `${label}: parent traversal should be rejected`); + assert.equal( + downloadDirectory.filenameInDownloadDirectory('Work/WebBrain', '/Users/test/Downloads/report.pdf'), + 'Work/WebBrain/report.pdf', + `${label}: configured directory should prefix the tentative basename`, + ); + } + + let storageChangeListener = null; + let determiningFilenameListener = null; + const chromeApi = { + runtime: { id: 'webbrain-extension-id' }, + storage: { + local: { + async get() { + return { downloadDirectory: 'WebBrain' }; + }, + }, + onChanged: { + addListener(listener) { + storageChangeListener = listener; + }, + }, + }, + downloads: { + onDeterminingFilename: { + addListener(listener) { + determiningFilenameListener = listener; + }, + }, + }, + }; + + const installed = DownloadDirectoryCh.installDownloadDirectoryRouting(chromeApi); + assert.equal(installed, determiningFilenameListener, 'chrome: routing listener should register'); + + const ownSuggestions = []; + assert.equal( + installed( + { + byExtensionId: chromeApi.runtime.id, + filename: '/Users/test/Downloads/generated.txt', + }, + (suggestion) => ownSuggestions.push(suggestion), + ), + true, + 'chrome: own downloads should wait for the stored directory', + ); + await new Promise((resolve) => setImmediate(resolve)); + assert.deepEqual(ownSuggestions, [{ filename: 'WebBrain/generated.txt' }], 'chrome: own download should be routed'); + + const unrelatedSuggestions = []; + assert.equal( + installed( + { byExtensionId: 'another-extension', filename: '/Users/test/Downloads/other.txt' }, + (suggestion) => unrelatedSuggestions.push(suggestion), + ), + undefined, + 'chrome: unrelated downloads should not wait', + ); + assert.deepEqual(unrelatedSuggestions, [undefined], 'chrome: unrelated downloads should keep their filename'); + + storageChangeListener({ downloadDirectory: { newValue: 'Work/Exports' } }, 'local'); + const updatedSuggestions = []; + installed( + { byExtensionId: chromeApi.runtime.id, filename: 'next.csv' }, + (suggestion) => updatedSuggestions.push(suggestion), + ); + await new Promise((resolve) => setImmediate(resolve)); + assert.deepEqual(updatedSuggestions, [{ filename: 'Work/Exports/next.csv' }], 'chrome: storage changes should apply live'); + + const firefoxApi = { + storage: { + local: { + async get() { + return { downloadDirectory: 'Work/Exports' }; + }, + }, + }, + }; + assert.equal( + await DownloadDirectoryFx.filenameInConfiguredDownloadDirectory(firefoxApi, 'report.pdf'), + 'Work/Exports/report.pdf', + 'firefox: explicit filenames should be routed before downloads.download', + ); + assert.equal( + await DownloadDirectoryFx.filenameInConfiguredDownloadDirectory(firefoxApi, undefined, 'https://example.com/files/report.pdf'), + 'Work/Exports/report.pdf', + 'firefox: URL-derived filenames should be routed before downloads.download', + ); +}); + +test('Firefox download_files passes the configured relative directory to downloads.download', async () => { + const originalBrowser = globalThis.browser; + const downloadCalls = []; + try { + globalThis.browser = { + storage: { + local: { + async get() { + return { downloadDirectory: 'Work/WebBrain' }; + }, + }, + }, + downloads: { + async download(options) { + downloadCalls.push(options); + return 9101; + }, + async search() { + return [{ + id: 9101, + filename: '/Users/test/Downloads/Work/WebBrain/report.pdf', + state: 'complete', + bytesReceived: 100, + totalBytes: 100, + }]; + }, + }, + }; + + const result = await downloadFilesFx({ urls: ['https://example.com/files/report.pdf'] }); + assert.equal(result.success, true); + assert.equal(downloadCalls.length, 1); + assert.equal(downloadCalls[0].filename, 'Work/WebBrain/report.pdf'); + } finally { + if (originalBrowser === undefined) delete globalThis.browser; + else globalThis.browser = originalBrowser; + } +}); + test('/export --config and /import JSON or --file are wired in both browsers', () => { for (const [label, prefix] of [ ['chrome', 'src/chrome'], @@ -9911,6 +10060,7 @@ test('settings moves profile and memory controls into Memory while CAPTCHA stays for (const id of [ 'select-language', 'select-theme', + 'input-download-directory', 'toggle-verbose', 'select-plan-before-act-mode', 'toggle-scheduled-tasks', @@ -9982,6 +10132,40 @@ test('settings General search filters visible and Advanced controls', () => { } }); +test('Settings > General configures WebBrain download subdirectory with system-default fallback', () => { + for (const [label, prefix, runtime] of [ + ['chrome', 'src/chrome', 'chrome'], + ['firefox', 'src/firefox', 'browser'], + ]) { + const html = fs.readFileSync(path.join(ROOT, prefix, 'src/ui/settings.html'), 'utf8'); + const settings = fs.readFileSync(path.join(ROOT, prefix, 'src/ui/settings.js'), 'utf8'); + const locale = fs.readFileSync(path.join(ROOT, prefix, 'src/ui/locales/en.js'), 'utf8'); + const background = fs.readFileSync(path.join(ROOT, prefix, 'src/background.js'), 'utf8'); + const config = fs.readFileSync(path.join(ROOT, prefix, 'src/config-transfer.js'), 'utf8'); + + assert.match(html, /id="input-download-directory"[\s\S]*?data-i18n-placeholder="st\.display\.download_directory\.placeholder"/, `${label}: General should expose the download directory field`); + assert.match(locale, /'st\.display\.download_directory\.placeholder': 'System default'/, `${label}: blank field should explain its system default`); + assert.match(locale, /Optional subfolder inside your browser or system Downloads directory/, `${label}: setting should explain the browser API boundary`); + assert.match(settings, /downloadDirectoryInput\.value = normalizeDownloadDirectory\(stored\[DOWNLOAD_DIRECTORY_STORAGE_KEY\]\)/, `${label}: stored directory should hydrate the field`); + assert.match(settings, new RegExp(`${runtime}\\.storage\\.local\\.set\\(\\{ \\[DOWNLOAD_DIRECTORY_STORAGE_KEY\\]: directory \\}\\)`), `${label}: valid directory should persist`); + assert.match(settings, /if \(raw && !directory\) \{[\s\S]*?setCustomValidity\(t\('st\.display\.download_directory\.error'\)\)[\s\S]*?return;/, `${label}: invalid absolute or parent paths should not persist`); + assert.match(config, /downloadDirectory: ''/, `${label}: portable config should default to the system directory`); + assert.match(config, /'downloadDirectory'/, `${label}: portable config should allow the setting`); + + if (label === 'chrome') { + assert.match(background, /installDownloadDirectoryRouting\(chrome\);/, 'chrome: background should route extension downloads centrally'); + } else { + const network = fs.readFileSync(path.join(ROOT, prefix, 'src/network/network-tools.js'), 'utf8'); + const agent = fs.readFileSync(path.join(ROOT, prefix, 'src/agent/agent.js'), 'utf8'); + const capture = fs.readFileSync(path.join(ROOT, prefix, 'src/run-capture.js'), 'utf8'); + assert.doesNotMatch(background, /onDeterminingFilename|installDownloadDirectoryRouting/, 'firefox: background must not depend on unsupported filename events'); + assert.match(network, /filenameInConfiguredDownloadDirectory\(browser,[\s\S]*?browser\.downloads\.download/g, 'firefox: network downloads should resolve the configured path before starting'); + assert.match(agent, /filenameInConfiguredDownloadDirectory\(browser, filename, crop\.dataUrl\)[\s\S]*?browser\.downloads\.download/, 'firefox: visible-media downloads should resolve the configured path'); + assert.match(capture, /filenameInConfiguredDownloadDirectory\(api, filename, dataUrl\)[\s\S]*?api\.downloads\.download/, 'firefox: run captures should resolve the configured path'); + } + } +}); + test('settings Providers tab has a search box beside provider filters', () => { for (const [label, htmlRel, settingsRel, localeRel] of [ ['chrome', 'src/chrome/src/ui/settings.html', 'src/chrome/src/ui/settings.js', 'src/chrome/src/ui/locales/en.js'], From 15472fc13aeca1aaa3b166f9856ab4b6f6f4966c Mon Sep 17 00:00:00 2001 From: Emre Sokullu Date: Fri, 17 Jul 2026 09:26:19 +0300 Subject: [PATCH 2/7] Preserve Firefox download filenames --- src/firefox/src/agent/agent.js | 2 +- src/firefox/src/download-directory.js | 21 +++----- src/firefox/src/network/network-tools.js | 62 +++++++++++++++++++++--- src/firefox/src/run-capture.js | 2 +- test/run.js | 61 ++++++++++++++++++----- 5 files changed, 113 insertions(+), 35 deletions(-) diff --git a/src/firefox/src/agent/agent.js b/src/firefox/src/agent/agent.js index 63b4ac3a..1b12ff9a 100644 --- a/src/firefox/src/agent/agent.js +++ b/src/firefox/src/agent/agent.js @@ -3405,7 +3405,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d filename = filename.split('/').pop().split('\\').pop(); filename = filename.replace(/\.(jpe?g|webp)$/i, '.png'); if (!/\.png$/i.test(filename)) filename += '.png'; - filename = await filenameInConfiguredDownloadDirectory(browser, filename, crop.dataUrl); + filename = await filenameInConfiguredDownloadDirectory(browser, filename); const downloadId = await browser.downloads.download({ url: crop.dataUrl, filename, saveAs: false }); return { diff --git a/src/firefox/src/download-directory.js b/src/firefox/src/download-directory.js index 9ccecfe9..e79c53fb 100644 --- a/src/firefox/src/download-directory.js +++ b/src/firefox/src/download-directory.js @@ -30,32 +30,23 @@ export function filenameInDownloadDirectory(directory, filename) { return `${normalizedDirectory}/${basename}`; } -function filenameFromDownloadUrl(value) { - try { - const url = new URL(String(value || '')); - if (url.protocol !== 'http:' && url.protocol !== 'https:') return ''; - return decodeURIComponent(url.pathname.split('/').filter(Boolean).pop() || ''); - } catch { - return ''; - } -} - /** * Firefox does not expose downloads.onDeterminingFilename. Its download call * sites therefore resolve the configured relative path before starting each * download. Missing storage keeps tests and older runtimes on the original - * filename, while a configured directory also supplies a URL-derived fallback - * for downloads that did not request a name. + * filename. Callers must supply a real filename rather than guessing from the + * URL, because doing so would override a server-selected Content-Disposition + * filename. */ -export async function filenameInConfiguredDownloadDirectory(api, filename, url = '') { +export async function filenameInConfiguredDownloadDirectory(api, filename) { const original = filename || undefined; if (!api?.storage?.local?.get) return original; try { const stored = await api.storage.local.get(DOWNLOAD_DIRECTORY_STORAGE_KEY); const directory = normalizeDownloadDirectory(stored?.[DOWNLOAD_DIRECTORY_STORAGE_KEY]); if (!directory) return original; - const candidate = original || filenameFromDownloadUrl(url) || 'download'; - return filenameInDownloadDirectory(directory, candidate) || `${directory}/download`; + if (!original) return undefined; + return filenameInDownloadDirectory(directory, original) || original; } catch { return original; } diff --git a/src/firefox/src/network/network-tools.js b/src/firefox/src/network/network-tools.js index 50e0604b..83375bd6 100644 --- a/src/firefox/src/network/network-tools.js +++ b/src/firefox/src/network/network-tools.js @@ -419,6 +419,55 @@ export function filenameFromContentDisposition(value) { return safeDownloadFilename(parameters.get('filename')); } +function filenameFromHttpUrl(value) { + try { + const url = new URL(String(value || '')); + if (url.protocol !== 'http:' && url.protocol !== 'https:') return undefined; + const encoded = url.pathname.split('/').filter(Boolean).pop() || ''; + let decoded = encoded; + try { decoded = decodeURIComponent(encoded); } catch (_) {} + return safeDownloadFilename(decoded); + } catch { + return undefined; + } +} + +/** + * Firefox has no downloads.onDeterminingFilename event, so a complete filename + * must be supplied to downloads.download() to select a subdirectory. Discover + * the server's preferred basename with a bounded HEAD request first. If the + * request cannot be inspected, return undefined and let Firefox preserve its + * normal Content-Disposition filename instead of fabricating one. + */ +async function discoverHttpDownloadFilename(url) { + const target = String(url || '').trim(); + const urlCheck = validateFetchUrl(target, { allowLocalNetwork: getAllowLocalNetwork() }); + if (!urlCheck.ok) return undefined; + const controller = typeof AbortController === 'function' ? new AbortController() : null; + const timeout = controller ? setTimeout(() => controller.abort(), 5000) : null; + try { + const response = await fetch(target, { + method: 'HEAD', + credentials: 'include', + redirect: 'manual', + cache: 'no-store', + ...(controller ? { signal: controller.signal } : {}), + }); + if (response?.type === 'opaqueredirect' || isHttpRedirectStatus(response?.status) || !response?.ok) { + return undefined; + } + const responseUrl = response.url || target; + const responseUrlCheck = validateFetchUrl(responseUrl, { allowLocalNetwork: getAllowLocalNetwork() }); + if (!responseUrlCheck.ok) return undefined; + return filenameFromContentDisposition(response.headers?.get?.('content-disposition')) + || filenameFromHttpUrl(responseUrl); + } catch { + return undefined; + } finally { + if (timeout) clearTimeout(timeout); + } +} + function defaultSkillDownloadFilename(contentType) { const type = safeDataUrlMimeType(contentType); const extension = { @@ -909,7 +958,7 @@ async function downloadSkillFile(url, filename, waitMs = 60000) { || filenameFromContentDisposition(file.contentDisposition); const opts = { url: staged ? staged.localUrl : file.dataUrl, conflictAction: 'uniquify' }; const safeName = safeDownloadFilename(filename) || responseFilename || defaultSkillDownloadFilename(contentType); - const configuredName = await filenameInConfiguredDownloadDirectory(browser, safeName, url); + const configuredName = await filenameInConfiguredDownloadDirectory(browser, safeName); if (configuredName) opts.filename = configuredName; let downloadId; try { @@ -2099,7 +2148,8 @@ export async function downloadResourceFromPage(tabId, args = {}) { }; } - const downloadFilename = await filenameInConfiguredDownloadDirectory(browser, filename, r.url); + const suggestedFilename = filename || await discoverHttpDownloadFilename(r.url); + const downloadFilename = await filenameInConfiguredDownloadDirectory(browser, suggestedFilename); const downloadId = await browser.downloads.download({ url: r.url, filename: downloadFilename, @@ -2166,11 +2216,9 @@ export async function downloadFiles(args = {}) { if (i >= urls.length) return; const url = urls[i]; try { - const filename = await filenameInConfiguredDownloadDirectory( - browser, - i === 0 ? singleFilename : null, - url, - ); + const suggestedFilename = (i === 0 ? singleFilename : null) + || await discoverHttpDownloadFilename(url); + const filename = await filenameInConfiguredDownloadDirectory(browser, suggestedFilename); const opts = { url, conflictAction: 'uniquify' }; if (filename) opts.filename = filename; const downloadId = await browser.downloads.download(opts); diff --git a/src/firefox/src/run-capture.js b/src/firefox/src/run-capture.js index 8f1c023a..14f94749 100644 --- a/src/firefox/src/run-capture.js +++ b/src/firefox/src/run-capture.js @@ -62,7 +62,7 @@ export async function captureAndSaveRunScreenshot(api, tabId, filename) { throw new Error('The run tab could not be activated for capture.'); } const dataUrl = await api.tabs.captureVisibleTab(tab.windowId, { format: 'png' }); - filename = await filenameInConfiguredDownloadDirectory(api, filename, dataUrl); + filename = await filenameInConfiguredDownloadDirectory(api, filename); const downloadId = await api.downloads.download({ url: dataUrl, filename, diff --git a/test/run.js b/test/run.js index 52943291..600f875b 100644 --- a/test/run.js +++ b/test/run.js @@ -2255,15 +2255,17 @@ test('download directory routing is relative and uses each browser-supported pat 'firefox: explicit filenames should be routed before downloads.download', ); assert.equal( - await DownloadDirectoryFx.filenameInConfiguredDownloadDirectory(firefoxApi, undefined, 'https://example.com/files/report.pdf'), - 'Work/Exports/report.pdf', - 'firefox: URL-derived filenames should be routed before downloads.download', + await DownloadDirectoryFx.filenameInConfiguredDownloadDirectory(firefoxApi, undefined), + undefined, + 'firefox: missing filenames should be left to the browser instead of overriding Content-Disposition', ); }); -test('Firefox download_files passes the configured relative directory to downloads.download', async () => { +test('Firefox download_files preserves server-selected filenames inside the configured directory', async () => { const originalBrowser = globalThis.browser; + const originalFetch = globalThis.fetch; const downloadCalls = []; + const fetchCalls = []; try { globalThis.browser = { storage: { @@ -2276,11 +2278,11 @@ test('Firefox download_files passes the configured relative directory to downloa downloads: { async download(options) { downloadCalls.push(options); - return 9101; + return 9100 + downloadCalls.length; }, - async search() { + async search({ id }) { return [{ - id: 9101, + id, filename: '/Users/test/Downloads/Work/WebBrain/report.pdf', state: 'complete', bytesReceived: 100, @@ -2289,14 +2291,51 @@ test('Firefox download_files passes the configured relative directory to downloa }, }, }; + globalThis.fetch = async (url, options) => { + fetchCalls.push({ url, options }); + return { + ok: true, + status: 200, + type: 'basic', + url, + headers: { + get(name) { + return String(name).toLowerCase() === 'content-disposition' + ? 'attachment; filename="quarterly-report.pdf"' + : null; + }, + }, + }; + }; - const result = await downloadFilesFx({ urls: ['https://example.com/files/report.pdf'] }); + const result = await downloadFilesFx({ urls: ['https://example.com/export?id=1'] }); assert.equal(result.success, true); assert.equal(downloadCalls.length, 1); - assert.equal(downloadCalls[0].filename, 'Work/WebBrain/report.pdf'); + assert.equal(downloadCalls[0].filename, 'Work/WebBrain/quarterly-report.pdf'); + assert.equal(fetchCalls.length, 1); + assert.equal(fetchCalls[0].options.method, 'HEAD'); + assert.equal(fetchCalls[0].options.credentials, 'include'); + + globalThis.fetch = async () => ({ + ok: false, + status: 405, + type: 'basic', + url: 'https://example.com/export?id=2', + headers: { get() { return null; } }, + }); + const fallback = await downloadFilesFx({ urls: ['https://example.com/export?id=2'] }); + assert.equal(fallback.success, true); + assert.equal(downloadCalls.length, 2); + assert.equal( + Object.hasOwn(downloadCalls[1], 'filename'), + false, + 'an unavailable HEAD response should leave filename selection to Firefox', + ); } finally { if (originalBrowser === undefined) delete globalThis.browser; else globalThis.browser = originalBrowser; + if (originalFetch === undefined) delete globalThis.fetch; + else globalThis.fetch = originalFetch; } }); @@ -10160,8 +10199,8 @@ test('Settings > General configures WebBrain download subdirectory with system-d const capture = fs.readFileSync(path.join(ROOT, prefix, 'src/run-capture.js'), 'utf8'); assert.doesNotMatch(background, /onDeterminingFilename|installDownloadDirectoryRouting/, 'firefox: background must not depend on unsupported filename events'); assert.match(network, /filenameInConfiguredDownloadDirectory\(browser,[\s\S]*?browser\.downloads\.download/g, 'firefox: network downloads should resolve the configured path before starting'); - assert.match(agent, /filenameInConfiguredDownloadDirectory\(browser, filename, crop\.dataUrl\)[\s\S]*?browser\.downloads\.download/, 'firefox: visible-media downloads should resolve the configured path'); - assert.match(capture, /filenameInConfiguredDownloadDirectory\(api, filename, dataUrl\)[\s\S]*?api\.downloads\.download/, 'firefox: run captures should resolve the configured path'); + assert.match(agent, /filenameInConfiguredDownloadDirectory\(browser, filename\)[\s\S]*?browser\.downloads\.download/, 'firefox: visible-media downloads should resolve the configured path'); + assert.match(capture, /filenameInConfiguredDownloadDirectory\(api, filename\)[\s\S]*?api\.downloads\.download/, 'firefox: run captures should resolve the configured path'); } } }); From e649b20489090315e8ef9f320ae3dc21eaeba918 Mon Sep 17 00:00:00 2001 From: Emre Sokullu Date: Fri, 17 Jul 2026 09:26:49 +0300 Subject: [PATCH 3/7] Add first-install extension pinning guide --- src/chrome/src/background.js | 13 +- src/chrome/src/ui/i18n.js | 12 +- src/chrome/src/ui/install.css | 474 +++++++++++++++++++++++++++++++ src/chrome/src/ui/install.html | 92 ++++++ src/chrome/src/ui/install.js | 133 +++++++++ src/chrome/src/ui/locales/ar.js | 18 ++ src/chrome/src/ui/locales/en.js | 18 ++ src/chrome/src/ui/locales/es.js | 18 ++ src/chrome/src/ui/locales/fr.js | 18 ++ src/chrome/src/ui/locales/he.js | 18 ++ src/chrome/src/ui/locales/id.js | 18 ++ src/chrome/src/ui/locales/ja.js | 18 ++ src/chrome/src/ui/locales/ko.js | 18 ++ src/chrome/src/ui/locales/ms.js | 18 ++ src/chrome/src/ui/locales/pl.js | 18 ++ src/chrome/src/ui/locales/ru.js | 18 ++ src/chrome/src/ui/locales/th.js | 18 ++ src/chrome/src/ui/locales/tl.js | 18 ++ src/chrome/src/ui/locales/tr.js | 18 ++ src/chrome/src/ui/locales/uk.js | 18 ++ src/chrome/src/ui/locales/zh.js | 18 ++ src/firefox/src/background.js | 13 +- src/firefox/src/ui/i18n.js | 12 +- src/firefox/src/ui/install.css | 474 +++++++++++++++++++++++++++++++ src/firefox/src/ui/install.html | 92 ++++++ src/firefox/src/ui/install.js | 133 +++++++++ src/firefox/src/ui/locales/ar.js | 18 ++ src/firefox/src/ui/locales/en.js | 18 ++ src/firefox/src/ui/locales/es.js | 18 ++ src/firefox/src/ui/locales/fr.js | 18 ++ src/firefox/src/ui/locales/he.js | 18 ++ src/firefox/src/ui/locales/id.js | 18 ++ src/firefox/src/ui/locales/ja.js | 18 ++ src/firefox/src/ui/locales/ko.js | 18 ++ src/firefox/src/ui/locales/ms.js | 18 ++ src/firefox/src/ui/locales/pl.js | 18 ++ src/firefox/src/ui/locales/ru.js | 18 ++ src/firefox/src/ui/locales/th.js | 18 ++ src/firefox/src/ui/locales/tl.js | 18 ++ src/firefox/src/ui/locales/tr.js | 18 ++ src/firefox/src/ui/locales/uk.js | 18 ++ src/firefox/src/ui/locales/zh.js | 18 ++ test/run.js | 102 +++++++ 43 files changed, 2114 insertions(+), 12 deletions(-) create mode 100644 src/chrome/src/ui/install.css create mode 100644 src/chrome/src/ui/install.html create mode 100644 src/chrome/src/ui/install.js create mode 100644 src/firefox/src/ui/install.css create mode 100644 src/firefox/src/ui/install.html create mode 100644 src/firefox/src/ui/install.js diff --git a/src/chrome/src/background.js b/src/chrome/src/background.js index 3d5fa22f..d671eb03 100644 --- a/src/chrome/src/background.js +++ b/src/chrome/src/background.js @@ -731,8 +731,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(); diff --git a/src/chrome/src/ui/i18n.js b/src/chrome/src/ui/i18n.js index c147ce94..6c1644c4 100644 --- a/src/chrome/src/ui/i18n.js +++ b/src/chrome/src/ui/i18n.js @@ -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'; } @@ -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); + } } diff --git a/src/chrome/src/ui/install.css b/src/chrome/src/ui/install.css new file mode 100644 index 00000000..502057a2 --- /dev/null +++ b/src/chrome/src/ui/install.css @@ -0,0 +1,474 @@ +:root { + color-scheme: dark; + --page: #121325; + --card: #191b31; + --panel: #222541; + --panel-strong: #2a2e50; + --text: #f5f3ff; + --muted: #aaa9c3; + --quiet: #9292aa; + --accent: #837cff; + --accent-strong: #6c63ff; + --accent-soft: rgba(131, 124, 255, 0.15); + --line: rgba(255, 255, 255, 0.1); + --success: #6bd6a2; + --shadow: 0 28px 80px rgba(3, 4, 16, 0.42); +} + +:root[data-theme="light"] { + color-scheme: light; + --page: #f2eadc; + --card: #fffdf8; + --panel: #f7f0e4; + --panel-strong: #eee4d5; + --text: #2c1810; + --muted: #6b5b47; + --quiet: #766a58; + --accent: #675ee9; + --accent-strong: #5147d7; + --accent-soft: rgba(91, 82, 232, 0.12); + --line: rgba(89, 55, 25, 0.14); + --success: #27845f; + --shadow: 0 28px 80px rgba(80, 52, 26, 0.14); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; + background: var(--page); + color: var(--text); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +button, +kbd { + font: inherit; +} + +.install-page { + min-height: 100vh; + display: grid; + place-items: center; + padding: 42px 20px; +} + +.install-card { + width: min(760px, 100%); + overflow: hidden; + background: var(--card); + border: 1px solid var(--line); + border-radius: 24px; + box-shadow: var(--shadow); +} + +.install-header { + min-height: 78px; + padding: 16px 22px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 18px; + border-bottom: 1px solid var(--line); +} + +.brand { + display: flex; + align-items: center; + gap: 12px; +} + +.brand img { + border-radius: 11px; +} + +.installed-label { + font-size: 14px; + font-weight: 720; + letter-spacing: -0.01em; +} + +.browser-label { + margin-top: 3px; + color: var(--quiet); + font-size: 12px; +} + +.status-chip { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 7px 10px; + border: 1px solid var(--line); + border-radius: 999px; + color: var(--muted); + font-size: 11px; + font-weight: 650; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.status-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--success); + box-shadow: 0 0 0 4px color-mix(in srgb, var(--success) 15%, transparent); +} + +.toolbar-rehearsal { + position: relative; + height: 190px; + margin: 28px 28px 0; + padding: 17px; + display: grid; + grid-template-columns: auto minmax(120px, 1fr) auto; + align-items: start; + gap: 14px; + border: 1px solid var(--line); + border-radius: 16px; + background: var(--panel); +} + +.window-controls { + display: flex; + gap: 6px; + padding-top: 11px; +} + +.window-controls span { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--line); +} + +.address-field { + min-width: 0; + height: 34px; + padding: 9px 13px; + overflow: hidden; + border: 1px solid var(--line); + border-radius: 9px; + background: var(--card); + color: var(--quiet); + font-size: 11px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.toolbar-action { + position: relative; + width: 34px; + height: 34px; + display: grid; + place-items: center; + border: 1px solid color-mix(in srgb, var(--accent) 55%, transparent); + border-radius: 9px; + background: var(--accent-soft); + color: var(--accent); +} + +.toolbar-action svg { + width: 17px; + height: 17px; + fill: currentColor; +} + +.action-number { + position: absolute; + top: -9px; + right: -9px; + z-index: 2; + width: 19px; + height: 19px; + display: grid; + place-items: center; + border: 2px solid var(--panel); + border-radius: 50%; + background: var(--accent-strong); + color: white; + font-size: 9px; + font-weight: 800; +} + +.toolbar-action-extensions::after { + content: ""; + position: absolute; + inset: -7px; + border: 1px solid var(--accent); + border-radius: 13px; + animation: attention 1.8s ease-out infinite; +} + +.extension-menu { + position: absolute; + top: 63px; + right: 17px; + width: min(248px, calc(100% - 34px)); + padding: 13px; + border: 1px solid var(--line); + border-radius: 12px; + background: var(--card); + box-shadow: 0 16px 30px rgba(3, 4, 16, 0.22); +} + +.menu-title { + margin: 0 0 10px 2px; + color: var(--quiet); + font-size: 10px; + font-weight: 750; + text-transform: uppercase; + letter-spacing: 0.1em; +} + +.extension-row { + min-height: 48px; + padding: 9px 10px; + display: grid; + grid-template-columns: 26px 1fr 34px; + align-items: center; + gap: 10px; + border: 1px solid var(--line); + border-radius: 9px; + background: var(--panel); + font-size: 12px; + font-weight: 650; +} + +.extension-row img { + border-radius: 7px; +} + +.pin-target { + position: relative; + width: 30px; + height: 30px; + display: grid; + place-items: center; + border-radius: 8px; + background: var(--accent-strong); + color: white; +} + +.pin-target svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 1.8; + stroke-linecap: round; + stroke-linejoin: round; +} + +.pin-target .action-number { + border-color: var(--card); +} + +.install-copy { + padding: 34px 44px 0; +} + +.eyebrow { + margin: 0 0 8px; + color: var(--accent); + font-size: 11px; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.11em; +} + +h1 { + max-width: 590px; + margin: 0; + font-size: clamp(28px, 5vw, 43px); + line-height: 1.06; + letter-spacing: -0.042em; +} + +.intro { + max-width: 600px; + margin: 15px 0 0; + color: var(--muted); + font-size: 15px; + line-height: 1.6; +} + +.guide-steps { + margin: 28px 44px 34px; + padding: 0; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + list-style: none; +} + +.guide-steps li { + min-width: 0; + padding: 0 20px; + display: flex; + gap: 11px; + border-left: 1px solid var(--line); +} + +.guide-steps li:first-child { + padding-left: 0; + border-left: 0; +} + +.guide-steps li:last-child { + padding-right: 0; +} + +.step-number { + flex: 0 0 auto; + width: 22px; + height: 22px; + display: grid; + place-items: center; + border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent); + border-radius: 7px; + background: var(--accent-soft); + color: var(--accent); + font-size: 10px; + font-weight: 800; +} + +.guide-steps strong { + display: block; + margin: 2px 0 7px; + font-size: 12px; +} + +.guide-steps p { + margin: 0; + color: var(--quiet); + font-size: 11px; + line-height: 1.5; +} + +kbd { + display: inline-block; + padding: 1px 4px; + border: 1px solid var(--line); + border-bottom-width: 2px; + border-radius: 4px; + background: var(--panel); + color: var(--muted); + font-size: 9px; +} + +.install-footer { + min-height: 72px; + padding: 14px 22px 14px 44px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + border-top: 1px solid var(--line); + background: color-mix(in srgb, var(--panel) 38%, transparent); +} + +.install-footer p { + margin: 0; + color: var(--quiet); + font-size: 11px; + line-height: 1.45; +} + +.install-footer button { + flex: 0 0 auto; + min-width: 104px; + padding: 11px 18px; + border: 0; + border-radius: 9px; + background: var(--accent-strong); + color: white; + cursor: pointer; + font-weight: 750; + box-shadow: 0 8px 18px color-mix(in srgb, var(--accent-strong) 25%, transparent); + transition: transform 150ms ease, background 150ms ease; +} + +.install-footer button:hover { + background: var(--accent); + transform: translateY(-1px); +} + +.install-footer button:focus-visible { + outline: 3px solid color-mix(in srgb, var(--accent) 50%, transparent); + outline-offset: 3px; +} + +@keyframes attention { + 0% { + opacity: 0.8; + transform: scale(0.82); + } + 72%, + 100% { + opacity: 0; + transform: scale(1.14); + } +} + +@media (max-width: 680px) { + .install-page { + padding: 16px 10px; + } + + .install-card { + border-radius: 18px; + } + + .toolbar-rehearsal { + margin: 18px 14px 0; + } + + .install-copy { + padding: 28px 22px 0; + } + + .guide-steps { + margin: 25px 22px 28px; + grid-template-columns: 1fr; + gap: 18px; + } + + .guide-steps li, + .guide-steps li:first-child, + .guide-steps li:last-child { + padding: 0; + border-left: 0; + } + + .install-footer { + padding: 18px 22px; + align-items: stretch; + flex-direction: column; + } + + .install-footer button { + width: 100%; + } +} + +@media (max-width: 420px) { + .status-chip { + display: none; + } +} + +@media (prefers-reduced-motion: reduce) { + .toolbar-action-extensions::after { + animation: none; + opacity: 0.55; + } + + .install-footer button { + transition: none; + } +} diff --git a/src/chrome/src/ui/install.html b/src/chrome/src/ui/install.html new file mode 100644 index 00000000..6eeafe3d --- /dev/null +++ b/src/chrome/src/ui/install.html @@ -0,0 +1,92 @@ + + + + + + WebBrain installed + + + + +
+
+
+
+ +
+
WebBrain installed
+
Checking your browser…
+
+
+ + + Ready + +
+ + + +
+

One small setup step

+

Keep WebBrain one click away

+

Your browser may hide newly installed extensions. Keep WebBrain visible so it stays easy to open.

+
+ +
    +
  1. + 1 +
    + Open Extensions +

    Open your browser’s extension controls near the address bar.

    +
    +
  2. +
  3. + 2 +
    + Keep WebBrain visible +

    Find WebBrain and choose the pin, show, or keep-in-toolbar option.

    +
    +
  4. +
  5. + 3 +
    + Open it from any page +

    Select the WebBrain icon, or press Alt+Shift+W.

    +
    +
  6. +
+ +
+

This cannot be done automatically—your browser leaves toolbar choices to you.

+ +
+
+
+ + + + diff --git a/src/chrome/src/ui/install.js b/src/chrome/src/ui/install.js new file mode 100644 index 00000000..adc686c0 --- /dev/null +++ b/src/chrome/src/ui/install.js @@ -0,0 +1,133 @@ +import { applyDOMTranslations, getLocale, t } from './i18n.js'; + +const GUIDES = { + chrome: { + name: 'Google Chrome', + intro: 'Chrome usually places new extensions inside its Extensions menu. Pin WebBrain so the side panel stays one click away.', + firstTitle: 'Open Extensions', + firstBody: 'Select the puzzle-piece icon to the right of the address bar.', + secondTitle: 'Pin WebBrain', + secondBody: 'Find WebBrain, then select the pin icon beside it.', + }, + edge: { + name: 'Microsoft Edge', + intro: 'Edge may keep new extensions inside its Extensions menu. Keep WebBrain on the toolbar so the side panel stays one click away.', + firstTitle: 'Open Extensions', + firstBody: 'Select the Extensions icon to the right of the address bar.', + secondTitle: 'Show WebBrain', + secondBody: 'Find WebBrain, then choose Show in toolbar.', + }, + brave: { + name: 'Brave', + intro: 'Brave may tuck new extensions into its Extensions menu. Pin WebBrain so the side panel stays one click away.', + firstTitle: 'Open Extensions', + firstBody: 'Select the puzzle-piece icon to the right of the address bar.', + secondTitle: 'Pin WebBrain', + secondBody: 'Find WebBrain, then select the pin icon beside it.', + }, + vivaldi: { + name: 'Vivaldi', + intro: 'Vivaldi can hide extension buttons beside the Address Bar. Keep WebBrain visible so the side panel stays one click away.', + firstTitle: 'Check the Address Bar', + firstBody: 'Look for the extension controls on the right side of the Address Bar.', + secondTitle: 'Show WebBrain', + secondBody: 'If it is hidden, open hidden extensions, right-click WebBrain, and choose Show Button.', + }, + opera: { + name: 'Opera', + intro: 'Opera may keep new extensions inside its Extensions menu. Pin WebBrain so the side panel stays one click away.', + firstTitle: 'Open Extensions', + firstBody: 'Select the Extensions icon to the right of the address bar.', + secondTitle: 'Pin WebBrain', + secondBody: 'Find WebBrain, then use its pin control to keep it on the toolbar.', + }, + firefox: { + name: 'Firefox', + intro: 'Firefox may place new add-ons inside its Extensions panel. Pin WebBrain so the sidebar stays one click away.', + firstTitle: 'Open Extensions', + firstBody: 'Select the puzzle-piece icon in the toolbar.', + secondTitle: 'Pin WebBrain', + secondBody: 'Open the gear menu beside WebBrain, then choose Pin to Toolbar.', + }, + chromium: { + name: 'Chromium', + intro: 'Your browser may hide new extensions in its extension controls. Keep WebBrain visible so the side panel stays one click away.', + firstTitle: 'Open extension controls', + firstBody: 'Look beside the address bar for Extensions or hidden extension buttons.', + secondTitle: 'Keep WebBrain visible', + secondBody: 'Find WebBrain and use the pin, show, or keep-in-toolbar control.', + }, + unknown: { + name: 'Browser', + intro: 'Your browser may hide new extensions in its extension controls. Keep WebBrain visible so it stays easy to open.', + firstTitle: 'Open extension controls', + firstBody: 'Look near the address bar or in your browser menu for Extensions or Add-ons.', + secondTitle: 'Keep WebBrain visible', + secondBody: 'Find WebBrain and choose the option that keeps it in your toolbar.', + }, +}; + +function browserBrands(navigatorLike) { + return (navigatorLike?.userAgentData?.brands || []) + .map((entry) => String(entry?.brand || '').toLowerCase()) + .join(' '); +} + +export async function detectBrowser(navigatorLike = globalThis.navigator) { + try { + if (await navigatorLike?.brave?.isBrave?.()) return 'brave'; + } catch { + // Brave detection is an optional progressive enhancement. + } + + const ua = String(navigatorLike?.userAgent || ''); + const brands = browserBrands(navigatorLike); + if (/Firefox\//i.test(ua)) return 'firefox'; + if (/\bEdg(?:e|A|iOS)?\//i.test(ua) || brands.includes('microsoft edge')) return 'edge'; + if (/\bOPR\//i.test(ua) || brands.includes('opera')) return 'opera'; + if (/\bVivaldi\//i.test(ua) || brands.includes('vivaldi')) return 'vivaldi'; + if (brands.includes('google chrome')) return 'chrome'; + if (/\b(?:Chrome|Chromium)\//i.test(ua) || brands.includes('chromium')) return 'chromium'; + return 'unknown'; +} + +export function getBrowserGuide(browserKey) { + return GUIDES[browserKey] || GUIDES.unknown; +} + +async function closeGuideTab() { + const api = globalThis.browser?.tabs ? globalThis.browser : globalThis.chrome; + try { + const tab = await api?.tabs?.getCurrent?.(); + if (tab?.id != null) { + await api.tabs.remove(tab.id); + return; + } + } catch { + // window.close() is a harmless fallback when tab lookup is unavailable. + } + window.close(); +} + +async function hydrateGuide() { + const browserKey = await detectBrowser(); + const guide = getBrowserGuide(browserKey); + applyDOMTranslations(document); + document.documentElement.dataset.browser = browserKey; + document.getElementById('browser-label').textContent = t('install.browser.detected', { browser: guide.name }); + if (getLocale() === 'en') { + document.getElementById('install-intro').textContent = guide.intro; + document.getElementById('step-one-title').textContent = guide.firstTitle; + document.getElementById('step-one-body').textContent = guide.firstBody; + document.getElementById('step-two-title').textContent = guide.secondTitle; + document.getElementById('step-two-body').textContent = guide.secondBody; + } + if (document.documentElement.dataset.build === 'firefox') { + document.getElementById('shortcut-hint').remove(); + } + document.getElementById('done-button').addEventListener('click', closeGuideTab); +} + +if (typeof document !== 'undefined') { + hydrateGuide().catch(() => {}); +} diff --git a/src/chrome/src/ui/locales/ar.js b/src/chrome/src/ui/locales/ar.js index 4220912a..517e077d 100644 --- a/src/chrome/src/ui/locales/ar.js +++ b/src/chrome/src/ui/locales/ar.js @@ -33,6 +33,24 @@ export default { 'st.perms.gate.label': "السؤال قبل الإجراءات المهمة", 'brand': 'WebBrain', + 'install.page_title': 'تم تثبيت WebBrain', + 'install.installed': 'تم تثبيت WebBrain', + 'install.checking': 'جارٍ التحقق من متصفحك…', + 'install.ready': 'جاهز', + 'install.extensions': 'الإضافات', + 'install.eyebrow': 'خطوة إعداد صغيرة', + 'install.title': 'اجعل WebBrain على بُعد نقرة', + 'install.intro': 'قد يخفي متصفحك الإضافات المثبتة حديثًا. أبقِ WebBrain ظاهرًا ليسهل فتحه.', + 'install.open_extensions.title': 'افتح الإضافات', + 'install.open_extensions.body': 'افتح عناصر التحكم في إضافات المتصفح بالقرب من شريط العناوين.', + 'install.keep_visible.title': 'أبقِ WebBrain ظاهرًا', + 'install.keep_visible.body': 'ابحث عن WebBrain واختر التثبيت أو الإظهار أو الإبقاء في شريط الأدوات.', + 'install.open.title': 'افتحه من أي صفحة', + 'install.open.body': 'حدد أيقونة WebBrain', + 'install.open.shortcut_html': '، أو اضغط Alt+Shift+W', + 'install.footer': 'لا يمكن تنفيذ ذلك تلقائيًا؛ يترك لك المتصفح اختيار عناصر شريط الأدوات.', + 'install.done': 'فهمت', + 'install.browser.detected': 'تم اكتشاف {browser}', // --- Onboarding -------------------------------------------------------- 'ob.step': '{current} من {total}', diff --git a/src/chrome/src/ui/locales/en.js b/src/chrome/src/ui/locales/en.js index 5d8840c4..3816bb01 100644 --- a/src/chrome/src/ui/locales/en.js +++ b/src/chrome/src/ui/locales/en.js @@ -9,6 +9,24 @@ export default { 'st.providers.select_for_chat': 'Select for chat', // Brand name: kept in English across locales. 'brand': 'WebBrain', + 'install.page_title': 'WebBrain installed', + 'install.installed': 'WebBrain installed', + 'install.checking': 'Checking your browser…', + 'install.ready': 'Ready', + 'install.extensions': 'Extensions', + 'install.eyebrow': 'One small setup step', + 'install.title': 'Keep WebBrain one click away', + 'install.intro': 'Your browser may hide newly installed extensions. Keep WebBrain visible so it stays easy to open.', + 'install.open_extensions.title': 'Open Extensions', + 'install.open_extensions.body': 'Open your browser’s extension controls near the address bar.', + 'install.keep_visible.title': 'Keep WebBrain visible', + 'install.keep_visible.body': 'Find WebBrain and choose the pin, show, or keep-in-toolbar option.', + 'install.open.title': 'Open it from any page', + 'install.open.body': 'Select the WebBrain icon', + 'install.open.shortcut_html': ', or press Alt+Shift+W', + 'install.footer': 'This cannot be done automatically—your browser leaves toolbar choices to you.', + 'install.done': 'Got it', + 'install.browser.detected': '{browser} detected', // --- Sidepanel --------------------------------------------------------- 'sp.status.disconnected': 'Disconnected', diff --git a/src/chrome/src/ui/locales/es.js b/src/chrome/src/ui/locales/es.js index f03ab878..d0672308 100644 --- a/src/chrome/src/ui/locales/es.js +++ b/src/chrome/src/ui/locales/es.js @@ -33,6 +33,24 @@ export default { 'st.perms.gate.label': "Preguntar antes de acciones importantes", 'brand': 'WebBrain', + 'install.page_title': 'WebBrain instalado', + 'install.installed': 'WebBrain instalado', + 'install.checking': 'Comprobando tu navegador…', + 'install.ready': 'Listo', + 'install.extensions': 'Extensiones', + 'install.eyebrow': 'Un pequeño paso de configuración', + 'install.title': 'Mantén WebBrain a un clic', + 'install.intro': 'Tu navegador puede ocultar las extensiones recién instaladas. Mantén WebBrain visible para abrirlo fácilmente.', + 'install.open_extensions.title': 'Abre Extensiones', + 'install.open_extensions.body': 'Abre los controles de extensiones del navegador junto a la barra de direcciones.', + 'install.keep_visible.title': 'Mantén WebBrain visible', + 'install.keep_visible.body': 'Busca WebBrain y elige la opción para fijarlo, mostrarlo o mantenerlo en la barra de herramientas.', + 'install.open.title': 'Ábrelo desde cualquier página', + 'install.open.body': 'Selecciona el icono de WebBrain', + 'install.open.shortcut_html': ', o pulsa Alt+Mayús+W', + 'install.footer': 'Esto no puede hacerse automáticamente: tú decides qué aparece en la barra de herramientas.', + 'install.done': 'Entendido', + 'install.browser.detected': '{browser} detectado', // --- Onboarding -------------------------------------------------------- 'ob.step': '{current} de {total}', diff --git a/src/chrome/src/ui/locales/fr.js b/src/chrome/src/ui/locales/fr.js index 2105a810..cd137b30 100644 --- a/src/chrome/src/ui/locales/fr.js +++ b/src/chrome/src/ui/locales/fr.js @@ -33,6 +33,24 @@ export default { 'st.perms.gate.label': "Demander avant les actions importantes", 'brand': 'WebBrain', + 'install.page_title': 'WebBrain installé', + 'install.installed': 'WebBrain installé', + 'install.checking': 'Vérification de votre navigateur…', + 'install.ready': 'Prêt', + 'install.extensions': 'Extensions', + 'install.eyebrow': 'Une petite étape de configuration', + 'install.title': 'Gardez WebBrain à portée de clic', + 'install.intro': 'Votre navigateur peut masquer les extensions récemment installées. Gardez WebBrain visible pour pouvoir l’ouvrir facilement.', + 'install.open_extensions.title': 'Ouvrez Extensions', + 'install.open_extensions.body': 'Ouvrez les commandes d’extensions du navigateur près de la barre d’adresse.', + 'install.keep_visible.title': 'Gardez WebBrain visible', + 'install.keep_visible.body': 'Repérez WebBrain et choisissez l’option pour l’épingler, l’afficher ou le garder dans la barre d’outils.', + 'install.open.title': 'Ouvrez-le depuis n’importe quelle page', + 'install.open.body': 'Sélectionnez l’icône WebBrain', + 'install.open.shortcut_html': ', ou appuyez sur Alt+Maj+W', + 'install.footer': 'Cette action ne peut pas être automatique : votre navigateur vous laisse choisir les éléments de la barre d’outils.', + 'install.done': 'Compris', + 'install.browser.detected': '{browser} détecté', // --- Onboarding -------------------------------------------------------- 'ob.step': '{current} sur {total}', diff --git a/src/chrome/src/ui/locales/he.js b/src/chrome/src/ui/locales/he.js index 068b0238..27d34fa8 100644 --- a/src/chrome/src/ui/locales/he.js +++ b/src/chrome/src/ui/locales/he.js @@ -8,6 +8,24 @@ export default { 'st.providers.selected': 'נבחר', 'st.providers.select_for_chat': 'בחירה לצ׳אט', "brand": "WebBrain", + 'install.page_title': 'WebBrain הותקן', + 'install.installed': 'WebBrain הותקן', + 'install.checking': 'בודק את הדפדפן שלך…', + 'install.ready': 'מוכן', + 'install.extensions': 'הרחבות', + 'install.eyebrow': 'עוד שלב הגדרה קטן', + 'install.title': 'WebBrain במרחק לחיצה אחת', + 'install.intro': 'הדפדפן עשוי להסתיר הרחבות שהותקנו לאחרונה. כדאי להשאיר את WebBrain גלוי כדי לפתוח אותו בקלות.', + 'install.open_extensions.title': 'פתיחת ההרחבות', + 'install.open_extensions.body': 'פתח את פקדי ההרחבות ליד שורת הכתובת.', + 'install.keep_visible.title': 'השארת WebBrain גלוי', + 'install.keep_visible.body': 'מצא את WebBrain ובחר להצמיד, להציג או להשאיר אותו בסרגל הכלים.', + 'install.open.title': 'פתיחה מכל דף', + 'install.open.body': 'בחר בסמל WebBrain', + 'install.open.shortcut_html': ', או הקש Alt+Shift+W', + 'install.footer': 'לא ניתן לבצע זאת אוטומטית — הדפדפן משאיר לך את הבחירה מה יוצג בסרגל הכלים.', + 'install.done': 'הבנתי', + 'install.browser.detected': '{browser} זוהה', "sp.status.disconnected": "לא מחובר", "sp.status.connected": "מחובר ({model})", "sp.status.error": "שגיאה: {msg}", diff --git a/src/chrome/src/ui/locales/id.js b/src/chrome/src/ui/locales/id.js index 10230896..ce7d05fc 100644 --- a/src/chrome/src/ui/locales/id.js +++ b/src/chrome/src/ui/locales/id.js @@ -33,6 +33,24 @@ export default { 'st.perms.gate.label': "Tanya sebelum tindakan penting", 'brand': 'WebBrain', + 'install.page_title': 'WebBrain terpasang', + 'install.installed': 'WebBrain terpasang', + 'install.checking': 'Memeriksa browser Anda…', + 'install.ready': 'Siap', + 'install.extensions': 'Ekstensi', + 'install.eyebrow': 'Satu langkah penyiapan kecil', + 'install.title': 'Buka WebBrain dengan sekali klik', + 'install.intro': 'Browser Anda mungkin menyembunyikan ekstensi yang baru dipasang. Biarkan WebBrain terlihat agar mudah dibuka.', + 'install.open_extensions.title': 'Buka Ekstensi', + 'install.open_extensions.body': 'Buka kontrol ekstensi browser di dekat bilah alamat.', + 'install.keep_visible.title': 'Biarkan WebBrain terlihat', + 'install.keep_visible.body': 'Temukan WebBrain lalu pilih opsi sematkan, tampilkan, atau pertahankan di bilah alat.', + 'install.open.title': 'Buka dari halaman mana pun', + 'install.open.body': 'Pilih ikon WebBrain', + 'install.open.shortcut_html': ', atau tekan Alt+Shift+W', + 'install.footer': 'Ini tidak dapat dilakukan secara otomatis—browser menyerahkan pilihan bilah alat kepada Anda.', + 'install.done': 'Mengerti', + 'install.browser.detected': '{browser} terdeteksi', // --- Onboarding -------------------------------------------------------- 'ob.step': '{current} dari {total}', diff --git a/src/chrome/src/ui/locales/ja.js b/src/chrome/src/ui/locales/ja.js index d678b0f8..a7352aa7 100644 --- a/src/chrome/src/ui/locales/ja.js +++ b/src/chrome/src/ui/locales/ja.js @@ -33,6 +33,24 @@ export default { 'st.perms.gate.label': "重要な操作の前に確認する", 'brand': 'WebBrain', + 'install.page_title': 'WebBrain をインストールしました', + 'install.installed': 'WebBrain をインストールしました', + 'install.checking': 'ブラウザーを確認しています…', + 'install.ready': '準備完了', + 'install.extensions': '拡張機能', + 'install.eyebrow': 'あとひとつの簡単な設定', + 'install.title': 'WebBrain をワンクリックで開けるように', + 'install.intro': '新しくインストールした拡張機能はブラウザーに隠れる場合があります。WebBrain を表示して、すぐ開けるようにしましょう。', + 'install.open_extensions.title': '拡張機能を開く', + 'install.open_extensions.body': 'アドレスバーの近くにある拡張機能のコントロールを開きます。', + 'install.keep_visible.title': 'WebBrain を表示する', + 'install.keep_visible.body': 'WebBrain を見つけ、固定、表示、またはツールバーに残すオプションを選びます。', + 'install.open.title': 'どのページからでも開く', + 'install.open.body': 'WebBrain アイコンを選択', + 'install.open.shortcut_html': '、または Alt+Shift+W を押します', + 'install.footer': 'これは自動では行えません。ツールバーに表示する項目はブラウザー上で選択してください。', + 'install.done': '了解', + 'install.browser.detected': '{browser} を検出しました', // --- Onboarding -------------------------------------------------------- 'ob.step': '{current} / {total}', diff --git a/src/chrome/src/ui/locales/ko.js b/src/chrome/src/ui/locales/ko.js index 4758ed62..74aaad00 100644 --- a/src/chrome/src/ui/locales/ko.js +++ b/src/chrome/src/ui/locales/ko.js @@ -33,6 +33,24 @@ export default { 'st.perms.gate.label': "중요한 작업 전에 확인", 'brand': 'WebBrain', + 'install.page_title': 'WebBrain 설치 완료', + 'install.installed': 'WebBrain 설치 완료', + 'install.checking': '브라우저를 확인하는 중…', + 'install.ready': '준비됨', + 'install.extensions': '확장 프로그램', + 'install.eyebrow': '간단한 설정 한 단계', + 'install.title': 'WebBrain을 한 번의 클릭으로 여세요', + 'install.intro': '브라우저가 새로 설치한 확장 프로그램을 숨길 수 있습니다. WebBrain을 표시해 두면 쉽게 열 수 있습니다.', + 'install.open_extensions.title': '확장 프로그램 열기', + 'install.open_extensions.body': '주소 표시줄 근처의 브라우저 확장 프로그램 컨트롤을 엽니다.', + 'install.keep_visible.title': 'WebBrain 표시하기', + 'install.keep_visible.body': 'WebBrain을 찾아 고정, 표시 또는 도구 모음에 유지하는 옵션을 선택합니다.', + 'install.open.title': '어느 페이지에서나 열기', + 'install.open.body': 'WebBrain 아이콘을 선택하세요', + 'install.open.shortcut_html': ' 또는 Alt+Shift+W를 누르세요', + 'install.footer': '이 작업은 자동으로 할 수 없습니다. 도구 모음 항목은 브라우저에서 직접 선택해야 합니다.', + 'install.done': '확인', + 'install.browser.detected': '{browser} 감지됨', // --- Onboarding -------------------------------------------------------- 'ob.step': '{current} / {total}', diff --git a/src/chrome/src/ui/locales/ms.js b/src/chrome/src/ui/locales/ms.js index c3b7fd3c..4446a191 100644 --- a/src/chrome/src/ui/locales/ms.js +++ b/src/chrome/src/ui/locales/ms.js @@ -33,6 +33,24 @@ export default { 'st.perms.gate.label': "Tanya sebelum tindakan penting", 'brand': 'WebBrain', + 'install.page_title': 'WebBrain dipasang', + 'install.installed': 'WebBrain dipasang', + 'install.checking': 'Memeriksa pelayar anda…', + 'install.ready': 'Sedia', + 'install.extensions': 'Sambungan', + 'install.eyebrow': 'Satu langkah persediaan ringkas', + 'install.title': 'Buka WebBrain dengan satu klik', + 'install.intro': 'Pelayar anda mungkin menyembunyikan sambungan yang baru dipasang. Pastikan WebBrain kelihatan supaya mudah dibuka.', + 'install.open_extensions.title': 'Buka Sambungan', + 'install.open_extensions.body': 'Buka kawalan sambungan pelayar berhampiran bar alamat.', + 'install.keep_visible.title': 'Pastikan WebBrain kelihatan', + 'install.keep_visible.body': 'Cari WebBrain dan pilih pilihan semat, tunjuk atau kekalkan pada bar alat.', + 'install.open.title': 'Buka dari mana-mana halaman', + 'install.open.body': 'Pilih ikon WebBrain', + 'install.open.shortcut_html': ', atau tekan Alt+Shift+W', + 'install.footer': 'Ini tidak boleh dilakukan secara automatik—pelayar menyerahkan pilihan bar alat kepada anda.', + 'install.done': 'Faham', + 'install.browser.detected': '{browser} dikesan', // --- Onboarding -------------------------------------------------------- 'ob.step': '{current} daripada {total}', diff --git a/src/chrome/src/ui/locales/pl.js b/src/chrome/src/ui/locales/pl.js index 00335318..74186cda 100644 --- a/src/chrome/src/ui/locales/pl.js +++ b/src/chrome/src/ui/locales/pl.js @@ -8,6 +8,24 @@ export default { 'st.providers.selected': 'WYBRANO', 'st.providers.select_for_chat': 'Wybierz do czatu', 'brand': 'WebBrain', + 'install.page_title': 'WebBrain zainstalowany', + 'install.installed': 'WebBrain zainstalowany', + 'install.checking': 'Sprawdzamy Twoją przeglądarkę…', + 'install.ready': 'Gotowe', + 'install.extensions': 'Rozszerzenia', + 'install.eyebrow': 'Jeszcze jeden mały krok', + 'install.title': 'Miej WebBrain zawsze pod ręką', + 'install.intro': 'Przeglądarka może ukrywać nowo zainstalowane rozszerzenia. Pozostaw WebBrain widoczny, aby łatwo go otwierać.', + 'install.open_extensions.title': 'Otwórz rozszerzenia', + 'install.open_extensions.body': 'Otwórz elementy sterowania rozszerzeniami obok paska adresu.', + 'install.keep_visible.title': 'Pozostaw WebBrain widoczny', + 'install.keep_visible.body': 'Znajdź WebBrain i wybierz przypięcie, pokazanie lub pozostawienie na pasku narzędzi.', + 'install.open.title': 'Otwieraj z dowolnej strony', + 'install.open.body': 'Wybierz ikonę WebBrain', + 'install.open.shortcut_html': ' lub naciśnij Alt+Shift+W', + 'install.footer': 'Nie można zrobić tego automatycznie — przeglądarka pozostawia wybór elementów paska narzędzi Tobie.', + 'install.done': 'Rozumiem', + 'install.browser.detected': 'Wykryto {browser}', 'sp.status.disconnected': 'Rozłączono', 'sp.status.connected': 'Połączono ({model})', 'sp.status.error': 'Błąd: {msg}', diff --git a/src/chrome/src/ui/locales/ru.js b/src/chrome/src/ui/locales/ru.js index 495bad1c..d72419dd 100644 --- a/src/chrome/src/ui/locales/ru.js +++ b/src/chrome/src/ui/locales/ru.js @@ -33,6 +33,24 @@ export default { 'st.perms.gate.label': "Спрашивать перед важными действиями", 'brand': 'WebBrain', + 'install.page_title': 'WebBrain установлен', + 'install.installed': 'WebBrain установлен', + 'install.checking': 'Проверяем ваш браузер…', + 'install.ready': 'Готово', + 'install.extensions': 'Расширения', + 'install.eyebrow': 'Остался один небольшой шаг', + 'install.title': 'Держите WebBrain под рукой', + 'install.intro': 'Браузер может скрывать недавно установленные расширения. Оставьте WebBrain на виду, чтобы легко его открывать.', + 'install.open_extensions.title': 'Откройте расширения', + 'install.open_extensions.body': 'Откройте элементы управления расширениями рядом с адресной строкой.', + 'install.keep_visible.title': 'Оставьте WebBrain на виду', + 'install.keep_visible.body': 'Найдите WebBrain и выберите закрепление, отображение или сохранение на панели инструментов.', + 'install.open.title': 'Открывайте с любой страницы', + 'install.open.body': 'Нажмите значок WebBrain', + 'install.open.shortcut_html': ' или клавиши Alt+Shift+W', + 'install.footer': 'Это нельзя сделать автоматически — браузер оставляет выбор элементов панели инструментов за вами.', + 'install.done': 'Понятно', + 'install.browser.detected': 'Обнаружен {browser}', // --- Onboarding -------------------------------------------------------- 'ob.step': '{current} из {total}', diff --git a/src/chrome/src/ui/locales/th.js b/src/chrome/src/ui/locales/th.js index 8030e799..35f606c8 100644 --- a/src/chrome/src/ui/locales/th.js +++ b/src/chrome/src/ui/locales/th.js @@ -33,6 +33,24 @@ export default { 'st.perms.gate.label': "ถามก่อนการดำเนินการสำคัญ", 'brand': 'WebBrain', + 'install.page_title': 'ติดตั้ง WebBrain แล้ว', + 'install.installed': 'ติดตั้ง WebBrain แล้ว', + 'install.checking': 'กำลังตรวจสอบเบราว์เซอร์ของคุณ…', + 'install.ready': 'พร้อม', + 'install.extensions': 'ส่วนขยาย', + 'install.eyebrow': 'ตั้งค่าอีกเพียงขั้นตอนเดียว', + 'install.title': 'เปิด WebBrain ได้ในคลิกเดียว', + 'install.intro': 'เบราว์เซอร์อาจซ่อนส่วนขยายที่เพิ่งติดตั้ง ให้ WebBrain แสดงอยู่เสมอเพื่อเปิดใช้งานได้ง่าย', + 'install.open_extensions.title': 'เปิดส่วนขยาย', + 'install.open_extensions.body': 'เปิดตัวควบคุมส่วนขยายของเบราว์เซอร์ใกล้แถบที่อยู่', + 'install.keep_visible.title': 'ให้ WebBrain แสดงอยู่เสมอ', + 'install.keep_visible.body': 'ค้นหา WebBrain แล้วเลือกปักหมุด แสดง หรือเก็บไว้บนแถบเครื่องมือ', + 'install.open.title': 'เปิดจากหน้าใดก็ได้', + 'install.open.body': 'เลือกไอคอน WebBrain', + 'install.open.shortcut_html': ' หรือกด Alt+Shift+W', + 'install.footer': 'ไม่สามารถทำขั้นตอนนี้โดยอัตโนมัติได้ เบราว์เซอร์ให้คุณเลือกสิ่งที่จะแสดงบนแถบเครื่องมือ', + 'install.done': 'เข้าใจแล้ว', + 'install.browser.detected': 'ตรวจพบ {browser}', // --- Onboarding -------------------------------------------------------- 'ob.step': '{current} จาก {total}', diff --git a/src/chrome/src/ui/locales/tl.js b/src/chrome/src/ui/locales/tl.js index 215d76a4..9fe1165b 100644 --- a/src/chrome/src/ui/locales/tl.js +++ b/src/chrome/src/ui/locales/tl.js @@ -33,6 +33,24 @@ export default { 'st.perms.gate.label': "Magtanong bago ang mahahalagang aksyon", 'brand': 'WebBrain', + 'install.page_title': 'Naka-install na ang WebBrain', + 'install.installed': 'Naka-install na ang WebBrain', + 'install.checking': 'Sinusuri ang iyong browser…', + 'install.ready': 'Handa na', + 'install.extensions': 'Mga extension', + 'install.eyebrow': 'Isang maliit na hakbang sa pag-set up', + 'install.title': 'Panatilihing isang click lang ang WebBrain', + 'install.intro': 'Maaaring itago ng browser ang mga bagong extension. Panatilihing nakikita ang WebBrain para madali itong buksan.', + 'install.open_extensions.title': 'Buksan ang mga extension', + 'install.open_extensions.body': 'Buksan ang mga kontrol ng extension malapit sa address bar.', + 'install.keep_visible.title': 'Panatilihing nakikita ang WebBrain', + 'install.keep_visible.body': 'Hanapin ang WebBrain at piliing i-pin, ipakita, o panatilihin ito sa toolbar.', + 'install.open.title': 'Buksan mula sa anumang page', + 'install.open.body': 'Piliin ang icon ng WebBrain', + 'install.open.shortcut_html': ', o pindutin ang Alt+Shift+W', + 'install.footer': 'Hindi ito awtomatikong magagawa—ikaw ang pinapapili ng browser kung ano ang lalabas sa toolbar.', + 'install.done': 'Nakuha ko', + 'install.browser.detected': 'Natukoy ang {browser}', // --- Onboarding -------------------------------------------------------- 'ob.step': '{current} sa {total}', diff --git a/src/chrome/src/ui/locales/tr.js b/src/chrome/src/ui/locales/tr.js index 17c72fce..cbe238d4 100644 --- a/src/chrome/src/ui/locales/tr.js +++ b/src/chrome/src/ui/locales/tr.js @@ -68,6 +68,24 @@ export default { 'st.skills.error.add_failed': 'Skill eklenemedi.', 'brand': 'WebBrain', + 'install.page_title': 'WebBrain yüklendi', + 'install.installed': 'WebBrain yüklendi', + 'install.checking': 'Tarayıcınız kontrol ediliyor…', + 'install.ready': 'Hazır', + 'install.extensions': 'Uzantılar', + 'install.eyebrow': 'Küçük bir kurulum adımı', + 'install.title': 'WebBrain tek tıklama uzağınızda olsun', + 'install.intro': 'Tarayıcınız yeni yüklenen uzantıları gizleyebilir. Kolayca açabilmek için WebBrain’i görünür tutun.', + 'install.open_extensions.title': 'Uzantıları açın', + 'install.open_extensions.body': 'Adres çubuğunun yanındaki tarayıcı uzantı denetimlerini açın.', + 'install.keep_visible.title': 'WebBrain’i görünür tutun', + 'install.keep_visible.body': 'WebBrain’i bulun ve sabitleme, gösterme veya araç çubuğunda tutma seçeneğini kullanın.', + 'install.open.title': 'Herhangi bir sayfadan açın', + 'install.open.body': 'WebBrain simgesini seçin', + 'install.open.shortcut_html': ' veya Alt+Shift+W tuşlarına basın', + 'install.footer': 'Bu otomatik olarak yapılamaz; araç çubuğu seçimini tarayıcınız size bırakır.', + 'install.done': 'Anladım', + 'install.browser.detected': '{browser} algılandı', // --- Onboarding -------------------------------------------------------- 'ob.step': '{current} / {total}', diff --git a/src/chrome/src/ui/locales/uk.js b/src/chrome/src/ui/locales/uk.js index 7ed5cc67..ea5ffabe 100644 --- a/src/chrome/src/ui/locales/uk.js +++ b/src/chrome/src/ui/locales/uk.js @@ -33,6 +33,24 @@ export default { 'st.perms.gate.label': "Питати перед важливими діями", 'brand': 'WebBrain', + 'install.page_title': 'WebBrain установлено', + 'install.installed': 'WebBrain установлено', + 'install.checking': 'Перевіряємо ваш браузер…', + 'install.ready': 'Готово', + 'install.extensions': 'Розширення', + 'install.eyebrow': 'Залишився один невеликий крок', + 'install.title': 'Тримайте WebBrain під рукою', + 'install.intro': 'Браузер може приховувати щойно встановлені розширення. Залиште WebBrain видимим, щоб легко його відкривати.', + 'install.open_extensions.title': 'Відкрийте розширення', + 'install.open_extensions.body': 'Відкрийте елементи керування розширеннями біля адресного рядка.', + 'install.keep_visible.title': 'Залиште WebBrain видимим', + 'install.keep_visible.body': 'Знайдіть WebBrain і виберіть закріплення, показ або збереження на панелі інструментів.', + 'install.open.title': 'Відкривайте з будь-якої сторінки', + 'install.open.body': 'Натисніть значок WebBrain', + 'install.open.shortcut_html': ' або клавіші Alt+Shift+W', + 'install.footer': 'Це не можна зробити автоматично — браузер залишає вибір елементів панелі інструментів за вами.', + 'install.done': 'Зрозуміло', + 'install.browser.detected': 'Виявлено {browser}', // --- Onboarding -------------------------------------------------------- 'ob.step': '{current} з {total}', diff --git a/src/chrome/src/ui/locales/zh.js b/src/chrome/src/ui/locales/zh.js index 94b491c1..dd62747d 100644 --- a/src/chrome/src/ui/locales/zh.js +++ b/src/chrome/src/ui/locales/zh.js @@ -33,6 +33,24 @@ export default { 'st.perms.gate.label': "执行重要操作前询问", 'brand': 'WebBrain', + 'install.page_title': 'WebBrain 已安装', + 'install.installed': 'WebBrain 已安装', + 'install.checking': '正在检查您的浏览器…', + 'install.ready': '已就绪', + 'install.extensions': '扩展程序', + 'install.eyebrow': '还差一个小设置', + 'install.title': '让 WebBrain 一键可用', + 'install.intro': '浏览器可能会隐藏新安装的扩展程序。让 WebBrain 保持可见,以便随时打开。', + 'install.open_extensions.title': '打开扩展程序', + 'install.open_extensions.body': '打开地址栏旁边的浏览器扩展程序控件。', + 'install.keep_visible.title': '让 WebBrain 保持可见', + 'install.keep_visible.body': '找到 WebBrain,然后选择固定、显示或保留在工具栏中的选项。', + 'install.open.title': '从任意页面打开', + 'install.open.body': '选择 WebBrain 图标', + 'install.open.shortcut_html': ',或按 Alt+Shift+W', + 'install.footer': '此操作无法自动完成;浏览器会将工具栏选择权交给您。', + 'install.done': '知道了', + 'install.browser.detected': '已检测到 {browser}', // --- Onboarding -------------------------------------------------------- 'ob.step': '{current} / {total}', diff --git a/src/firefox/src/background.js b/src/firefox/src/background.js index 2e46f44c..8eece667 100644 --- a/src/firefox/src/background.js +++ b/src/firefox/src/background.js @@ -718,8 +718,19 @@ async function loadPlanReviewSettings() { const planBeforeActReady = loadPlanBeforeAct(); const planReviewReady = loadPlanReviewSettings(); +function showFirstInstallGuide(details) { + if (details?.reason !== 'install') return; + browser.tabs.create({ + url: browser.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 -browser.runtime.onInstalled.addListener(async () => { +browser.runtime.onInstalled.addListener(async (details) => { + showFirstInstallGuide(details); createContextMenus(); await providerManager.load(); await loadMaxSteps(); diff --git a/src/firefox/src/ui/i18n.js b/src/firefox/src/ui/i18n.js index c147ce94..6c1644c4 100644 --- a/src/firefox/src/ui/i18n.js +++ b/src/firefox/src/ui/i18n.js @@ -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'; } @@ -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); + } } diff --git a/src/firefox/src/ui/install.css b/src/firefox/src/ui/install.css new file mode 100644 index 00000000..502057a2 --- /dev/null +++ b/src/firefox/src/ui/install.css @@ -0,0 +1,474 @@ +:root { + color-scheme: dark; + --page: #121325; + --card: #191b31; + --panel: #222541; + --panel-strong: #2a2e50; + --text: #f5f3ff; + --muted: #aaa9c3; + --quiet: #9292aa; + --accent: #837cff; + --accent-strong: #6c63ff; + --accent-soft: rgba(131, 124, 255, 0.15); + --line: rgba(255, 255, 255, 0.1); + --success: #6bd6a2; + --shadow: 0 28px 80px rgba(3, 4, 16, 0.42); +} + +:root[data-theme="light"] { + color-scheme: light; + --page: #f2eadc; + --card: #fffdf8; + --panel: #f7f0e4; + --panel-strong: #eee4d5; + --text: #2c1810; + --muted: #6b5b47; + --quiet: #766a58; + --accent: #675ee9; + --accent-strong: #5147d7; + --accent-soft: rgba(91, 82, 232, 0.12); + --line: rgba(89, 55, 25, 0.14); + --success: #27845f; + --shadow: 0 28px 80px rgba(80, 52, 26, 0.14); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; + background: var(--page); + color: var(--text); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +button, +kbd { + font: inherit; +} + +.install-page { + min-height: 100vh; + display: grid; + place-items: center; + padding: 42px 20px; +} + +.install-card { + width: min(760px, 100%); + overflow: hidden; + background: var(--card); + border: 1px solid var(--line); + border-radius: 24px; + box-shadow: var(--shadow); +} + +.install-header { + min-height: 78px; + padding: 16px 22px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 18px; + border-bottom: 1px solid var(--line); +} + +.brand { + display: flex; + align-items: center; + gap: 12px; +} + +.brand img { + border-radius: 11px; +} + +.installed-label { + font-size: 14px; + font-weight: 720; + letter-spacing: -0.01em; +} + +.browser-label { + margin-top: 3px; + color: var(--quiet); + font-size: 12px; +} + +.status-chip { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 7px 10px; + border: 1px solid var(--line); + border-radius: 999px; + color: var(--muted); + font-size: 11px; + font-weight: 650; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.status-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--success); + box-shadow: 0 0 0 4px color-mix(in srgb, var(--success) 15%, transparent); +} + +.toolbar-rehearsal { + position: relative; + height: 190px; + margin: 28px 28px 0; + padding: 17px; + display: grid; + grid-template-columns: auto minmax(120px, 1fr) auto; + align-items: start; + gap: 14px; + border: 1px solid var(--line); + border-radius: 16px; + background: var(--panel); +} + +.window-controls { + display: flex; + gap: 6px; + padding-top: 11px; +} + +.window-controls span { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--line); +} + +.address-field { + min-width: 0; + height: 34px; + padding: 9px 13px; + overflow: hidden; + border: 1px solid var(--line); + border-radius: 9px; + background: var(--card); + color: var(--quiet); + font-size: 11px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.toolbar-action { + position: relative; + width: 34px; + height: 34px; + display: grid; + place-items: center; + border: 1px solid color-mix(in srgb, var(--accent) 55%, transparent); + border-radius: 9px; + background: var(--accent-soft); + color: var(--accent); +} + +.toolbar-action svg { + width: 17px; + height: 17px; + fill: currentColor; +} + +.action-number { + position: absolute; + top: -9px; + right: -9px; + z-index: 2; + width: 19px; + height: 19px; + display: grid; + place-items: center; + border: 2px solid var(--panel); + border-radius: 50%; + background: var(--accent-strong); + color: white; + font-size: 9px; + font-weight: 800; +} + +.toolbar-action-extensions::after { + content: ""; + position: absolute; + inset: -7px; + border: 1px solid var(--accent); + border-radius: 13px; + animation: attention 1.8s ease-out infinite; +} + +.extension-menu { + position: absolute; + top: 63px; + right: 17px; + width: min(248px, calc(100% - 34px)); + padding: 13px; + border: 1px solid var(--line); + border-radius: 12px; + background: var(--card); + box-shadow: 0 16px 30px rgba(3, 4, 16, 0.22); +} + +.menu-title { + margin: 0 0 10px 2px; + color: var(--quiet); + font-size: 10px; + font-weight: 750; + text-transform: uppercase; + letter-spacing: 0.1em; +} + +.extension-row { + min-height: 48px; + padding: 9px 10px; + display: grid; + grid-template-columns: 26px 1fr 34px; + align-items: center; + gap: 10px; + border: 1px solid var(--line); + border-radius: 9px; + background: var(--panel); + font-size: 12px; + font-weight: 650; +} + +.extension-row img { + border-radius: 7px; +} + +.pin-target { + position: relative; + width: 30px; + height: 30px; + display: grid; + place-items: center; + border-radius: 8px; + background: var(--accent-strong); + color: white; +} + +.pin-target svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 1.8; + stroke-linecap: round; + stroke-linejoin: round; +} + +.pin-target .action-number { + border-color: var(--card); +} + +.install-copy { + padding: 34px 44px 0; +} + +.eyebrow { + margin: 0 0 8px; + color: var(--accent); + font-size: 11px; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.11em; +} + +h1 { + max-width: 590px; + margin: 0; + font-size: clamp(28px, 5vw, 43px); + line-height: 1.06; + letter-spacing: -0.042em; +} + +.intro { + max-width: 600px; + margin: 15px 0 0; + color: var(--muted); + font-size: 15px; + line-height: 1.6; +} + +.guide-steps { + margin: 28px 44px 34px; + padding: 0; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + list-style: none; +} + +.guide-steps li { + min-width: 0; + padding: 0 20px; + display: flex; + gap: 11px; + border-left: 1px solid var(--line); +} + +.guide-steps li:first-child { + padding-left: 0; + border-left: 0; +} + +.guide-steps li:last-child { + padding-right: 0; +} + +.step-number { + flex: 0 0 auto; + width: 22px; + height: 22px; + display: grid; + place-items: center; + border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent); + border-radius: 7px; + background: var(--accent-soft); + color: var(--accent); + font-size: 10px; + font-weight: 800; +} + +.guide-steps strong { + display: block; + margin: 2px 0 7px; + font-size: 12px; +} + +.guide-steps p { + margin: 0; + color: var(--quiet); + font-size: 11px; + line-height: 1.5; +} + +kbd { + display: inline-block; + padding: 1px 4px; + border: 1px solid var(--line); + border-bottom-width: 2px; + border-radius: 4px; + background: var(--panel); + color: var(--muted); + font-size: 9px; +} + +.install-footer { + min-height: 72px; + padding: 14px 22px 14px 44px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + border-top: 1px solid var(--line); + background: color-mix(in srgb, var(--panel) 38%, transparent); +} + +.install-footer p { + margin: 0; + color: var(--quiet); + font-size: 11px; + line-height: 1.45; +} + +.install-footer button { + flex: 0 0 auto; + min-width: 104px; + padding: 11px 18px; + border: 0; + border-radius: 9px; + background: var(--accent-strong); + color: white; + cursor: pointer; + font-weight: 750; + box-shadow: 0 8px 18px color-mix(in srgb, var(--accent-strong) 25%, transparent); + transition: transform 150ms ease, background 150ms ease; +} + +.install-footer button:hover { + background: var(--accent); + transform: translateY(-1px); +} + +.install-footer button:focus-visible { + outline: 3px solid color-mix(in srgb, var(--accent) 50%, transparent); + outline-offset: 3px; +} + +@keyframes attention { + 0% { + opacity: 0.8; + transform: scale(0.82); + } + 72%, + 100% { + opacity: 0; + transform: scale(1.14); + } +} + +@media (max-width: 680px) { + .install-page { + padding: 16px 10px; + } + + .install-card { + border-radius: 18px; + } + + .toolbar-rehearsal { + margin: 18px 14px 0; + } + + .install-copy { + padding: 28px 22px 0; + } + + .guide-steps { + margin: 25px 22px 28px; + grid-template-columns: 1fr; + gap: 18px; + } + + .guide-steps li, + .guide-steps li:first-child, + .guide-steps li:last-child { + padding: 0; + border-left: 0; + } + + .install-footer { + padding: 18px 22px; + align-items: stretch; + flex-direction: column; + } + + .install-footer button { + width: 100%; + } +} + +@media (max-width: 420px) { + .status-chip { + display: none; + } +} + +@media (prefers-reduced-motion: reduce) { + .toolbar-action-extensions::after { + animation: none; + opacity: 0.55; + } + + .install-footer button { + transition: none; + } +} diff --git a/src/firefox/src/ui/install.html b/src/firefox/src/ui/install.html new file mode 100644 index 00000000..188b06a0 --- /dev/null +++ b/src/firefox/src/ui/install.html @@ -0,0 +1,92 @@ + + + + + + WebBrain installed + + + + +
+
+
+
+ +
+
WebBrain installed
+
Checking your browser…
+
+
+ + + Ready + +
+ + + +
+

One small setup step

+

Keep WebBrain one click away

+

Your browser may hide newly installed extensions. Keep WebBrain visible so it stays easy to open.

+
+ +
    +
  1. + 1 +
    + Open Extensions +

    Open your browser’s extension controls near the address bar.

    +
    +
  2. +
  3. + 2 +
    + Keep WebBrain visible +

    Find WebBrain and choose the pin, show, or keep-in-toolbar option.

    +
    +
  4. +
  5. + 3 +
    + Open it from any page +

    Select the WebBrain icon, or press Alt+Shift+W.

    +
    +
  6. +
+ +
+

This cannot be done automatically—your browser leaves toolbar choices to you.

+ +
+
+
+ + + + diff --git a/src/firefox/src/ui/install.js b/src/firefox/src/ui/install.js new file mode 100644 index 00000000..adc686c0 --- /dev/null +++ b/src/firefox/src/ui/install.js @@ -0,0 +1,133 @@ +import { applyDOMTranslations, getLocale, t } from './i18n.js'; + +const GUIDES = { + chrome: { + name: 'Google Chrome', + intro: 'Chrome usually places new extensions inside its Extensions menu. Pin WebBrain so the side panel stays one click away.', + firstTitle: 'Open Extensions', + firstBody: 'Select the puzzle-piece icon to the right of the address bar.', + secondTitle: 'Pin WebBrain', + secondBody: 'Find WebBrain, then select the pin icon beside it.', + }, + edge: { + name: 'Microsoft Edge', + intro: 'Edge may keep new extensions inside its Extensions menu. Keep WebBrain on the toolbar so the side panel stays one click away.', + firstTitle: 'Open Extensions', + firstBody: 'Select the Extensions icon to the right of the address bar.', + secondTitle: 'Show WebBrain', + secondBody: 'Find WebBrain, then choose Show in toolbar.', + }, + brave: { + name: 'Brave', + intro: 'Brave may tuck new extensions into its Extensions menu. Pin WebBrain so the side panel stays one click away.', + firstTitle: 'Open Extensions', + firstBody: 'Select the puzzle-piece icon to the right of the address bar.', + secondTitle: 'Pin WebBrain', + secondBody: 'Find WebBrain, then select the pin icon beside it.', + }, + vivaldi: { + name: 'Vivaldi', + intro: 'Vivaldi can hide extension buttons beside the Address Bar. Keep WebBrain visible so the side panel stays one click away.', + firstTitle: 'Check the Address Bar', + firstBody: 'Look for the extension controls on the right side of the Address Bar.', + secondTitle: 'Show WebBrain', + secondBody: 'If it is hidden, open hidden extensions, right-click WebBrain, and choose Show Button.', + }, + opera: { + name: 'Opera', + intro: 'Opera may keep new extensions inside its Extensions menu. Pin WebBrain so the side panel stays one click away.', + firstTitle: 'Open Extensions', + firstBody: 'Select the Extensions icon to the right of the address bar.', + secondTitle: 'Pin WebBrain', + secondBody: 'Find WebBrain, then use its pin control to keep it on the toolbar.', + }, + firefox: { + name: 'Firefox', + intro: 'Firefox may place new add-ons inside its Extensions panel. Pin WebBrain so the sidebar stays one click away.', + firstTitle: 'Open Extensions', + firstBody: 'Select the puzzle-piece icon in the toolbar.', + secondTitle: 'Pin WebBrain', + secondBody: 'Open the gear menu beside WebBrain, then choose Pin to Toolbar.', + }, + chromium: { + name: 'Chromium', + intro: 'Your browser may hide new extensions in its extension controls. Keep WebBrain visible so the side panel stays one click away.', + firstTitle: 'Open extension controls', + firstBody: 'Look beside the address bar for Extensions or hidden extension buttons.', + secondTitle: 'Keep WebBrain visible', + secondBody: 'Find WebBrain and use the pin, show, or keep-in-toolbar control.', + }, + unknown: { + name: 'Browser', + intro: 'Your browser may hide new extensions in its extension controls. Keep WebBrain visible so it stays easy to open.', + firstTitle: 'Open extension controls', + firstBody: 'Look near the address bar or in your browser menu for Extensions or Add-ons.', + secondTitle: 'Keep WebBrain visible', + secondBody: 'Find WebBrain and choose the option that keeps it in your toolbar.', + }, +}; + +function browserBrands(navigatorLike) { + return (navigatorLike?.userAgentData?.brands || []) + .map((entry) => String(entry?.brand || '').toLowerCase()) + .join(' '); +} + +export async function detectBrowser(navigatorLike = globalThis.navigator) { + try { + if (await navigatorLike?.brave?.isBrave?.()) return 'brave'; + } catch { + // Brave detection is an optional progressive enhancement. + } + + const ua = String(navigatorLike?.userAgent || ''); + const brands = browserBrands(navigatorLike); + if (/Firefox\//i.test(ua)) return 'firefox'; + if (/\bEdg(?:e|A|iOS)?\//i.test(ua) || brands.includes('microsoft edge')) return 'edge'; + if (/\bOPR\//i.test(ua) || brands.includes('opera')) return 'opera'; + if (/\bVivaldi\//i.test(ua) || brands.includes('vivaldi')) return 'vivaldi'; + if (brands.includes('google chrome')) return 'chrome'; + if (/\b(?:Chrome|Chromium)\//i.test(ua) || brands.includes('chromium')) return 'chromium'; + return 'unknown'; +} + +export function getBrowserGuide(browserKey) { + return GUIDES[browserKey] || GUIDES.unknown; +} + +async function closeGuideTab() { + const api = globalThis.browser?.tabs ? globalThis.browser : globalThis.chrome; + try { + const tab = await api?.tabs?.getCurrent?.(); + if (tab?.id != null) { + await api.tabs.remove(tab.id); + return; + } + } catch { + // window.close() is a harmless fallback when tab lookup is unavailable. + } + window.close(); +} + +async function hydrateGuide() { + const browserKey = await detectBrowser(); + const guide = getBrowserGuide(browserKey); + applyDOMTranslations(document); + document.documentElement.dataset.browser = browserKey; + document.getElementById('browser-label').textContent = t('install.browser.detected', { browser: guide.name }); + if (getLocale() === 'en') { + document.getElementById('install-intro').textContent = guide.intro; + document.getElementById('step-one-title').textContent = guide.firstTitle; + document.getElementById('step-one-body').textContent = guide.firstBody; + document.getElementById('step-two-title').textContent = guide.secondTitle; + document.getElementById('step-two-body').textContent = guide.secondBody; + } + if (document.documentElement.dataset.build === 'firefox') { + document.getElementById('shortcut-hint').remove(); + } + document.getElementById('done-button').addEventListener('click', closeGuideTab); +} + +if (typeof document !== 'undefined') { + hydrateGuide().catch(() => {}); +} diff --git a/src/firefox/src/ui/locales/ar.js b/src/firefox/src/ui/locales/ar.js index c3a58736..f7788aa8 100644 --- a/src/firefox/src/ui/locales/ar.js +++ b/src/firefox/src/ui/locales/ar.js @@ -33,6 +33,24 @@ export default { 'st.perms.gate.label': "السؤال قبل الإجراءات المهمة", 'brand': 'WebBrain', + 'install.page_title': 'تم تثبيت WebBrain', + 'install.installed': 'تم تثبيت WebBrain', + 'install.checking': 'جارٍ التحقق من متصفحك…', + 'install.ready': 'جاهز', + 'install.extensions': 'الإضافات', + 'install.eyebrow': 'خطوة إعداد صغيرة', + 'install.title': 'اجعل WebBrain على بُعد نقرة', + 'install.intro': 'قد يخفي متصفحك الإضافات المثبتة حديثًا. أبقِ WebBrain ظاهرًا ليسهل فتحه.', + 'install.open_extensions.title': 'افتح الإضافات', + 'install.open_extensions.body': 'افتح عناصر التحكم في إضافات المتصفح بالقرب من شريط العناوين.', + 'install.keep_visible.title': 'أبقِ WebBrain ظاهرًا', + 'install.keep_visible.body': 'ابحث عن WebBrain واختر التثبيت أو الإظهار أو الإبقاء في شريط الأدوات.', + 'install.open.title': 'افتحه من أي صفحة', + 'install.open.body': 'حدد أيقونة WebBrain', + 'install.open.shortcut_html': '، أو اضغط Alt+Shift+W', + 'install.footer': 'لا يمكن تنفيذ ذلك تلقائيًا؛ يترك لك المتصفح اختيار عناصر شريط الأدوات.', + 'install.done': 'فهمت', + 'install.browser.detected': 'تم اكتشاف {browser}', // --- Onboarding -------------------------------------------------------- 'ob.step': '{current} من {total}', diff --git a/src/firefox/src/ui/locales/en.js b/src/firefox/src/ui/locales/en.js index 13e9db4b..f0f206f9 100644 --- a/src/firefox/src/ui/locales/en.js +++ b/src/firefox/src/ui/locales/en.js @@ -9,6 +9,24 @@ export default { 'st.providers.select_for_chat': 'Select for chat', // Brand name: kept in English across locales. 'brand': 'WebBrain', + 'install.page_title': 'WebBrain installed', + 'install.installed': 'WebBrain installed', + 'install.checking': 'Checking your browser…', + 'install.ready': 'Ready', + 'install.extensions': 'Extensions', + 'install.eyebrow': 'One small setup step', + 'install.title': 'Keep WebBrain one click away', + 'install.intro': 'Your browser may hide newly installed extensions. Keep WebBrain visible so it stays easy to open.', + 'install.open_extensions.title': 'Open Extensions', + 'install.open_extensions.body': 'Open your browser’s extension controls near the address bar.', + 'install.keep_visible.title': 'Keep WebBrain visible', + 'install.keep_visible.body': 'Find WebBrain and choose the pin, show, or keep-in-toolbar option.', + 'install.open.title': 'Open it from any page', + 'install.open.body': 'Select the WebBrain icon', + 'install.open.shortcut_html': ', or press Alt+Shift+W', + 'install.footer': 'This cannot be done automatically—your browser leaves toolbar choices to you.', + 'install.done': 'Got it', + 'install.browser.detected': '{browser} detected', // --- Sidepanel --------------------------------------------------------- 'sp.status.disconnected': 'Disconnected', diff --git a/src/firefox/src/ui/locales/es.js b/src/firefox/src/ui/locales/es.js index 67036a62..a92f271f 100644 --- a/src/firefox/src/ui/locales/es.js +++ b/src/firefox/src/ui/locales/es.js @@ -33,6 +33,24 @@ export default { 'st.perms.gate.label': "Preguntar antes de acciones importantes", 'brand': 'WebBrain', + 'install.page_title': 'WebBrain instalado', + 'install.installed': 'WebBrain instalado', + 'install.checking': 'Comprobando tu navegador…', + 'install.ready': 'Listo', + 'install.extensions': 'Extensiones', + 'install.eyebrow': 'Un pequeño paso de configuración', + 'install.title': 'Mantén WebBrain a un clic', + 'install.intro': 'Tu navegador puede ocultar las extensiones recién instaladas. Mantén WebBrain visible para abrirlo fácilmente.', + 'install.open_extensions.title': 'Abre Extensiones', + 'install.open_extensions.body': 'Abre los controles de extensiones del navegador junto a la barra de direcciones.', + 'install.keep_visible.title': 'Mantén WebBrain visible', + 'install.keep_visible.body': 'Busca WebBrain y elige la opción para fijarlo, mostrarlo o mantenerlo en la barra de herramientas.', + 'install.open.title': 'Ábrelo desde cualquier página', + 'install.open.body': 'Selecciona el icono de WebBrain', + 'install.open.shortcut_html': ', o pulsa Alt+Mayús+W', + 'install.footer': 'Esto no puede hacerse automáticamente: tú decides qué aparece en la barra de herramientas.', + 'install.done': 'Entendido', + 'install.browser.detected': '{browser} detectado', // --- Onboarding -------------------------------------------------------- 'ob.step': '{current} de {total}', diff --git a/src/firefox/src/ui/locales/fr.js b/src/firefox/src/ui/locales/fr.js index 74358dbf..2ce3e930 100644 --- a/src/firefox/src/ui/locales/fr.js +++ b/src/firefox/src/ui/locales/fr.js @@ -33,6 +33,24 @@ export default { 'st.perms.gate.label': "Demander avant les actions importantes", 'brand': 'WebBrain', + 'install.page_title': 'WebBrain installé', + 'install.installed': 'WebBrain installé', + 'install.checking': 'Vérification de votre navigateur…', + 'install.ready': 'Prêt', + 'install.extensions': 'Extensions', + 'install.eyebrow': 'Une petite étape de configuration', + 'install.title': 'Gardez WebBrain à portée de clic', + 'install.intro': 'Votre navigateur peut masquer les extensions récemment installées. Gardez WebBrain visible pour pouvoir l’ouvrir facilement.', + 'install.open_extensions.title': 'Ouvrez Extensions', + 'install.open_extensions.body': 'Ouvrez les commandes d’extensions du navigateur près de la barre d’adresse.', + 'install.keep_visible.title': 'Gardez WebBrain visible', + 'install.keep_visible.body': 'Repérez WebBrain et choisissez l’option pour l’épingler, l’afficher ou le garder dans la barre d’outils.', + 'install.open.title': 'Ouvrez-le depuis n’importe quelle page', + 'install.open.body': 'Sélectionnez l’icône WebBrain', + 'install.open.shortcut_html': ', ou appuyez sur Alt+Maj+W', + 'install.footer': 'Cette action ne peut pas être automatique : votre navigateur vous laisse choisir les éléments de la barre d’outils.', + 'install.done': 'Compris', + 'install.browser.detected': '{browser} détecté', // --- Onboarding -------------------------------------------------------- 'ob.step': '{current} sur {total}', diff --git a/src/firefox/src/ui/locales/he.js b/src/firefox/src/ui/locales/he.js index ed793aae..91694455 100644 --- a/src/firefox/src/ui/locales/he.js +++ b/src/firefox/src/ui/locales/he.js @@ -8,6 +8,24 @@ export default { 'st.providers.selected': 'נבחר', 'st.providers.select_for_chat': 'בחירה לצ׳אט', "brand": "WebBrain", + 'install.page_title': 'WebBrain הותקן', + 'install.installed': 'WebBrain הותקן', + 'install.checking': 'בודק את הדפדפן שלך…', + 'install.ready': 'מוכן', + 'install.extensions': 'הרחבות', + 'install.eyebrow': 'עוד שלב הגדרה קטן', + 'install.title': 'WebBrain במרחק לחיצה אחת', + 'install.intro': 'הדפדפן עשוי להסתיר הרחבות שהותקנו לאחרונה. כדאי להשאיר את WebBrain גלוי כדי לפתוח אותו בקלות.', + 'install.open_extensions.title': 'פתיחת ההרחבות', + 'install.open_extensions.body': 'פתח את פקדי ההרחבות ליד שורת הכתובת.', + 'install.keep_visible.title': 'השארת WebBrain גלוי', + 'install.keep_visible.body': 'מצא את WebBrain ובחר להצמיד, להציג או להשאיר אותו בסרגל הכלים.', + 'install.open.title': 'פתיחה מכל דף', + 'install.open.body': 'בחר בסמל WebBrain', + 'install.open.shortcut_html': ', או הקש Alt+Shift+W', + 'install.footer': 'לא ניתן לבצע זאת אוטומטית — הדפדפן משאיר לך את הבחירה מה יוצג בסרגל הכלים.', + 'install.done': 'הבנתי', + 'install.browser.detected': '{browser} זוהה', "sp.status.disconnected": "לא מחובר", "sp.status.connected": "מחובר ({model})", "sp.status.error": "שגיאה: {msg}", diff --git a/src/firefox/src/ui/locales/id.js b/src/firefox/src/ui/locales/id.js index 6b0b686b..155bfeb0 100644 --- a/src/firefox/src/ui/locales/id.js +++ b/src/firefox/src/ui/locales/id.js @@ -33,6 +33,24 @@ export default { 'st.perms.gate.label': "Tanya sebelum tindakan penting", 'brand': 'WebBrain', + 'install.page_title': 'WebBrain terpasang', + 'install.installed': 'WebBrain terpasang', + 'install.checking': 'Memeriksa browser Anda…', + 'install.ready': 'Siap', + 'install.extensions': 'Ekstensi', + 'install.eyebrow': 'Satu langkah penyiapan kecil', + 'install.title': 'Buka WebBrain dengan sekali klik', + 'install.intro': 'Browser Anda mungkin menyembunyikan ekstensi yang baru dipasang. Biarkan WebBrain terlihat agar mudah dibuka.', + 'install.open_extensions.title': 'Buka Ekstensi', + 'install.open_extensions.body': 'Buka kontrol ekstensi browser di dekat bilah alamat.', + 'install.keep_visible.title': 'Biarkan WebBrain terlihat', + 'install.keep_visible.body': 'Temukan WebBrain lalu pilih opsi sematkan, tampilkan, atau pertahankan di bilah alat.', + 'install.open.title': 'Buka dari halaman mana pun', + 'install.open.body': 'Pilih ikon WebBrain', + 'install.open.shortcut_html': ', atau tekan Alt+Shift+W', + 'install.footer': 'Ini tidak dapat dilakukan secara otomatis—browser menyerahkan pilihan bilah alat kepada Anda.', + 'install.done': 'Mengerti', + 'install.browser.detected': '{browser} terdeteksi', // --- Onboarding -------------------------------------------------------- 'ob.step': '{current} dari {total}', diff --git a/src/firefox/src/ui/locales/ja.js b/src/firefox/src/ui/locales/ja.js index 085fd1f2..ce7b9337 100644 --- a/src/firefox/src/ui/locales/ja.js +++ b/src/firefox/src/ui/locales/ja.js @@ -33,6 +33,24 @@ export default { 'st.perms.gate.label': "重要な操作の前に確認する", 'brand': 'WebBrain', + 'install.page_title': 'WebBrain をインストールしました', + 'install.installed': 'WebBrain をインストールしました', + 'install.checking': 'ブラウザーを確認しています…', + 'install.ready': '準備完了', + 'install.extensions': '拡張機能', + 'install.eyebrow': 'あとひとつの簡単な設定', + 'install.title': 'WebBrain をワンクリックで開けるように', + 'install.intro': '新しくインストールした拡張機能はブラウザーに隠れる場合があります。WebBrain を表示して、すぐ開けるようにしましょう。', + 'install.open_extensions.title': '拡張機能を開く', + 'install.open_extensions.body': 'アドレスバーの近くにある拡張機能のコントロールを開きます。', + 'install.keep_visible.title': 'WebBrain を表示する', + 'install.keep_visible.body': 'WebBrain を見つけ、固定、表示、またはツールバーに残すオプションを選びます。', + 'install.open.title': 'どのページからでも開く', + 'install.open.body': 'WebBrain アイコンを選択', + 'install.open.shortcut_html': '、または Alt+Shift+W を押します', + 'install.footer': 'これは自動では行えません。ツールバーに表示する項目はブラウザー上で選択してください。', + 'install.done': '了解', + 'install.browser.detected': '{browser} を検出しました', // --- Onboarding -------------------------------------------------------- 'ob.step': '{current} / {total}', diff --git a/src/firefox/src/ui/locales/ko.js b/src/firefox/src/ui/locales/ko.js index 768c328c..ac4ab2ae 100644 --- a/src/firefox/src/ui/locales/ko.js +++ b/src/firefox/src/ui/locales/ko.js @@ -33,6 +33,24 @@ export default { 'st.perms.gate.label': "중요한 작업 전에 확인", 'brand': 'WebBrain', + 'install.page_title': 'WebBrain 설치 완료', + 'install.installed': 'WebBrain 설치 완료', + 'install.checking': '브라우저를 확인하는 중…', + 'install.ready': '준비됨', + 'install.extensions': '확장 프로그램', + 'install.eyebrow': '간단한 설정 한 단계', + 'install.title': 'WebBrain을 한 번의 클릭으로 여세요', + 'install.intro': '브라우저가 새로 설치한 확장 프로그램을 숨길 수 있습니다. WebBrain을 표시해 두면 쉽게 열 수 있습니다.', + 'install.open_extensions.title': '확장 프로그램 열기', + 'install.open_extensions.body': '주소 표시줄 근처의 브라우저 확장 프로그램 컨트롤을 엽니다.', + 'install.keep_visible.title': 'WebBrain 표시하기', + 'install.keep_visible.body': 'WebBrain을 찾아 고정, 표시 또는 도구 모음에 유지하는 옵션을 선택합니다.', + 'install.open.title': '어느 페이지에서나 열기', + 'install.open.body': 'WebBrain 아이콘을 선택하세요', + 'install.open.shortcut_html': ' 또는 Alt+Shift+W를 누르세요', + 'install.footer': '이 작업은 자동으로 할 수 없습니다. 도구 모음 항목은 브라우저에서 직접 선택해야 합니다.', + 'install.done': '확인', + 'install.browser.detected': '{browser} 감지됨', // --- Onboarding -------------------------------------------------------- 'ob.step': '{current} / {total}', diff --git a/src/firefox/src/ui/locales/ms.js b/src/firefox/src/ui/locales/ms.js index accf9686..a8a4bd53 100644 --- a/src/firefox/src/ui/locales/ms.js +++ b/src/firefox/src/ui/locales/ms.js @@ -33,6 +33,24 @@ export default { 'st.perms.gate.label': "Tanya sebelum tindakan penting", 'brand': 'WebBrain', + 'install.page_title': 'WebBrain dipasang', + 'install.installed': 'WebBrain dipasang', + 'install.checking': 'Memeriksa pelayar anda…', + 'install.ready': 'Sedia', + 'install.extensions': 'Sambungan', + 'install.eyebrow': 'Satu langkah persediaan ringkas', + 'install.title': 'Buka WebBrain dengan satu klik', + 'install.intro': 'Pelayar anda mungkin menyembunyikan sambungan yang baru dipasang. Pastikan WebBrain kelihatan supaya mudah dibuka.', + 'install.open_extensions.title': 'Buka Sambungan', + 'install.open_extensions.body': 'Buka kawalan sambungan pelayar berhampiran bar alamat.', + 'install.keep_visible.title': 'Pastikan WebBrain kelihatan', + 'install.keep_visible.body': 'Cari WebBrain dan pilih pilihan semat, tunjuk atau kekalkan pada bar alat.', + 'install.open.title': 'Buka dari mana-mana halaman', + 'install.open.body': 'Pilih ikon WebBrain', + 'install.open.shortcut_html': ', atau tekan Alt+Shift+W', + 'install.footer': 'Ini tidak boleh dilakukan secara automatik—pelayar menyerahkan pilihan bar alat kepada anda.', + 'install.done': 'Faham', + 'install.browser.detected': '{browser} dikesan', // --- Onboarding -------------------------------------------------------- 'ob.step': '{current} daripada {total}', diff --git a/src/firefox/src/ui/locales/pl.js b/src/firefox/src/ui/locales/pl.js index c2af3780..ca73e9ff 100644 --- a/src/firefox/src/ui/locales/pl.js +++ b/src/firefox/src/ui/locales/pl.js @@ -8,6 +8,24 @@ export default { 'st.providers.selected': 'WYBRANO', 'st.providers.select_for_chat': 'Wybierz do czatu', 'brand': 'WebBrain', + 'install.page_title': 'WebBrain zainstalowany', + 'install.installed': 'WebBrain zainstalowany', + 'install.checking': 'Sprawdzamy Twoją przeglądarkę…', + 'install.ready': 'Gotowe', + 'install.extensions': 'Rozszerzenia', + 'install.eyebrow': 'Jeszcze jeden mały krok', + 'install.title': 'Miej WebBrain zawsze pod ręką', + 'install.intro': 'Przeglądarka może ukrywać nowo zainstalowane rozszerzenia. Pozostaw WebBrain widoczny, aby łatwo go otwierać.', + 'install.open_extensions.title': 'Otwórz rozszerzenia', + 'install.open_extensions.body': 'Otwórz elementy sterowania rozszerzeniami obok paska adresu.', + 'install.keep_visible.title': 'Pozostaw WebBrain widoczny', + 'install.keep_visible.body': 'Znajdź WebBrain i wybierz przypięcie, pokazanie lub pozostawienie na pasku narzędzi.', + 'install.open.title': 'Otwieraj z dowolnej strony', + 'install.open.body': 'Wybierz ikonę WebBrain', + 'install.open.shortcut_html': ' lub naciśnij Alt+Shift+W', + 'install.footer': 'Nie można zrobić tego automatycznie — przeglądarka pozostawia wybór elementów paska narzędzi Tobie.', + 'install.done': 'Rozumiem', + 'install.browser.detected': 'Wykryto {browser}', 'sp.status.disconnected': 'Rozłączono', 'sp.status.connected': 'Połączono ({model})', 'sp.status.error': 'Błąd: {msg}', diff --git a/src/firefox/src/ui/locales/ru.js b/src/firefox/src/ui/locales/ru.js index 36a49c03..da6db02d 100644 --- a/src/firefox/src/ui/locales/ru.js +++ b/src/firefox/src/ui/locales/ru.js @@ -33,6 +33,24 @@ export default { 'st.perms.gate.label': "Спрашивать перед важными действиями", 'brand': 'WebBrain', + 'install.page_title': 'WebBrain установлен', + 'install.installed': 'WebBrain установлен', + 'install.checking': 'Проверяем ваш браузер…', + 'install.ready': 'Готово', + 'install.extensions': 'Расширения', + 'install.eyebrow': 'Остался один небольшой шаг', + 'install.title': 'Держите WebBrain под рукой', + 'install.intro': 'Браузер может скрывать недавно установленные расширения. Оставьте WebBrain на виду, чтобы легко его открывать.', + 'install.open_extensions.title': 'Откройте расширения', + 'install.open_extensions.body': 'Откройте элементы управления расширениями рядом с адресной строкой.', + 'install.keep_visible.title': 'Оставьте WebBrain на виду', + 'install.keep_visible.body': 'Найдите WebBrain и выберите закрепление, отображение или сохранение на панели инструментов.', + 'install.open.title': 'Открывайте с любой страницы', + 'install.open.body': 'Нажмите значок WebBrain', + 'install.open.shortcut_html': ' или клавиши Alt+Shift+W', + 'install.footer': 'Это нельзя сделать автоматически — браузер оставляет выбор элементов панели инструментов за вами.', + 'install.done': 'Понятно', + 'install.browser.detected': 'Обнаружен {browser}', // --- Onboarding -------------------------------------------------------- 'ob.step': '{current} из {total}', diff --git a/src/firefox/src/ui/locales/th.js b/src/firefox/src/ui/locales/th.js index a113030c..1c0377e4 100644 --- a/src/firefox/src/ui/locales/th.js +++ b/src/firefox/src/ui/locales/th.js @@ -33,6 +33,24 @@ export default { 'st.perms.gate.label': "ถามก่อนการดำเนินการสำคัญ", 'brand': 'WebBrain', + 'install.page_title': 'ติดตั้ง WebBrain แล้ว', + 'install.installed': 'ติดตั้ง WebBrain แล้ว', + 'install.checking': 'กำลังตรวจสอบเบราว์เซอร์ของคุณ…', + 'install.ready': 'พร้อม', + 'install.extensions': 'ส่วนขยาย', + 'install.eyebrow': 'ตั้งค่าอีกเพียงขั้นตอนเดียว', + 'install.title': 'เปิด WebBrain ได้ในคลิกเดียว', + 'install.intro': 'เบราว์เซอร์อาจซ่อนส่วนขยายที่เพิ่งติดตั้ง ให้ WebBrain แสดงอยู่เสมอเพื่อเปิดใช้งานได้ง่าย', + 'install.open_extensions.title': 'เปิดส่วนขยาย', + 'install.open_extensions.body': 'เปิดตัวควบคุมส่วนขยายของเบราว์เซอร์ใกล้แถบที่อยู่', + 'install.keep_visible.title': 'ให้ WebBrain แสดงอยู่เสมอ', + 'install.keep_visible.body': 'ค้นหา WebBrain แล้วเลือกปักหมุด แสดง หรือเก็บไว้บนแถบเครื่องมือ', + 'install.open.title': 'เปิดจากหน้าใดก็ได้', + 'install.open.body': 'เลือกไอคอน WebBrain', + 'install.open.shortcut_html': ' หรือกด Alt+Shift+W', + 'install.footer': 'ไม่สามารถทำขั้นตอนนี้โดยอัตโนมัติได้ เบราว์เซอร์ให้คุณเลือกสิ่งที่จะแสดงบนแถบเครื่องมือ', + 'install.done': 'เข้าใจแล้ว', + 'install.browser.detected': 'ตรวจพบ {browser}', // --- Onboarding -------------------------------------------------------- 'ob.step': '{current} จาก {total}', diff --git a/src/firefox/src/ui/locales/tl.js b/src/firefox/src/ui/locales/tl.js index e400070d..2dae3c54 100644 --- a/src/firefox/src/ui/locales/tl.js +++ b/src/firefox/src/ui/locales/tl.js @@ -33,6 +33,24 @@ export default { 'st.perms.gate.label': "Magtanong bago ang mahahalagang aksyon", 'brand': 'WebBrain', + 'install.page_title': 'Naka-install na ang WebBrain', + 'install.installed': 'Naka-install na ang WebBrain', + 'install.checking': 'Sinusuri ang iyong browser…', + 'install.ready': 'Handa na', + 'install.extensions': 'Mga extension', + 'install.eyebrow': 'Isang maliit na hakbang sa pag-set up', + 'install.title': 'Panatilihing isang click lang ang WebBrain', + 'install.intro': 'Maaaring itago ng browser ang mga bagong extension. Panatilihing nakikita ang WebBrain para madali itong buksan.', + 'install.open_extensions.title': 'Buksan ang mga extension', + 'install.open_extensions.body': 'Buksan ang mga kontrol ng extension malapit sa address bar.', + 'install.keep_visible.title': 'Panatilihing nakikita ang WebBrain', + 'install.keep_visible.body': 'Hanapin ang WebBrain at piliing i-pin, ipakita, o panatilihin ito sa toolbar.', + 'install.open.title': 'Buksan mula sa anumang page', + 'install.open.body': 'Piliin ang icon ng WebBrain', + 'install.open.shortcut_html': ', o pindutin ang Alt+Shift+W', + 'install.footer': 'Hindi ito awtomatikong magagawa—ikaw ang pinapapili ng browser kung ano ang lalabas sa toolbar.', + 'install.done': 'Nakuha ko', + 'install.browser.detected': 'Natukoy ang {browser}', // --- Onboarding -------------------------------------------------------- 'ob.step': '{current} sa {total}', diff --git a/src/firefox/src/ui/locales/tr.js b/src/firefox/src/ui/locales/tr.js index c688173f..9725f741 100644 --- a/src/firefox/src/ui/locales/tr.js +++ b/src/firefox/src/ui/locales/tr.js @@ -68,6 +68,24 @@ export default { 'st.skills.error.add_failed': 'Skill eklenemedi.', 'brand': 'WebBrain', + 'install.page_title': 'WebBrain yüklendi', + 'install.installed': 'WebBrain yüklendi', + 'install.checking': 'Tarayıcınız kontrol ediliyor…', + 'install.ready': 'Hazır', + 'install.extensions': 'Uzantılar', + 'install.eyebrow': 'Küçük bir kurulum adımı', + 'install.title': 'WebBrain tek tıklama uzağınızda olsun', + 'install.intro': 'Tarayıcınız yeni yüklenen uzantıları gizleyebilir. Kolayca açabilmek için WebBrain’i görünür tutun.', + 'install.open_extensions.title': 'Uzantıları açın', + 'install.open_extensions.body': 'Adres çubuğunun yanındaki tarayıcı uzantı denetimlerini açın.', + 'install.keep_visible.title': 'WebBrain’i görünür tutun', + 'install.keep_visible.body': 'WebBrain’i bulun ve sabitleme, gösterme veya araç çubuğunda tutma seçeneğini kullanın.', + 'install.open.title': 'Herhangi bir sayfadan açın', + 'install.open.body': 'WebBrain simgesini seçin', + 'install.open.shortcut_html': ' veya Alt+Shift+W tuşlarına basın', + 'install.footer': 'Bu otomatik olarak yapılamaz; araç çubuğu seçimini tarayıcınız size bırakır.', + 'install.done': 'Anladım', + 'install.browser.detected': '{browser} algılandı', // --- Onboarding -------------------------------------------------------- 'ob.step': '{current} / {total}', diff --git a/src/firefox/src/ui/locales/uk.js b/src/firefox/src/ui/locales/uk.js index 9b590478..c1e01145 100644 --- a/src/firefox/src/ui/locales/uk.js +++ b/src/firefox/src/ui/locales/uk.js @@ -33,6 +33,24 @@ export default { 'st.perms.gate.label': "Питати перед важливими діями", 'brand': 'WebBrain', + 'install.page_title': 'WebBrain установлено', + 'install.installed': 'WebBrain установлено', + 'install.checking': 'Перевіряємо ваш браузер…', + 'install.ready': 'Готово', + 'install.extensions': 'Розширення', + 'install.eyebrow': 'Залишився один невеликий крок', + 'install.title': 'Тримайте WebBrain під рукою', + 'install.intro': 'Браузер може приховувати щойно встановлені розширення. Залиште WebBrain видимим, щоб легко його відкривати.', + 'install.open_extensions.title': 'Відкрийте розширення', + 'install.open_extensions.body': 'Відкрийте елементи керування розширеннями біля адресного рядка.', + 'install.keep_visible.title': 'Залиште WebBrain видимим', + 'install.keep_visible.body': 'Знайдіть WebBrain і виберіть закріплення, показ або збереження на панелі інструментів.', + 'install.open.title': 'Відкривайте з будь-якої сторінки', + 'install.open.body': 'Натисніть значок WebBrain', + 'install.open.shortcut_html': ' або клавіші Alt+Shift+W', + 'install.footer': 'Це не можна зробити автоматично — браузер залишає вибір елементів панелі інструментів за вами.', + 'install.done': 'Зрозуміло', + 'install.browser.detected': 'Виявлено {browser}', // --- Onboarding -------------------------------------------------------- 'ob.step': '{current} з {total}', diff --git a/src/firefox/src/ui/locales/zh.js b/src/firefox/src/ui/locales/zh.js index 9aceb4e9..8a372c79 100644 --- a/src/firefox/src/ui/locales/zh.js +++ b/src/firefox/src/ui/locales/zh.js @@ -33,6 +33,24 @@ export default { 'st.perms.gate.label': "执行重要操作前询问", 'brand': 'WebBrain', + 'install.page_title': 'WebBrain 已安装', + 'install.installed': 'WebBrain 已安装', + 'install.checking': '正在检查您的浏览器…', + 'install.ready': '已就绪', + 'install.extensions': '扩展程序', + 'install.eyebrow': '还差一个小设置', + 'install.title': '让 WebBrain 一键可用', + 'install.intro': '浏览器可能会隐藏新安装的扩展程序。让 WebBrain 保持可见,以便随时打开。', + 'install.open_extensions.title': '打开扩展程序', + 'install.open_extensions.body': '打开地址栏旁边的浏览器扩展程序控件。', + 'install.keep_visible.title': '让 WebBrain 保持可见', + 'install.keep_visible.body': '找到 WebBrain,然后选择固定、显示或保留在工具栏中的选项。', + 'install.open.title': '从任意页面打开', + 'install.open.body': '选择 WebBrain 图标', + 'install.open.shortcut_html': ',或按 Alt+Shift+W', + 'install.footer': '此操作无法自动完成;浏览器会将工具栏选择权交给您。', + 'install.done': '知道了', + 'install.browser.detected': '已检测到 {browser}', // --- Onboarding -------------------------------------------------------- 'ob.step': '{current} / {total}', diff --git a/test/run.js b/test/run.js index 26354593..9beb4658 100644 --- a/test/run.js +++ b/test/run.js @@ -8825,6 +8825,108 @@ test('sidepanel awaits onboarding completion persistence before hiding', () => { } }); +test('first install opens a browser-aware pinning guide in both builds', async () => { + for (const [label, prefix, runtime] of [ + ['chrome', 'src/chrome', 'chrome'], + ['firefox', 'src/firefox', 'browser'], + ]) { + const background = fs.readFileSync(path.join(ROOT, prefix, 'src/background.js'), 'utf8'); + const html = fs.readFileSync(path.join(ROOT, prefix, 'src/ui/install.html'), 'utf8'); + const css = fs.readFileSync(path.join(ROOT, prefix, 'src/ui/install.css'), 'utf8'); + const installModule = await import(pathToFileURL(path.join(ROOT, prefix, 'src/ui/install.js')).href); + + assert.match( + background, + /function showFirstInstallGuide\(details\) \{[\s\S]*?details\?\.reason !== 'install'[\s\S]*?tabs\.create\(\{[\s\S]*?runtime\.getURL\('src\/ui\/install\.html'\)/, + `${label}: the guide should open only for a genuine first install`, + ); + assert.match( + background, + new RegExp(`${runtime}\\.runtime\\.onInstalled\\.addListener\\(async \\(details\\) => \\{\\s*showFirstInstallGuide\\(details\\);`), + `${label}: onInstalled should pass install details to the guide gate`, + ); + assert.match(html, /class="toolbar-rehearsal"/, `${label}: install guide should include the visual toolbar rehearsal`); + assert.match(html, /id="done-button"/, `${label}: install guide should provide a keyboard-accessible dismissal`); + if (label === 'firefox') { + assert.match(html, /data-build="firefox"/, 'firefox: the install page should identify its build'); + assert.match( + fs.readFileSync(path.join(ROOT, prefix, 'src/ui/install.js'), 'utf8'), + /dataset\.build === 'firefox'[\s\S]*?getElementById\('shortcut-hint'\)\.remove\(\)/, + 'firefox: install guidance should remove the undeclared Chromium keyboard shortcut', + ); + } + assert.match(css, /@media \(prefers-reduced-motion: reduce\)/, `${label}: install guide should respect reduced motion`); + assert.match(css, /@media \(max-width: 680px\)/, `${label}: install guide should adapt to narrow windows`); + + assert.equal( + await installModule.detectBrowser({ userAgent: 'Mozilla/5.0 Chrome/140.0 Safari/537.36 Edg/140.0' }), + 'edge', + `${label}: Edge should be detected before generic Chromium`, + ); + assert.equal( + await installModule.detectBrowser({ userAgent: 'Mozilla/5.0 Chrome/140.0 Safari/537.36 Vivaldi/7.5' }), + 'vivaldi', + `${label}: Vivaldi should get its own toolbar guidance`, + ); + assert.equal( + await installModule.detectBrowser({ userAgent: 'Mozilla/5.0 Chrome/140.0 Safari/537.36 OPR/121.0' }), + 'opera', + `${label}: Opera should be detected before generic Chromium`, + ); + assert.equal( + await installModule.detectBrowser({ userAgent: 'Mozilla/5.0 Firefox/142.0' }), + 'firefox', + `${label}: Firefox should get its native toolbar guidance`, + ); + assert.equal( + await installModule.detectBrowser({ + userAgent: 'Mozilla/5.0 Chrome/140.0 Safari/537.36', + brave: { isBrave: async () => true }, + }), + 'brave', + `${label}: Brave should use its native detection hook`, + ); + assert.equal( + await installModule.detectBrowser({ + userAgent: 'Mozilla/5.0', + userAgentData: { brands: [{ brand: 'Google Chrome', version: '140' }] }, + }), + 'chrome', + `${label}: UA Client Hints should identify Chrome`, + ); + assert.equal( + await installModule.detectBrowser({ userAgent: 'Mozilla/5.0 Chrome/140.0 Safari/537.36' }), + 'chromium', + `${label}: unidentified Chromium forks should receive safe generic guidance`, + ); + assert.match( + installModule.getBrowserGuide('firefox').secondBody, + /Pin to Toolbar/, + `${label}: Firefox guidance should match its extensions menu wording`, + ); + assert.match( + installModule.getBrowserGuide('vivaldi').secondBody, + /Show Button/, + `${label}: Vivaldi guidance should match its hidden-button wording`, + ); + } + + const localeFilenames = fs.readdirSync(path.join(ROOT, 'src/chrome/src/ui/locales')) + .filter((name) => name.endsWith('.js')) + .sort(); + for (const filename of localeFilenames) { + const chromeLocale = (await import(pathToFileURL(path.join(ROOT, 'src/chrome/src/ui/locales', filename)).href)).default; + const firefoxLocale = (await import(pathToFileURL(path.join(ROOT, 'src/firefox/src/ui/locales', filename)).href)).default; + const installKeys = Object.keys(chromeLocale).filter((key) => key.startsWith('install.')); + assert.equal(installKeys.length, 18, `${filename}: install guide should translate every user-facing string`); + assert.deepEqual( + Object.fromEntries(installKeys.map((key) => [key, chromeLocale[key]])), + Object.fromEntries(installKeys.map((key) => [key, firefoxLocale[key]])), + `${filename}: install guide translations should match across browser builds`, + ); + } +}); + test('chrome /record reports mic denial as a warning, not recording failure', () => { const panel = fs.readFileSync(path.join(ROOT, 'src/chrome/src/ui/sidepanel.js'), 'utf8'); const locale = fs.readFileSync(path.join(ROOT, 'src/chrome/src/ui/locales/en.js'), 'utf8'); From 4e964d5350c3a4a6d862178143d85f6008f69a60 Mon Sep 17 00:00:00 2001 From: Emre Sokullu Date: Fri, 17 Jul 2026 09:33:02 +0300 Subject: [PATCH 4/7] Skip unnecessary Firefox filename probes --- src/firefox/src/download-directory.js | 24 ++++++++++------- src/firefox/src/network/network-tools.js | 27 ++++++++++++++----- test/run.js | 33 +++++++++++++++++++++++- 3 files changed, 67 insertions(+), 17 deletions(-) diff --git a/src/firefox/src/download-directory.js b/src/firefox/src/download-directory.js index e79c53fb..4603382f 100644 --- a/src/firefox/src/download-directory.js +++ b/src/firefox/src/download-directory.js @@ -30,6 +30,16 @@ export function filenameInDownloadDirectory(directory, filename) { return `${normalizedDirectory}/${basename}`; } +export async function configuredDownloadDirectory(api) { + if (!api?.storage?.local?.get) return ''; + try { + const stored = await api.storage.local.get(DOWNLOAD_DIRECTORY_STORAGE_KEY); + return normalizeDownloadDirectory(stored?.[DOWNLOAD_DIRECTORY_STORAGE_KEY]); + } catch { + return ''; + } +} + /** * Firefox does not expose downloads.onDeterminingFilename. Its download call * sites therefore resolve the configured relative path before starting each @@ -40,14 +50,8 @@ export function filenameInDownloadDirectory(directory, filename) { */ export async function filenameInConfiguredDownloadDirectory(api, filename) { const original = filename || undefined; - if (!api?.storage?.local?.get) return original; - try { - const stored = await api.storage.local.get(DOWNLOAD_DIRECTORY_STORAGE_KEY); - const directory = normalizeDownloadDirectory(stored?.[DOWNLOAD_DIRECTORY_STORAGE_KEY]); - if (!directory) return original; - if (!original) return undefined; - return filenameInDownloadDirectory(directory, original) || original; - } catch { - return original; - } + const directory = await configuredDownloadDirectory(api); + if (!directory) return original; + if (!original) return undefined; + return filenameInDownloadDirectory(directory, original) || original; } diff --git a/src/firefox/src/network/network-tools.js b/src/firefox/src/network/network-tools.js index 83375bd6..de3b6619 100644 --- a/src/firefox/src/network/network-tools.js +++ b/src/firefox/src/network/network-tools.js @@ -1,5 +1,9 @@ import { firefoxRestrictedDomainFailure } from '../firefox-restricted-domains.js'; -import { filenameInConfiguredDownloadDirectory } from '../download-directory.js'; +import { + configuredDownloadDirectory, + filenameInConfiguredDownloadDirectory, + filenameInDownloadDirectory, +} from '../download-directory.js'; /** * Network & download tools for the WebBrain agent (Firefox). @@ -468,6 +472,17 @@ async function discoverHttpDownloadFilename(url) { } } +async function filenameForConfiguredHttpDownload(filename, url) { + const explicitFilename = safeDownloadFilename(filename); + if (explicitFilename) { + return filenameInConfiguredDownloadDirectory(browser, explicitFilename); + } + const directory = await configuredDownloadDirectory(browser); + if (!directory) return undefined; + const discoveredFilename = await discoverHttpDownloadFilename(url); + return filenameInDownloadDirectory(directory, discoveredFilename) || undefined; +} + function defaultSkillDownloadFilename(contentType) { const type = safeDataUrlMimeType(contentType); const extension = { @@ -2148,8 +2163,7 @@ export async function downloadResourceFromPage(tabId, args = {}) { }; } - const suggestedFilename = filename || await discoverHttpDownloadFilename(r.url); - const downloadFilename = await filenameInConfiguredDownloadDirectory(browser, suggestedFilename); + const downloadFilename = await filenameForConfiguredHttpDownload(filename, r.url); const downloadId = await browser.downloads.download({ url: r.url, filename: downloadFilename, @@ -2216,9 +2230,10 @@ export async function downloadFiles(args = {}) { if (i >= urls.length) return; const url = urls[i]; try { - const suggestedFilename = (i === 0 ? singleFilename : null) - || await discoverHttpDownloadFilename(url); - const filename = await filenameInConfiguredDownloadDirectory(browser, suggestedFilename); + const filename = await filenameForConfiguredHttpDownload( + i === 0 ? singleFilename : null, + url, + ); const opts = { url, conflictAction: 'uniquify' }; if (filename) opts.filename = filename; const downloadId = await browser.downloads.download(opts); diff --git a/test/run.js b/test/run.js index 600f875b..f456d697 100644 --- a/test/run.js +++ b/test/run.js @@ -2259,6 +2259,11 @@ test('download directory routing is relative and uses each browser-supported pat undefined, 'firefox: missing filenames should be left to the browser instead of overriding Content-Disposition', ); + assert.equal( + await DownloadDirectoryFx.configuredDownloadDirectory(firefoxApi), + 'Work/Exports', + 'firefox: callers should be able to check whether routing is configured before probing a URL', + ); }); test('Firefox download_files preserves server-selected filenames inside the configured directory', async () => { @@ -2266,12 +2271,13 @@ test('Firefox download_files preserves server-selected filenames inside the conf const originalFetch = globalThis.fetch; const downloadCalls = []; const fetchCalls = []; + let configuredDirectory = 'Work/WebBrain'; try { globalThis.browser = { storage: { local: { async get() { - return { downloadDirectory: 'Work/WebBrain' }; + return { downloadDirectory: configuredDirectory }; }, }, }, @@ -2331,6 +2337,31 @@ test('Firefox download_files preserves server-selected filenames inside the conf false, 'an unavailable HEAD response should leave filename selection to Firefox', ); + + configuredDirectory = ''; + globalThis.fetch = async () => { + throw new Error('filename discovery should not run without a configured directory'); + }; + const systemDefault = await downloadFilesFx({ urls: ['https://example.com/export?id=3'] }); + assert.equal(systemDefault.success, true); + assert.equal(downloadCalls.length, 3); + assert.equal( + Object.hasOwn(downloadCalls[2], 'filename'), + false, + 'the system-default setting should not probe or override Firefox filename selection', + ); + + const explicit = await downloadFilesFx({ + urls: ['https://example.com/export?id=4'], + filename: 'manual-report.csv', + }); + assert.equal(explicit.success, true); + assert.equal(downloadCalls.length, 4); + assert.equal( + downloadCalls[3].filename, + 'manual-report.csv', + 'an explicit user filename should not require a HEAD probe when no directory is configured', + ); } finally { if (originalBrowser === undefined) delete globalThis.browser; else globalThis.browser = originalBrowser; From 2ae8546a279789f767840370d1c4af07bfa7d8bf Mon Sep 17 00:00:00 2001 From: Emre Sokullu Date: Fri, 17 Jul 2026 09:41:57 +0300 Subject: [PATCH 5/7] Honor GET-selected Firefox filenames --- src/firefox/src/network/network-tools.js | 16 +------------- test/run.js | 28 +++++++++++++++++++----- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/firefox/src/network/network-tools.js b/src/firefox/src/network/network-tools.js index de3b6619..0f0f9fc9 100644 --- a/src/firefox/src/network/network-tools.js +++ b/src/firefox/src/network/network-tools.js @@ -423,19 +423,6 @@ export function filenameFromContentDisposition(value) { return safeDownloadFilename(parameters.get('filename')); } -function filenameFromHttpUrl(value) { - try { - const url = new URL(String(value || '')); - if (url.protocol !== 'http:' && url.protocol !== 'https:') return undefined; - const encoded = url.pathname.split('/').filter(Boolean).pop() || ''; - let decoded = encoded; - try { decoded = decodeURIComponent(encoded); } catch (_) {} - return safeDownloadFilename(decoded); - } catch { - return undefined; - } -} - /** * Firefox has no downloads.onDeterminingFilename event, so a complete filename * must be supplied to downloads.download() to select a subdirectory. Discover @@ -463,8 +450,7 @@ async function discoverHttpDownloadFilename(url) { const responseUrl = response.url || target; const responseUrlCheck = validateFetchUrl(responseUrl, { allowLocalNetwork: getAllowLocalNetwork() }); if (!responseUrlCheck.ok) return undefined; - return filenameFromContentDisposition(response.headers?.get?.('content-disposition')) - || filenameFromHttpUrl(responseUrl); + return filenameFromContentDisposition(response.headers?.get?.('content-disposition')); } catch { return undefined; } finally { diff --git a/test/run.js b/test/run.js index f456d697..6188c4de 100644 --- a/test/run.js +++ b/test/run.js @@ -2338,27 +2338,43 @@ test('Firefox download_files preserves server-selected filenames inside the conf 'an unavailable HEAD response should leave filename selection to Firefox', ); + globalThis.fetch = async (url) => ({ + ok: true, + status: 200, + type: 'basic', + url, + headers: { get() { return null; } }, + }); + const getSelected = await downloadFilesFx({ urls: ['https://example.com/download?id=3'] }); + assert.equal(getSelected.success, true); + assert.equal(downloadCalls.length, 3); + assert.equal( + Object.hasOwn(downloadCalls[2], 'filename'), + false, + 'a successful HEAD without Content-Disposition should not override the actual GET filename', + ); + configuredDirectory = ''; globalThis.fetch = async () => { throw new Error('filename discovery should not run without a configured directory'); }; - const systemDefault = await downloadFilesFx({ urls: ['https://example.com/export?id=3'] }); + const systemDefault = await downloadFilesFx({ urls: ['https://example.com/export?id=4'] }); assert.equal(systemDefault.success, true); - assert.equal(downloadCalls.length, 3); + assert.equal(downloadCalls.length, 4); assert.equal( - Object.hasOwn(downloadCalls[2], 'filename'), + Object.hasOwn(downloadCalls[3], 'filename'), false, 'the system-default setting should not probe or override Firefox filename selection', ); const explicit = await downloadFilesFx({ - urls: ['https://example.com/export?id=4'], + urls: ['https://example.com/export?id=5'], filename: 'manual-report.csv', }); assert.equal(explicit.success, true); - assert.equal(downloadCalls.length, 4); + assert.equal(downloadCalls.length, 5); assert.equal( - downloadCalls[3].filename, + downloadCalls[4].filename, 'manual-report.csv', 'an explicit user filename should not require a HEAD probe when no directory is configured', ); From 5ff2dcf7fb4b29d9418612b89c5eadfd43bd3162 Mon Sep 17 00:00:00 2001 From: Emre Sokullu Date: Fri, 17 Jul 2026 09:50:40 +0300 Subject: [PATCH 6/7] Preserve one-time Firefox downloads --- src/chrome/src/download-directory.js | 10 ++- src/firefox/src/download-directory.js | 10 ++- src/firefox/src/network/network-tools.js | 58 ++---------------- test/run.js | 77 +++++++----------------- 4 files changed, 40 insertions(+), 115 deletions(-) diff --git a/src/chrome/src/download-directory.js b/src/chrome/src/download-directory.js index 911b4670..3d08943a 100644 --- a/src/chrome/src/download-directory.js +++ b/src/chrome/src/download-directory.js @@ -20,13 +20,17 @@ export function filenameInDownloadDirectory(directory, filename) { const normalizedDirectory = normalizeDownloadDirectory(directory); if (!normalizedDirectory) return ''; - const basename = String(filename ?? '') + let basename = String(filename ?? '') .replace(/\\/g, '/') .split('/') .pop() - .replace(/[\u0000-\u001f\u007f]/g, '') + .replace(/[\u0000-\u001f\u007f<>:"|?*]/g, '_') .trim(); - if (!basename || basename === '.' || basename === '..') return ''; + 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}`; } diff --git a/src/firefox/src/download-directory.js b/src/firefox/src/download-directory.js index 4603382f..7ea73e60 100644 --- a/src/firefox/src/download-directory.js +++ b/src/firefox/src/download-directory.js @@ -20,13 +20,17 @@ export function filenameInDownloadDirectory(directory, filename) { const normalizedDirectory = normalizeDownloadDirectory(directory); if (!normalizedDirectory) return ''; - const basename = String(filename ?? '') + let basename = String(filename ?? '') .replace(/\\/g, '/') .split('/') .pop() - .replace(/[\u0000-\u001f\u007f]/g, '') + .replace(/[\u0000-\u001f\u007f<>:"|?*]/g, '_') .trim(); - if (!basename || basename === '.' || basename === '..') return ''; + 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}`; } diff --git a/src/firefox/src/network/network-tools.js b/src/firefox/src/network/network-tools.js index 0f0f9fc9..64a0ac1c 100644 --- a/src/firefox/src/network/network-tools.js +++ b/src/firefox/src/network/network-tools.js @@ -1,9 +1,5 @@ import { firefoxRestrictedDomainFailure } from '../firefox-restricted-domains.js'; -import { - configuredDownloadDirectory, - filenameInConfiguredDownloadDirectory, - filenameInDownloadDirectory, -} from '../download-directory.js'; +import { filenameInConfiguredDownloadDirectory } from '../download-directory.js'; /** * Network & download tools for the WebBrain agent (Firefox). @@ -423,50 +419,9 @@ export function filenameFromContentDisposition(value) { return safeDownloadFilename(parameters.get('filename')); } -/** - * Firefox has no downloads.onDeterminingFilename event, so a complete filename - * must be supplied to downloads.download() to select a subdirectory. Discover - * the server's preferred basename with a bounded HEAD request first. If the - * request cannot be inspected, return undefined and let Firefox preserve its - * normal Content-Disposition filename instead of fabricating one. - */ -async function discoverHttpDownloadFilename(url) { - const target = String(url || '').trim(); - const urlCheck = validateFetchUrl(target, { allowLocalNetwork: getAllowLocalNetwork() }); - if (!urlCheck.ok) return undefined; - const controller = typeof AbortController === 'function' ? new AbortController() : null; - const timeout = controller ? setTimeout(() => controller.abort(), 5000) : null; - try { - const response = await fetch(target, { - method: 'HEAD', - credentials: 'include', - redirect: 'manual', - cache: 'no-store', - ...(controller ? { signal: controller.signal } : {}), - }); - if (response?.type === 'opaqueredirect' || isHttpRedirectStatus(response?.status) || !response?.ok) { - return undefined; - } - const responseUrl = response.url || target; - const responseUrlCheck = validateFetchUrl(responseUrl, { allowLocalNetwork: getAllowLocalNetwork() }); - if (!responseUrlCheck.ok) return undefined; - return filenameFromContentDisposition(response.headers?.get?.('content-disposition')); - } catch { - return undefined; - } finally { - if (timeout) clearTimeout(timeout); - } -} - -async function filenameForConfiguredHttpDownload(filename, url) { +async function filenameForConfiguredDownload(filename) { const explicitFilename = safeDownloadFilename(filename); - if (explicitFilename) { - return filenameInConfiguredDownloadDirectory(browser, explicitFilename); - } - const directory = await configuredDownloadDirectory(browser); - if (!directory) return undefined; - const discoveredFilename = await discoverHttpDownloadFilename(url); - return filenameInDownloadDirectory(directory, discoveredFilename) || undefined; + return filenameInConfiguredDownloadDirectory(browser, explicitFilename); } function defaultSkillDownloadFilename(contentType) { @@ -2149,7 +2104,7 @@ export async function downloadResourceFromPage(tabId, args = {}) { }; } - const downloadFilename = await filenameForConfiguredHttpDownload(filename, r.url); + const downloadFilename = await filenameForConfiguredDownload(filename); const downloadId = await browser.downloads.download({ url: r.url, filename: downloadFilename, @@ -2216,10 +2171,7 @@ export async function downloadFiles(args = {}) { if (i >= urls.length) return; const url = urls[i]; try { - const filename = await filenameForConfiguredHttpDownload( - i === 0 ? singleFilename : null, - url, - ); + const filename = await filenameForConfiguredDownload(i === 0 ? singleFilename : null); const opts = { url, conflictAction: 'uniquify' }; if (filename) opts.filename = filename; const downloadId = await browser.downloads.download(opts); diff --git a/test/run.js b/test/run.js index 6188c4de..4bf3d76f 100644 --- a/test/run.js +++ b/test/run.js @@ -2175,6 +2175,11 @@ test('download directory routing is relative and uses each browser-supported pat 'Work/WebBrain/report.pdf', `${label}: configured directory should prefix the tentative basename`, ); + assert.equal( + downloadDirectory.filenameInDownloadDirectory('Work/WebBrain', 'Q1: "report"?.pdf'), + 'Work/WebBrain/Q1_ _report__.pdf', + `${label}: explicit basenames should be safe for downloads.download`, + ); } let storageChangeListener = null; @@ -2266,11 +2271,10 @@ test('download directory routing is relative and uses each browser-supported pat ); }); -test('Firefox download_files preserves server-selected filenames inside the configured directory', async () => { +test('Firefox download_files routes known filenames without probing unknown downloads', async () => { const originalBrowser = globalThis.browser; const originalFetch = globalThis.fetch; const downloadCalls = []; - const fetchCalls = []; let configuredDirectory = 'Work/WebBrain'; try { globalThis.browser = { @@ -2298,85 +2302,46 @@ test('Firefox download_files preserves server-selected filenames inside the conf }, }; globalThis.fetch = async (url, options) => { - fetchCalls.push({ url, options }); - return { - ok: true, - status: 200, - type: 'basic', - url, - headers: { - get(name) { - return String(name).toLowerCase() === 'content-disposition' - ? 'attachment; filename="quarterly-report.pdf"' - : null; - }, - }, - }; + throw new Error(`filename discovery should not request ${url} with ${options?.method || 'GET'}`); }; - const result = await downloadFilesFx({ urls: ['https://example.com/export?id=1'] }); + const result = await downloadFilesFx({ + urls: ['https://example.com/export?id=1'], + filename: 'quarterly-report.pdf', + }); assert.equal(result.success, true); assert.equal(downloadCalls.length, 1); assert.equal(downloadCalls[0].filename, 'Work/WebBrain/quarterly-report.pdf'); - assert.equal(fetchCalls.length, 1); - assert.equal(fetchCalls[0].options.method, 'HEAD'); - assert.equal(fetchCalls[0].options.credentials, 'include'); - globalThis.fetch = async () => ({ - ok: false, - status: 405, - type: 'basic', - url: 'https://example.com/export?id=2', - headers: { get() { return null; } }, - }); - const fallback = await downloadFilesFx({ urls: ['https://example.com/export?id=2'] }); + const fallback = await downloadFilesFx({ urls: ['https://example.com/one-time?id=2'] }); assert.equal(fallback.success, true); assert.equal(downloadCalls.length, 2); assert.equal( Object.hasOwn(downloadCalls[1], 'filename'), false, - 'an unavailable HEAD response should leave filename selection to Firefox', - ); - - globalThis.fetch = async (url) => ({ - ok: true, - status: 200, - type: 'basic', - url, - headers: { get() { return null; } }, - }); - const getSelected = await downloadFilesFx({ urls: ['https://example.com/download?id=3'] }); - assert.equal(getSelected.success, true); - assert.equal(downloadCalls.length, 3); - assert.equal( - Object.hasOwn(downloadCalls[2], 'filename'), - false, - 'a successful HEAD without Content-Disposition should not override the actual GET filename', + 'an unknown one-time download should be requested exactly once and keep Firefox filename selection', ); configuredDirectory = ''; - globalThis.fetch = async () => { - throw new Error('filename discovery should not run without a configured directory'); - }; - const systemDefault = await downloadFilesFx({ urls: ['https://example.com/export?id=4'] }); + const systemDefault = await downloadFilesFx({ urls: ['https://example.com/export?id=3'] }); assert.equal(systemDefault.success, true); - assert.equal(downloadCalls.length, 4); + assert.equal(downloadCalls.length, 3); assert.equal( - Object.hasOwn(downloadCalls[3], 'filename'), + Object.hasOwn(downloadCalls[2], 'filename'), false, - 'the system-default setting should not probe or override Firefox filename selection', + 'the system-default setting should not override Firefox filename selection', ); const explicit = await downloadFilesFx({ - urls: ['https://example.com/export?id=5'], + urls: ['https://example.com/export?id=4'], filename: 'manual-report.csv', }); assert.equal(explicit.success, true); - assert.equal(downloadCalls.length, 5); + assert.equal(downloadCalls.length, 4); assert.equal( - downloadCalls[4].filename, + downloadCalls[3].filename, 'manual-report.csv', - 'an explicit user filename should not require a HEAD probe when no directory is configured', + 'an explicit user filename should be preserved when no directory is configured', ); } finally { if (originalBrowser === undefined) delete globalThis.browser; From a12f973250e7b7f8ab0ef003b5c55a2e688dd9bf Mon Sep 17 00:00:00 2001 From: Emre Sokullu Date: Fri, 17 Jul 2026 13:22:55 +0300 Subject: [PATCH 7/7] 24.2.1 --- package-lock.json | 4 ++-- package.json | 2 +- src/chrome/ARCHITECTURE.md | 2 +- src/chrome/manifest.json | 2 +- src/chrome/src/ui/settings.js | 2 +- src/firefox/ARCHITECTURE.md | 2 +- src/firefox/manifest.json | 2 +- src/firefox/src/ui/settings.js | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8b4fa8a6..22adde56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "webbrain", - "version": "24.2.0", + "version": "24.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "webbrain", - "version": "24.2.0", + "version": "24.2.1", "license": "MIT", "devDependencies": { "playwright": "^1.48.0" diff --git a/package.json b/package.json index c76e9736..a4760eba 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/chrome/ARCHITECTURE.md b/src/chrome/ARCHITECTURE.md index f86d4c09..afeb5c03 100644 --- a/src/chrome/ARCHITECTURE.md +++ b/src/chrome/ARCHITECTURE.md @@ -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 diff --git a/src/chrome/manifest.json b/src/chrome/manifest.json index e7614569..903257c1 100644 --- a/src/chrome/manifest.json +++ b/src/chrome/manifest.json @@ -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", diff --git a/src/chrome/src/ui/settings.js b/src/chrome/src/ui/settings.js index f29f2b01..90c9e9f7 100644 --- a/src/chrome/src/ui/settings.js +++ b/src/chrome/src/ui/settings.js @@ -39,7 +39,7 @@ import { // Version shown in the subtitle. Kept here so it only needs one update per // release; the subtitle string itself is translated. -const EXT_VERSION = '24.2.0'; +const EXT_VERSION = '24.2.1'; const providersContainer = document.getElementById('providers'); const displaySettings = document.getElementById('display-settings'); diff --git a/src/firefox/ARCHITECTURE.md b/src/firefox/ARCHITECTURE.md index 7639df69..0490728b 100644 --- a/src/firefox/ARCHITECTURE.md +++ b/src/firefox/ARCHITECTURE.md @@ -1,6 +1,6 @@ # WebBrain Firefox Extension — Architecture -> Version 24.2.0 · Manifest V2 · Background Page +> Version 24.2.1 · Manifest V2 · Background Page ## How Firefox Differs from Chrome diff --git a/src/firefox/manifest.json b/src/firefox/manifest.json index f50902d5..7e094f17 100644 --- a/src/firefox/manifest.json +++ b/src/firefox/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "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": [ "activeTab", diff --git a/src/firefox/src/ui/settings.js b/src/firefox/src/ui/settings.js index 3e73f1d8..b4904124 100644 --- a/src/firefox/src/ui/settings.js +++ b/src/firefox/src/ui/settings.js @@ -39,7 +39,7 @@ import { // Version shown in the subtitle. Kept here so it only needs one update per // release; the subtitle string itself is translated. -const EXT_VERSION = '24.2.0'; +const EXT_VERSION = '24.2.1'; const providersContainer = document.getElementById('providers'); const displaySettings = document.getElementById('display-settings');