From 1785b589d93d32f9fbb7a71c2285b0a83263d3bb Mon Sep 17 00:00:00 2001 From: Nikhil Sonti Date: Mon, 11 Aug 2025 11:46:10 -0700 Subject: [PATCH] update to new browseros patches, v20 release --- build/config/NXTSCAPE_VERSION | 2 +- build/config/gn/flags.linux.debug.gn | 3 +- build/context.py | 2 +- docs/appcast-x86_64.xml | 17 + docs/appcast.xml | 17 + .../add-sparkle-info-plist-keys.patch | 16 +- .../adding-new-vector-icons.patch | 6 +- .../branding-file-updates.patch} | 4 +- .../browseros-ai-settings-page.patch} | 1472 ++++------ .../browseros-api-updates.patch} | 1164 +++++++- .../browseros-api.patch} | 2453 ++++++++++++++--- patches/browseros/browseros-metrics.patch | 954 +++++++ .../browseros-ota-updater.patch} | 2 +- .../chrome-importer.patch} | 210 +- .../browseros/chrome-version-updater.patch | 24 + .../disable-chrome-labs-pinning.patch | 12 +- .../disable-google-key-info-bar.patch | 6 +- .../disable-info-bar-in-cdp.patch | 6 +- ...ser-gesture-restriction-on-sidepanel.patch | 6 +- .../llm-chat.patch} | 4 +- .../llm-hub.patch} | 4 +- .../mac-sparkle-updater.patch} | 12 +- .../pin-chat-and-hub.patch} | 2 +- .../pin-extensions-toolbar.patch} | 2 +- .../preferences-settings-page.patch} | 34 +- .../updates-llm-chat-and-hub.patch} | 118 +- .../BrowserOS-API-for-get-set-prefs.patch | 314 --- .../nxtscape/UI-updating-About-string.patch | 25 - patches/nxtscape/ai-chat-extension.patch | 114 - patches/nxtscape/branding.patch | 188 -- ...seros-api-fix-coordinate-calculation.patch | 718 ----- .../nxtscape/browseros-api-updates-1.patch | 1691 ------------ patches/nxtscape/bug-reporter-extension.patch | 111 - patches/nxtscape/first-run-nxtscape.patch | 261 -- patches/nxtscape/importer-updates.patch | 249 -- patches/nxtscape/new-snapshot-API.patch | 1221 -------- patches/nxtscape/nxtscape-settings-ui.patch | 1170 -------- patches/nxtscape/pin-nxtscape-ai-chat.patch | 33 - patches/series | 57 +- patches/series.backup | 36 + resources/entitlements/Info.plist.additions | 2 + 41 files changed, 5047 insertions(+), 7695 deletions(-) rename patches/{nxtscape => browseros}/add-sparkle-info-plist-keys.patch (62%) rename patches/{nxtscape => browseros}/adding-new-vector-icons.patch (96%) rename patches/{nxtscape/fixes-to-branding.patch => browseros/branding-file-updates.patch} (98%) rename patches/{nxtscape/New-BrowserOS-settings-AI-page-for-providers.patch => browseros/browseros-ai-settings-page.patch} (51%) rename patches/{nxtscape/BorwserOS-API-updates-improved-click-type-ad-clear-w.patch => browseros/browseros-api-updates.patch} (52%) rename patches/{nxtscape/browserOS-API.patch => browseros/browseros-api.patch} (59%) create mode 100644 patches/browseros/browseros-metrics.patch rename patches/{nxtscape/extensions-OTA-updater.patch => browseros/browseros-ota-updater.patch} (99%) rename patches/{nxtscape/add-importer-supporter-for-chrome.patch => browseros/chrome-importer.patch} (89%) create mode 100644 patches/browseros/chrome-version-updater.patch rename patches/{nxtscape => browseros}/disable-chrome-labs-pinning.patch (88%) rename patches/{nxtscape => browseros}/disable-google-key-info-bar.patch (89%) rename patches/{nxtscape => browseros}/disable-info-bar-in-cdp.patch (85%) rename patches/{nxtscape => browseros}/disable-user-gesture-restriction-on-sidepanel.patch (86%) rename patches/{nxtscape/embed-third-party-llm-in-side-panel.patch => browseros/llm-chat.patch} (99%) rename patches/{nxtscape/clash-of-gpts.patch => browseros/llm-hub.patch} (99%) rename patches/{nxtscape/nxtscape-updater-sparkle.patch => browseros/mac-sparkle-updater.patch} (99%) rename patches/{nxtscape/pin-browseros-native-ui.patch => browseros/pin-chat-and-hub.patch} (99%) rename patches/{nxtscape/pin-browseros-extensions.patch => browseros/pin-extensions-toolbar.patch} (98%) rename patches/{nxtscape/browseros-settings.patch => browseros/preferences-settings-page.patch} (96%) rename patches/{nxtscape/updates-to-llm-chat-and-hub.patch => browseros/updates-llm-chat-and-hub.patch} (82%) delete mode 100644 patches/nxtscape/BrowserOS-API-for-get-set-prefs.patch delete mode 100644 patches/nxtscape/UI-updating-About-string.patch delete mode 100644 patches/nxtscape/ai-chat-extension.patch delete mode 100644 patches/nxtscape/branding.patch delete mode 100644 patches/nxtscape/browseros-api-fix-coordinate-calculation.patch delete mode 100644 patches/nxtscape/browseros-api-updates-1.patch delete mode 100644 patches/nxtscape/bug-reporter-extension.patch delete mode 100644 patches/nxtscape/first-run-nxtscape.patch delete mode 100644 patches/nxtscape/importer-updates.patch delete mode 100644 patches/nxtscape/new-snapshot-API.patch delete mode 100644 patches/nxtscape/nxtscape-settings-ui.patch delete mode 100644 patches/nxtscape/pin-nxtscape-ai-chat.patch create mode 100644 patches/series.backup diff --git a/build/config/NXTSCAPE_VERSION b/build/config/NXTSCAPE_VERSION index 9e5feb525..95f9650f0 100644 --- a/build/config/NXTSCAPE_VERSION +++ b/build/config/NXTSCAPE_VERSION @@ -1 +1 @@ -46 +49 diff --git a/build/config/gn/flags.linux.debug.gn b/build/config/gn/flags.linux.debug.gn index 72eebc5ea..8b8bffc87 100644 --- a/build/config/gn/flags.linux.debug.gn +++ b/build/config/gn/flags.linux.debug.gn @@ -5,6 +5,7 @@ is_debug = true is_official_build = false is_component_build = false +use_siso=false # Compiler use_sysroot = true @@ -42,4 +43,4 @@ v8_enable_i18n_support = true # Debug specific dcheck_always_on = true -enable_iterator_debugging = false \ No newline at end of file +enable_iterator_debugging = false diff --git a/build/context.py b/build/context.py index 682da4474..57d6d2ccf 100644 --- a/build/context.py +++ b/build/context.py @@ -130,7 +130,7 @@ class BuildContext: def get_nxtscape_patches_dir(self) -> Path: """Get Nxtscape specific patches directory""" - return join_paths(self.get_patches_dir(), "nxtscape") + return join_paths(self.get_patches_dir(), "browseros") def get_sparkle_dir(self) -> Path: """Get Sparkle directory""" diff --git a/docs/appcast-x86_64.xml b/docs/appcast-x86_64.xml index 01a06d709..dc115e53c 100644 --- a/docs/appcast-x86_64.xml +++ b/docs/appcast-x86_64.xml @@ -8,6 +8,23 @@ en + + Nxtscape - 0.20.0 + +- New updated Agent UI! +- Fixed MacOS bug which caused the app to crash on startup for some users. This unfortunately also makes a breaking change, requiring re-installation of extensions and logins. + + 7200.69 + 0.20.1 + Fri, 09 Aug 2025 16:30:00 -0700 + https://nxtscape.ai + + 10.15 + Nxtscape - 0.19.0 diff --git a/docs/appcast.xml b/docs/appcast.xml index fbeedc283..b04539d08 100644 --- a/docs/appcast.xml +++ b/docs/appcast.xml @@ -8,6 +8,23 @@ en + + Nxtscape - 0.20.0 + +- New updated Agent UI! +- Fixed MacOS bug which caused the app to crash on startup for some users. This unfortunately also makes a breaking change, requiring re-installation of extensions and logins. + + 7200.69 + 0.20.1 + Fri, 09 Aug 2025 16:30:00 -0700 + https://nxtscape.ai + + 10.15 + Nxtscape - 0.19.0 diff --git a/patches/nxtscape/add-sparkle-info-plist-keys.patch b/patches/browseros/add-sparkle-info-plist-keys.patch similarity index 62% rename from patches/nxtscape/add-sparkle-info-plist-keys.patch rename to patches/browseros/add-sparkle-info-plist-keys.patch index 676da4040..ceffccc65 100644 --- a/patches/nxtscape/add-sparkle-info-plist-keys.patch +++ b/patches/browseros/add-sparkle-info-plist-keys.patch @@ -1,17 +1,17 @@ -From 5baf27ebd196f1762523600c41dda406e092c7a0 Mon Sep 17 00:00:00 2001 +From 84acdc3e97b088b1c68b3cfbda04ddac80fbd35a Mon Sep 17 00:00:00 2001 From: Nikhil Sonti -Date: Tue, 1 Jul 2025 13:57:56 -0700 -Subject: [PATCH] patch app info with Sparkler udpater +Date: Tue, 22 Jul 2025 21:35:46 -0700 +Subject: [PATCH] patch: app-info.plist changes --- - chrome/app/app-Info.plist | 10 ++++++++++ - 1 file changed, 10 insertions(+) + chrome/app/app-Info.plist | 12 ++++++++++++ + 1 file changed, 12 insertions(+) diff --git a/chrome/app/app-Info.plist b/chrome/app/app-Info.plist -index 9190c5ef6c092..debc1d8dabd34 100644 +index 9190c5ef6c092..cf0106b8ba53e 100644 --- a/chrome/app/app-Info.plist +++ b/chrome/app/app-Info.plist -@@ -409,5 +409,15 @@ +@@ -409,5 +409,17 @@ _googlecast._tcp @@ -25,6 +25,8 @@ index 9190c5ef6c092..debc1d8dabd34 100644 + + SUAutomaticallyUpdate + ++ CrProductDirName ++ BrowserOS -- diff --git a/patches/nxtscape/adding-new-vector-icons.patch b/patches/browseros/adding-new-vector-icons.patch similarity index 96% rename from patches/nxtscape/adding-new-vector-icons.patch rename to patches/browseros/adding-new-vector-icons.patch index d1c55ae89..558aac96d 100644 --- a/patches/nxtscape/adding-new-vector-icons.patch +++ b/patches/browseros/adding-new-vector-icons.patch @@ -1,7 +1,7 @@ -From bf9470a28e2b18c40599c7e6d5e7f5c592f29c53 Mon Sep 17 00:00:00 2001 +From 0dc6cadba7a7f65ea9ef822ee7641e314372d663 Mon Sep 17 00:00:00 2001 From: Nikhil Sonti -Date: Fri, 11 Jul 2025 08:55:19 -0700 -Subject: [PATCH] adding new vector icons +Date: Tue, 22 Jul 2025 21:35:58 -0700 +Subject: [PATCH] patch: adding-new-vector-icons --- components/vector_icons/BUILD.gn | 2 + diff --git a/patches/nxtscape/fixes-to-branding.patch b/patches/browseros/branding-file-updates.patch similarity index 98% rename from patches/nxtscape/fixes-to-branding.patch rename to patches/browseros/branding-file-updates.patch index 40ab1c76d..18606a419 100644 --- a/patches/nxtscape/fixes-to-branding.patch +++ b/patches/browseros/branding-file-updates.patch @@ -1,7 +1,7 @@ -From bd59642e3bda4d9d87dd7b0a553e25bd4b49ef9a Mon Sep 17 00:00:00 2001 +From 8d1ca75ba0a9a2f9f9e18c5644e7769cbec977a6 Mon Sep 17 00:00:00 2001 From: Nikhil Sonti Date: Thu, 24 Jul 2025 13:26:50 -0700 -Subject: [PATCH 18/20] browseros branding for file paths +Subject: [PATCH] browseros branding for file paths --- chrome/common/chrome_constants.cc | 18 +++++++++--------- diff --git a/patches/nxtscape/New-BrowserOS-settings-AI-page-for-providers.patch b/patches/browseros/browseros-ai-settings-page.patch similarity index 51% rename from patches/nxtscape/New-BrowserOS-settings-AI-page-for-providers.patch rename to patches/browseros/browseros-ai-settings-page.patch index a43183e76..4209449fc 100644 --- a/patches/nxtscape/New-BrowserOS-settings-AI-page-for-providers.patch +++ b/patches/browseros/browseros-ai-settings-page.patch @@ -1,39 +1,81 @@ -From 3e8f702bd2dca95dee182cd147a2cda413e0c9a6 Mon Sep 17 00:00:00 2001 +From 571131d545b8236884b38dcdb1c2746595414f4f Mon Sep 17 00:00:00 2001 From: Nikhil Sonti -Date: Tue, 5 Aug 2025 17:34:40 -0700 -Subject: [PATCH] New BrowserOS settings AI page for providers +Date: Tue, 22 Jul 2025 21:33:35 -0700 +Subject: [PATCH] patch(M): llm settings page --- - .../api/settings_private/prefs_util.cc | 1 + - chrome/browser/prefs/browser_prefs.cc | 12 +- - .../settings/nxtscape_page/nxtscape_page.html | 840 +++++++++--------- - .../settings/nxtscape_page/nxtscape_page.ts | 620 +++++++++---- - chrome/common/pref_names.h | 7 + - 5 files changed, 905 insertions(+), 575 deletions(-) + .../api/settings_private/prefs_util.cc | 27 + + chrome/browser/prefs/browser_prefs.cc | 35 + + chrome/browser/prefs/browser_prefs.h | 2 + + chrome/browser/resources/settings/BUILD.gn | 1 + + .../settings/nxtscape_page/nxtscape_page.html | 659 ++++++++++++++++++ + .../settings/nxtscape_page/nxtscape_page.ts | 582 ++++++++++++++++ + chrome/browser/resources/settings/route.ts | 1 + + chrome/browser/resources/settings/router.ts | 1 + + chrome/browser/resources/settings/settings.ts | 1 + + .../settings/settings_main/settings_main.html | 6 + + .../settings/settings_main/settings_main.ts | 15 +- + .../settings/settings_menu/settings_menu.html | 6 + + chrome/common/pref_names.h | 11 + + 13 files changed, 1343 insertions(+), 4 deletions(-) + create mode 100644 chrome/browser/resources/settings/nxtscape_page/nxtscape_page.html + create mode 100644 chrome/browser/resources/settings/nxtscape_page/nxtscape_page.ts diff --git a/chrome/browser/extensions/api/settings_private/prefs_util.cc b/chrome/browser/extensions/api/settings_private/prefs_util.cc -index 0ffbb43806598..1bff654e3ac42 100644 +index c27e0e96e4bce..200d72995460e 100644 --- a/chrome/browser/extensions/api/settings_private/prefs_util.cc +++ b/chrome/browser/extensions/api/settings_private/prefs_util.cc -@@ -581,6 +581,7 @@ const PrefsUtil::TypedPrefMap& PrefsUtil::GetAllowlistedKeys() { +@@ -580,6 +580,33 @@ const PrefsUtil::TypedPrefMap& PrefsUtil::GetAllowlistedKeys() { + (*s_allowlist)[::prefs::kCaretBrowsingEnabled] = settings_api::PrefType::kBoolean; - // Nxtscape AI provider preferences ++ // Nxtscape AI provider preferences + (*s_allowlist)[prefs::kBrowserOSProviders] = settings_api::PrefType::kString; - (*s_allowlist)["nxtscape.default_provider"] = settings_api::PrefType::kString; - - // Nxtscape provider settings ++ (*s_allowlist)["nxtscape.default_provider"] = settings_api::PrefType::kString; ++ ++ // Nxtscape provider settings ++ (*s_allowlist)["nxtscape.nxtscape_model"] = settings_api::PrefType::kString; ++ ++ // OpenAI provider settings ++ (*s_allowlist)["nxtscape.openai_api_key"] = settings_api::PrefType::kString; ++ (*s_allowlist)["nxtscape.openai_model"] = settings_api::PrefType::kString; ++ (*s_allowlist)["nxtscape.openai_base_url"] = settings_api::PrefType::kString; ++ ++ // Anthropic provider settings ++ (*s_allowlist)["nxtscape.anthropic_api_key"] = settings_api::PrefType::kString; ++ (*s_allowlist)["nxtscape.anthropic_model"] = settings_api::PrefType::kString; ++ (*s_allowlist)["nxtscape.anthropic_base_url"] = settings_api::PrefType::kString; ++ ++ // Gemini provider settings ++ (*s_allowlist)["nxtscape.gemini_api_key"] = settings_api::PrefType::kString; ++ (*s_allowlist)["nxtscape.gemini_model"] = settings_api::PrefType::kString; ++ (*s_allowlist)["nxtscape.gemini_base_url"] = settings_api::PrefType::kString; ++ ++ // Ollama provider settings ++ (*s_allowlist)["nxtscape.ollama_api_key"] = settings_api::PrefType::kString; ++ (*s_allowlist)["nxtscape.ollama_base_url"] = settings_api::PrefType::kString; ++ (*s_allowlist)["nxtscape.ollama_model"] = settings_api::PrefType::kString; ++ + #if BUILDFLAG(IS_CHROMEOS) + // Accounts / Users / People. + (*s_allowlist)[ash::kAccountsPrefAllowGuest] = diff --git a/chrome/browser/prefs/browser_prefs.cc b/chrome/browser/prefs/browser_prefs.cc -index cf120aacb58d3..7cc63f0c20cc4 100644 +index c83cfdfe3708c..e6f03310af7ee 100644 --- a/chrome/browser/prefs/browser_prefs.cc +++ b/chrome/browser/prefs/browser_prefs.cc -@@ -2324,9 +2324,15 @@ void RegisterGeminiSettingsPrefs(user_prefs::PrefRegistrySyncable* registry) { +@@ -1941,6 +1941,7 @@ void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry, + regional_capabilities::prefs::RegisterProfilePrefs(registry); + RegisterBrowserUserPrefs(registry); + RegisterGeminiSettingsPrefs(registry); ++ RegisterNxtscapePrefs(registry); + RegisterPrefersDefaultScrollbarStylesPrefs(registry); + RegisterSafetyHubProfilePrefs(registry); + #if BUILDFLAG(IS_CHROMEOS) +@@ -2324,6 +2325,40 @@ void RegisterGeminiSettingsPrefs(user_prefs::PrefRegistrySyncable* registry) { + registry->RegisterIntegerPref(prefs::kGeminiSettings, 0); } - void RegisterNxtscapePrefs(user_prefs::PrefRegistrySyncable* registry) { -- // Nxtscape AI provider preferences -- registry->RegisterStringPref("nxtscape.default_provider", "nxtscape"); -- ++void RegisterNxtscapePrefs(user_prefs::PrefRegistrySyncable* registry) { + // AI Provider configurations stored as JSON + // This will store the entire provider configuration including: + // - defaultProviderId @@ -43,30 +85,77 @@ index cf120aacb58d3..7cc63f0c20cc4 100644 + // Legacy preferences (kept for backward compatibility) + registry->RegisterStringPref("nxtscape.default_provider", "browseros"); + - // Nxtscape provider settings - registry->RegisterStringPref("nxtscape.nxtscape_model", ""); ++ // Nxtscape provider settings ++ registry->RegisterStringPref("nxtscape.nxtscape_model", ""); ++ ++ // OpenAI provider settings ++ registry->RegisterStringPref("nxtscape.openai_api_key", ""); ++ registry->RegisterStringPref("nxtscape.openai_model", "gpt-4o"); ++ registry->RegisterStringPref("nxtscape.openai_base_url", ""); ++ ++ // Anthropic provider settings ++ registry->RegisterStringPref("nxtscape.anthropic_api_key", ""); ++ registry->RegisterStringPref("nxtscape.anthropic_model", "claude-3-5-sonnet-latest"); ++ registry->RegisterStringPref("nxtscape.anthropic_base_url", ""); ++ ++ // Gemini provider settings ++ registry->RegisterStringPref("nxtscape.gemini_api_key", ""); ++ registry->RegisterStringPref("nxtscape.gemini_model", "gemini-1.5-pro"); ++ registry->RegisterStringPref("nxtscape.gemini_base_url", ""); ++ ++ // Ollama provider settings ++ registry->RegisterStringPref("nxtscape.ollama_api_key", ""); ++ registry->RegisterStringPref("nxtscape.ollama_base_url", "http://localhost:11434"); ++ registry->RegisterStringPref("nxtscape.ollama_model", ""); ++} ++ + #if BUILDFLAG(IS_CHROMEOS) + void RegisterSigninProfilePrefs(user_prefs::PrefRegistrySyncable* registry, + std::string_view country) { +diff --git a/chrome/browser/prefs/browser_prefs.h b/chrome/browser/prefs/browser_prefs.h +index 3a1c48b14b37f..5600baa2143e0 100644 +--- a/chrome/browser/prefs/browser_prefs.h ++++ b/chrome/browser/prefs/browser_prefs.h +@@ -32,6 +32,8 @@ void RegisterScreenshotPrefs(PrefRegistrySimple* registry); + void RegisterGeminiSettingsPrefs(user_prefs::PrefRegistrySyncable* registry); + ++void RegisterNxtscapePrefs(user_prefs::PrefRegistrySyncable* registry); ++ + // Register all prefs that will be used via a PrefService attached to a user + // Profile using the locale of |g_browser_process|. + void RegisterUserProfilePrefs(user_prefs::PrefRegistrySyncable* registry); +diff --git a/chrome/browser/resources/settings/BUILD.gn b/chrome/browser/resources/settings/BUILD.gn +index 6eb2b37837e97..1a8cd69860514 100644 +--- a/chrome/browser/resources/settings/BUILD.gn ++++ b/chrome/browser/resources/settings/BUILD.gn +@@ -56,6 +56,7 @@ build_webui("build") { + web_component_files = [ + "a11y_page/a11y_page.ts", + "about_page/about_page.ts", ++ "nxtscape_page/nxtscape_page.ts", + "ai_page/ai_compare_subpage.ts", + "ai_page/ai_info_card.ts", + "ai_page/ai_logging_info_bullet.ts", diff --git a/chrome/browser/resources/settings/nxtscape_page/nxtscape_page.html b/chrome/browser/resources/settings/nxtscape_page/nxtscape_page.html -index 28e18b5f69f95..9a429f31f43a0 100644 ---- a/chrome/browser/resources/settings/nxtscape_page/nxtscape_page.html +new file mode 100644 +index 0000000000000..9a429f31f43a0 +--- /dev/null +++ b/chrome/browser/resources/settings/nxtscape_page/nxtscape_page.html -@@ -1,13 +1,23 @@ - +@@ -0,0 +1,659 @@ ++ + - - ++ } ++ } ++ ++ + -
--
-- --
--
--

AI Provider Settings

--
-- Configure your preferred AI provider and model settings. Your selection will be used across all BrowserOS AI features. ++
+
+
+ @@ -611,26 +593,17 @@ index 28e18b5f69f95..9a429f31f43a0 100644 +
+ Add your provider and choose the default LLM +
-
-
--
-- -- -- -- -- -- -- ++ on-change="onDefaultProviderChange_"> + - ++ +
+ + @@ -641,35 +614,11 @@ index 28e18b5f69f95..9a429f31f43a0 100644 + Cancel + + -
-
- --
--
-- --
--

Your API keys are secure

--

-- All API keys are stored locally on your device. Your credentials remain private and encrypted on your computer. --

--
--
--
-- ++
++
++ + -
-- --
--
--
-- N --
--
--

BrowserOS AI

--
-- -- [[getProviderStatus_('nxtscape', prefs.nxtscape.default_provider.value)]] ++
+ +
+

@@ -698,9 +647,7 @@ index 28e18b5f69f95..9a429f31f43a0 100644 + + + -

--
-- Powered by BrowserOS's AI service. ++
+ +
+
-
--
--
-- --
--
- -- --
--
--
-- AI --
--
--

OpenAI

--
-- -- [[getProviderStatus_('openai', prefs.nxtscape.default_provider.value)]] ++
++
++ +
+
+ @@ -746,62 +668,8 @@ index 28e18b5f69f95..9a429f31f43a0 100644 + value="{{dialogBaseUrl_::input}}" + placeholder="Leave empty for default"> +
Override the default API endpoint
-
--
--
--
--
-- -- --
Your OpenAI API key (required)
--
--
-- -- --
Override the OpenAI API base URL (leave empty for default)
--
--
-- -- --
Select the OpenAI model to use for AI operations
--
--
--
- -- --
--
--
-- C --
--
--

Anthropic

--
-- -- [[getProviderStatus_('anthropic', prefs.nxtscape.default_provider.value)]] ++
++ +
+
-
--
--
++
++
+ -
-- ++
+ - --
Your Anthropic API key (required)
++ +
Your API key is encrypted and stored locally
-
--
-- -- --
Override the Anthropic API base URL (leave empty for default)
--
--
-- -- --
Choose your preferred Claude model
--
--
--
- -- --
--
--
-- G --
--
--

Google Gemini

--
-- -- [[getProviderStatus_('gemini', prefs.nxtscape.default_provider.value)]] ++
++ +
+
Model Configuration
+ @@ -897,45 +720,8 @@ index 28e18b5f69f95..9a429f31f43a0 100644 + step="0.1" + placeholder="0.7"> +
-
-
--
--
--
-- -- --
Your Google Gemini API key (required)
--
--
-- -- --
Override the Gemini API base URL (leave empty for default)
--
--
-- -- --
Select the Gemini model to use for AI operations
++
++
+ +
+ @@ -957,22 +743,10 @@ index 28e18b5f69f95..9a429f31f43a0 100644 + on-click="saveProvider_"> + Save + -
-
-
- -- --
--
--
-- O --
--
--

Ollama

--
-- -- [[getProviderStatus_('ollama', prefs.nxtscape.default_provider.value)]] ++
++
++
++ + + -
++
+ - ++ + -
- -- Settings saved successfully --
-\ No newline at end of file ++
++ + Settings saved successfully +
diff --git a/chrome/browser/resources/settings/nxtscape_page/nxtscape_page.ts b/chrome/browser/resources/settings/nxtscape_page/nxtscape_page.ts -index 8c4421ef76e45..e9bebf5e2236f 100644 ---- a/chrome/browser/resources/settings/nxtscape_page/nxtscape_page.ts +new file mode 100644 +index 0000000000000..f33e0463b1bfa +--- /dev/null +++ b/chrome/browser/resources/settings/nxtscape_page/nxtscape_page.ts -@@ -14,6 +14,8 @@ import 'chrome://resources/cr_elements/cr_button/cr_button.js'; - import 'chrome://resources/cr_elements/cr_icon/cr_icon.js'; - import 'chrome://resources/cr_elements/icons.html.js'; - import 'chrome://resources/cr_elements/cr_shared_style.css.js'; +@@ -0,0 +1,582 @@ ++// Copyright 2024 The Chromium Authors ++// Use of this source code is governed by a BSD-style license that can be ++// found in the LICENSE file. ++ ++/** ++ * @fileoverview 'settings-nxtscape-page' contains AI provider-specific settings. ++ */ ++ ++import '../settings_page/settings_section.js'; ++import '../settings_page_styles.css.js'; ++import '../settings_shared.css.js'; ++import '../controls/settings_toggle_button.js'; ++import 'chrome://resources/cr_elements/cr_button/cr_button.js'; ++import 'chrome://resources/cr_elements/cr_icon/cr_icon.js'; ++import 'chrome://resources/cr_elements/icons.html.js'; ++import 'chrome://resources/cr_elements/cr_shared_style.css.js'; +import 'chrome://resources/cr_elements/cr_input/cr_input.js'; +import 'chrome://resources/cr_elements/cr_checkbox/cr_checkbox.js'; - - import {PrefsMixin} from '/shared/settings/prefs/prefs_mixin.js'; - import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; -@@ -22,6 +24,84 @@ import {getTemplate} from './nxtscape_page.html.js'; - - const SettingsNxtscapePageElementBase = PrefsMixin(PolymerElement); - ++ ++import {PrefsMixin} from '/shared/settings/prefs/prefs_mixin.js'; ++import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; ++ ++import {getTemplate} from './nxtscape_page.html.js'; ++ ++const SettingsNxtscapePageElementBase = PrefsMixin(PolymerElement); ++ +export enum ProviderType { + BROWSEROS = 'browseros', + OPENAI_COMPATIBLE = 'openai_compatible', @@ -1169,19 +912,20 @@ index 8c4421ef76e45..e9bebf5e2236f 100644 + } +}; + - export class SettingsNxtscapePageElement extends SettingsNxtscapePageElementBase { - static get is() { - return 'settings-nxtscape-page'; -@@ -33,222 +113,440 @@ export class SettingsNxtscapePageElement extends SettingsNxtscapePageElementBase - - static get properties() { - return { -- /** -- * Preferences state. -- */ - prefs: { - type: Object, - notify: true, ++export class SettingsNxtscapePageElement extends SettingsNxtscapePageElementBase { ++ static get is() { ++ return 'settings-nxtscape-page'; ++ } ++ ++ static get template() { ++ return getTemplate(); ++ } ++ ++ static get properties() { ++ return { ++ prefs: { ++ type: Object, ++ notify: true, + observer: 'onPrefsChanged_', + }, + @@ -1248,12 +992,11 @@ index 8c4421ef76e45..e9bebf5e2236f 100644 + isTestingConnection_: { + type: Boolean, + value: false, - }, - }; - } - -- // Declare prefs property to satisfy ESLint - declare prefs: any; ++ }, ++ }; ++ } ++ ++ declare prefs: any; + private declare providers_: ProviderConfig[]; + private declare defaultProviderId_: string; + private declare showProviderForm_: boolean; @@ -1272,23 +1015,13 @@ index 8c4421ef76e45..e9bebf5e2236f 100644 + super.ready(); + this.loadProviders_(); + } - -- /** -- * Get the CSS class for a provider card based on selection -- */ -- private getProviderCardClass_(provider: string, selectedProvider: string): string { -- return provider === selectedProvider ? 'provider-card selected' : 'provider-card'; ++ + private onPrefsChanged_() { + if (this.prefs && this.prefs.browseros) { + this.loadProviders_(); + } - } - -- /** -- * Get the status text for a provider -- */ -- private getProviderStatus_(provider: string, selectedProvider: string): string { -- return provider === selectedProvider ? 'Active' : 'Inactive'; ++ } ++ + private loadProviders_() { + // Load from preferences or initialize with BrowserOS + if (!this.prefs || !this.prefs.browseros) { @@ -1308,14 +1041,8 @@ index 8c4421ef76e45..e9bebf5e2236f 100644 + } else { + this.initializeDefaultProviders_(); + } - } - -- /** -- * Handle default provider selection change -- */ -- private onDefaultProviderChange_(e: Event) { -- const select = e.target as HTMLSelectElement; -- const value = select.value; ++ } ++ + private initializeDefaultProviders_() { + const browseros: ProviderConfig = { + id: 'browseros', @@ -1326,53 +1053,31 @@ index 8c4421ef76e45..e9bebf5e2236f 100644 + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; - -- // Update the preference using PrefsMixin -- // @ts-ignore: setPrefValue exists at runtime from PrefsMixin -- this.setPrefValue('nxtscape.default_provider', value); -- this.showStatusMessage_(); ++ + this.providers_ = [browseros]; + this.defaultProviderId_ = 'browseros'; + this.saveProviders_(); - } - -- /** -- * Handle Nxtscape model selection change -- */ -- private onNxtscapeModelChange_(e: Event) { -- const select = e.target as HTMLSelectElement; -- const value = select.value; ++ } ++ + private saveProviders_() { + const data: AIProviderPreferences = { + defaultProviderId: this.defaultProviderId_, + providers: this.providers_, + }; - -- // Update the preference using PrefsMixin - // @ts-ignore: setPrefValue exists at runtime from PrefsMixin -- this.setPrefValue('nxtscape.nxtscape_model', value); ++ ++ // @ts-ignore: setPrefValue exists at runtime from PrefsMixin + this.setPrefValue('browseros.providers', JSON.stringify(data)); + console.log('browseros: Saving providers:', data); - this.showStatusMessage_(); - } - -- /** -- * Handle OpenAI model selection change -- */ -- private onOpenAIModelChange_(e: Event) { -- const select = e.target as HTMLSelectElement; -- const value = select.value; ++ this.showStatusMessage_(); ++ } ++ + private generateId_(): string { + return 'provider_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + } + + private onAddProvider_() { + console.log('browseros: Add Provider clicked'); - -- // Update the preference using PrefsMixin -- // @ts-ignore: setPrefValue exists at runtime from PrefsMixin -- this.setPrefValue('nxtscape.openai_model', value); -- this.showStatusMessage_(); ++ + // Toggle the form visibility + this.set('showProviderForm_', !this.showProviderForm_); + @@ -1392,23 +1097,13 @@ index 8c4421ef76e45..e9bebf5e2236f 100644 + } + + console.log('browseros: Form visibility:', this.showProviderForm_); - } - -- /** -- * Handle OpenAI API key change -- */ -- private onOpenAIApiKeyChange_(e: Event) { -- const input = e.target as HTMLInputElement; -- const value = input.value; ++ } ++ + private onEditProvider_(e: Event) { + const target = e.currentTarget as HTMLElement; + const providerId = target.dataset['providerId']; + const provider = this.providers_.find(p => p.id === providerId); - -- // Update the preference using PrefsMixin -- // @ts-ignore: setPrefValue exists at runtime from PrefsMixin -- this.setPrefValue('nxtscape.openai_api_key', value); -- this.showStatusMessage_(); ++ + if (!provider || provider.isBuiltIn) return; + + this.set('editingProvider_', provider); @@ -1422,14 +1117,8 @@ index 8c4421ef76e45..e9bebf5e2236f 100644 + this.set('dialogTemperature_', provider.modelConfig?.temperature || 0.7); + + this.set('showProviderForm_', true); - } - -- /** -- * Handle OpenAI base URL change -- */ -- private onOpenAIBaseUrlChange_(e: Event) { -- const input = e.target as HTMLInputElement; -- const value = input.value; ++ } ++ + private onDeleteProvider_(e: Event) { + e.stopPropagation(); + const target = e.currentTarget as HTMLElement; @@ -1455,11 +1144,7 @@ index 8c4421ef76e45..e9bebf5e2236f 100644 + this.showStatusMessage_('Provider deleted'); + } + } - -- // Update the preference using PrefsMixin -- // @ts-ignore: setPrefValue exists at runtime from PrefsMixin -- this.setPrefValue('nxtscape.openai_base_url', value); -- this.showStatusMessage_(); ++ + private onProviderTypeChange_(event?: Event) { + // Get the new value from the event if available, otherwise use the bound property + let providerType = this.dialogProviderType_; @@ -1482,43 +1167,23 @@ index 8c4421ef76e45..e9bebf5e2236f 100644 + this.set('dialogContextWindow_', defaults.modelConfig?.contextWindow || 128000); + this.set('dialogTemperature_', defaults.modelConfig?.temperature || 0.7); + } - } - -- /** -- * Handle Anthropic model selection change -- */ -- private onAnthropicModelChange_(e: Event) { -- const select = e.target as HTMLSelectElement; -- const value = select.value; ++ } ++ + private async testConnection_() { + this.isTestingConnection_ = true; - -- // Update the preference using PrefsMixin -- // @ts-ignore: setPrefValue exists at runtime from PrefsMixin -- this.setPrefValue('nxtscape.anthropic_model', value); -- this.showStatusMessage_(); ++ + // Simulate API test (in real implementation, this would call C++ backend) + await new Promise(resolve => setTimeout(resolve, 1500)); + + this.isTestingConnection_ = false; + this.showStatusMessage_('Connection successful!'); - } - -- /** -- * Handle Anthropic API key change -- */ -- private onAnthropicApiKeyChange_(e: Event) { -- const input = e.target as HTMLInputElement; -- const value = input.value; ++ } ++ + private saveProvider_() { + if (!this.validateProviderForm_()) { + return; + } - -- // Update the preference using PrefsMixin -- // @ts-ignore: setPrefValue exists at runtime from PrefsMixin -- this.setPrefValue('nxtscape.anthropic_api_key', value); -- this.showStatusMessage_(); ++ + const now = new Date().toISOString(); + + if (this.editingProvider_) { @@ -1565,18 +1230,17 @@ index 8c4421ef76e45..e9bebf5e2236f 100644 + updatedAt: now, + }; + this.push('providers_', newProvider); ++ ++ chrome.send('logBrowserOSMetric', ['settings.provider.added', { ++ provider_type: newProvider.type, ++ model_id: newProvider.modelId ++ }]); + } + + this.saveProviders_(); + this.closeProviderForm_(); - } - -- /** -- * Handle Anthropic base URL change -- */ -- private onAnthropicBaseUrlChange_(e: Event) { -- const input = e.target as HTMLInputElement; -- const value = input.value; ++ } ++ + private validateProviderForm_(): boolean { + console.log('browseros: Validating form:', { + name: this.dialogProviderName_, @@ -1606,64 +1270,46 @@ index 8c4421ef76e45..e9bebf5e2236f 100644 + + return true; + } - -- // Update the preference using PrefsMixin -- // @ts-ignore: setPrefValue exists at runtime from PrefsMixin -- this.setPrefValue('nxtscape.anthropic_base_url', value); -- this.showStatusMessage_(); ++ + private closeProviderForm_() { + this.set('showProviderForm_', false); + this.editingProvider_ = null; - } - -- /** -- * Handle Gemini model selection change -- */ -- private onGeminiModelChange_(e: Event) { ++ } ++ + private onDefaultProviderChange_(e: Event) { - const select = e.target as HTMLSelectElement; -- const value = select.value; -- -- // Update the preference using PrefsMixin -- // @ts-ignore: setPrefValue exists at runtime from PrefsMixin -- this.setPrefValue('nxtscape.gemini_model', value); -- this.showStatusMessage_(); ++ const select = e.target as HTMLSelectElement; ++ const oldProviderId = this.defaultProviderId_; + this.defaultProviderId_ = select.value; + this.updateProvidersDefaultStatus_(); + this.saveProviders_(); - } - -- /** -- * Handle Gemini API key change -- */ -- private onGeminiApiKeyChange_(e: Event) { -- const input = e.target as HTMLInputElement; -- const value = input.value; ++ ++ chrome.send('logBrowserOSMetric', ['settings.default_provider.changed', { ++ old_provider_id: oldProviderId, ++ new_provider_id: this.defaultProviderId_ ++ }]); ++ } ++ + private updateProvidersDefaultStatus_() { + this.providers_ = this.providers_.map(p => ({ + ...p, + isDefault: p.id === this.defaultProviderId_ + })); + } - -- // Update the preference using PrefsMixin -- // @ts-ignore: setPrefValue exists at runtime from PrefsMixin -- this.setPrefValue('nxtscape.gemini_api_key', value); -- this.showStatusMessage_(); ++ + private setAsDefault_(e: Event) { + const target = e.currentTarget as HTMLElement; + const providerId = target.dataset['providerId']; ++ const oldProviderId = this.defaultProviderId_; + this.defaultProviderId_ = providerId!; + this.updateProvidersDefaultStatus_(); + this.saveProviders_(); - } - -- /** -- * Handle Gemini base URL change -- */ -- private onGeminiBaseUrlChange_(e: Event) { -- const input = e.target as HTMLInputElement; -- const value = input.value; ++ ++ chrome.send('logBrowserOSMetric', ['settings.default_provider.changed', { ++ old_provider_id: oldProviderId, ++ new_provider_id: this.defaultProviderId_ ++ }]); ++ } ++ + private getProviderIcon_(type: ProviderType): string { + const icons: Record = { + [ProviderType.BROWSEROS]: 'B', @@ -1676,11 +1322,7 @@ index 8c4421ef76e45..e9bebf5e2236f 100644 + }; + return icons[type] || 'AI'; + } - -- // Update the preference using PrefsMixin -- // @ts-ignore: setPrefValue exists at runtime from PrefsMixin -- this.setPrefValue('nxtscape.gemini_base_url', value); -- this.showStatusMessage_(); ++ + private getProviderCardClass_(provider: ProviderConfig): string { + let classes = 'provider-card'; + if (provider.isDefault) { @@ -1690,23 +1332,13 @@ index 8c4421ef76e45..e9bebf5e2236f 100644 + classes += ' browseros'; + } + return classes; - } - -- /** -- * Handle Ollama API key change -- */ -- private onOllamaApiKeyChange_(e: Event) { -- const input = e.target as HTMLInputElement; -- const value = input.value; ++ } ++ + private getProviderSubtitle_(provider: ProviderConfig): string { + if (provider.type === ProviderType.BROWSEROS) { + return 'Automatically chooses the best model for each task'; + } - -- // Update the preference using PrefsMixin -- // @ts-ignore: setPrefValue exists at runtime from PrefsMixin -- this.setPrefValue('nxtscape.ollama_api_key', value); -- this.showStatusMessage_(); ++ + const parts = []; + if (provider.modelId) { + parts.push(`Model: ${provider.modelId}`); @@ -1715,48 +1347,20 @@ index 8c4421ef76e45..e9bebf5e2236f 100644 + parts.push(`URL: ${this.truncateUrl_(provider.baseUrl)}`); + } + return parts.join(' • ') || 'Not configured'; - } - -- /** -- * Handle Ollama base URL change -- */ -- private onOllamaBaseUrlChange_(e: Event) { -- const input = e.target as HTMLInputElement; -- const value = input.value; -- -- // Update the preference using PrefsMixin -- // @ts-ignore: setPrefValue exists at runtime from PrefsMixin -- this.setPrefValue('nxtscape.ollama_base_url', value); -- this.showStatusMessage_(); ++ } ++ + private isDefaultUrl_(type: ProviderType, url: string): boolean { + const defaults = PROVIDER_DEFAULTS[type]; + return defaults?.baseUrl === url; - } - -- /** -- * Handle Ollama model change -- */ -- private onOllamaModelChange_(e: Event) { -- const input = e.target as HTMLInputElement; -- const value = input.value; -- -- // Update the preference using PrefsMixin -- // @ts-ignore: setPrefValue exists at runtime from PrefsMixin -- this.setPrefValue('nxtscape.ollama_model', value); -- this.showStatusMessage_(); ++ } ++ + private truncateUrl_(url: string): string { + if (url.length > 30) { + return url.substring(0, 27) + '...'; + } + return url; - } - -- /** -- * Show status message briefly -- */ -- private showStatusMessage_() { -- // @ts-ignore: shadowRoot exists at runtime -- const statusMessage = this.shadowRoot!.querySelector('#statusMessage'); ++ } ++ + private getFormCardClass_(show: boolean): string { + return show ? 'show' : ''; + } @@ -1765,36 +1369,160 @@ index 8c4421ef76e45..e9bebf5e2236f 100644 + if (!this.shadowRoot) return; + + const statusMessage = this.shadowRoot.querySelector('#statusMessage') as HTMLElement; - if (statusMessage) { ++ if (statusMessage) { + if (message) { + statusMessage.textContent = message; + } + statusMessage.classList.toggle('error', isError); - statusMessage.classList.add('show'); - setTimeout(() => { - statusMessage.classList.remove('show'); -@@ -264,4 +562,4 @@ declare global { ++ statusMessage.classList.add('show'); ++ setTimeout(() => { ++ statusMessage.classList.remove('show'); ++ }, 2000); ++ } ++ } ++} ++ ++declare global { ++ interface HTMLElementTagNameMap { ++ 'settings-nxtscape-page': SettingsNxtscapePageElement; ++ } ++} ++ ++customElements.define( ++ SettingsNxtscapePageElement.is, SettingsNxtscapePageElement); +diff --git a/chrome/browser/resources/settings/route.ts b/chrome/browser/resources/settings/route.ts +index 2458ecb3791b0..e8dd01dc3e7b6 100644 +--- a/chrome/browser/resources/settings/route.ts ++++ b/chrome/browser/resources/settings/route.ts +@@ -183,6 +183,7 @@ function createRoutes(): SettingsRoutes { + // Root pages. + r.BASIC = new Route('/'); + r.ABOUT = new Route('/help', loadTimeData.getString('aboutPageTitle')); ++ r.NXTSCAPE = new Route('/browseros-ai', 'BrowserOS AI Settings'); + + r.SEARCH = r.BASIC.createSection( + '/search', 'search', loadTimeData.getString('searchPageTitle')); +diff --git a/chrome/browser/resources/settings/router.ts b/chrome/browser/resources/settings/router.ts +index 236c564f9b909..46c2093278ceb 100644 +--- a/chrome/browser/resources/settings/router.ts ++++ b/chrome/browser/resources/settings/router.ts +@@ -14,6 +14,7 @@ import {loadTimeData} from './i18n_setup.js'; + export interface SettingsRoutes { + ABOUT: Route; + ACCESSIBILITY: Route; ++ NXTSCAPE: Route; + ADDRESSES: Route; + ADVANCED: Route; + AI: Route; +diff --git a/chrome/browser/resources/settings/settings.ts b/chrome/browser/resources/settings/settings.ts +index 85e1db9929325..dbd5e82c285f9 100644 +--- a/chrome/browser/resources/settings/settings.ts ++++ b/chrome/browser/resources/settings/settings.ts +@@ -32,6 +32,7 @@ export {OpenWindowProxy, OpenWindowProxyImpl} from 'chrome://resources/js/open_w + export {PluralStringProxyImpl as SettingsPluralStringProxyImpl} from 'chrome://resources/js/plural_string_proxy.js'; + export {getTrustedHTML} from 'chrome://resources/js/static_types.js'; + export {SettingsAboutPageElement} from './about_page/about_page.js'; ++export {SettingsNxtscapePageElement} from './nxtscape_page/nxtscape_page.js'; + export {ControlledRadioButtonElement} from './controls/controlled_radio_button.js'; + export {SettingsDropdownMenuElement} from './controls/settings_dropdown_menu.js'; + export {SettingsToggleButtonElement} from './controls/settings_toggle_button.js'; +diff --git a/chrome/browser/resources/settings/settings_main/settings_main.html b/chrome/browser/resources/settings/settings_main/settings_main.html +index 329e9552760de..403f2f2258fb8 100644 +--- a/chrome/browser/resources/settings/settings_main/settings_main.html ++++ b/chrome/browser/resources/settings/settings_main/settings_main.html +@@ -49,3 +49,9 @@ + prefs="{{prefs}}"> + + ++ +diff --git a/chrome/browser/resources/settings/settings_main/settings_main.ts b/chrome/browser/resources/settings/settings_main/settings_main.ts +index 43fd55ea0b83c..433afef3be384 100644 +--- a/chrome/browser/resources/settings/settings_main/settings_main.ts ++++ b/chrome/browser/resources/settings/settings_main/settings_main.ts +@@ -14,6 +14,7 @@ import 'chrome://resources/cr_elements/icons.html.js'; + import 'chrome://resources/js/search_highlight_utils.js'; + import 'chrome://resources/cr_elements/cr_icon/cr_icon.js'; + import '../about_page/about_page.js'; ++import '../nxtscape_page/nxtscape_page.js'; + import '../basic_page/basic_page.js'; + import '../search_settings.js'; + import '../settings_shared.css.js'; +@@ -32,6 +33,7 @@ import {getTemplate} from './settings_main.html.js'; + interface MainPageVisibility { + about: boolean; + settings: boolean; ++ nxtscape: boolean; } - customElements.define( -- SettingsNxtscapePageElement.is, SettingsNxtscapePageElement); -\ No newline at end of file -+ SettingsNxtscapePageElement.is, SettingsNxtscapePageElement); + export interface SettingsMainElement { +@@ -68,7 +70,7 @@ export class SettingsMainElement extends SettingsMainElementBase { + showPages_: { + type: Object, + value() { +- return {about: false, settings: false}; ++ return {about: false, settings: false, nxtscape: false}; + }, + }, + +@@ -114,9 +116,14 @@ export class SettingsMainElement extends SettingsMainElementBase { + * current route. + */ + override currentRouteChanged() { +- const inAbout = +- routes.ABOUT.contains(Router.getInstance().getCurrentRoute()); +- this.showPages_ = {about: inAbout, settings: !inAbout}; ++ const currentRoute = Router.getInstance().getCurrentRoute(); ++ const inAbout = routes.ABOUT.contains(currentRoute); ++ const inNxtscape = routes.NXTSCAPE.contains(currentRoute); ++ this.showPages_ = { ++ about: inAbout, ++ settings: !inAbout && !inNxtscape, ++ nxtscape: inNxtscape ++ }; + } + + private onShowingSubpage_() { +diff --git a/chrome/browser/resources/settings/settings_menu/settings_menu.html b/chrome/browser/resources/settings/settings_menu/settings_menu.html +index 79aad7032abc7..8f123acf9b322 100644 +--- a/chrome/browser/resources/settings/settings_menu/settings_menu.html ++++ b/chrome/browser/resources/settings/settings_menu/settings_menu.html +@@ -57,6 +57,12 @@ + $i18n{peoplePageTitle} + + ++ ++ ++ BrowserOS AI ++ ++ +

chrome://browseros-first-run

-+
-+ -+
-+

🚀 Getting Started

-+

-+ -+ 📖 Quick Start Guide - bit.ly/BrowserOS-setup -+ -+

-+
-+ 📥 Import your data from Chrome -+
    -+
  1. Navigate to chrome://settings/importData
  2. -+
  3. Click "Import"
  4. -+
  5. Follow the on-screen prompts and click "Always allow" when prompted to import all your data at once
  6. -+
-+
-+
-+ 🔑 BYOK (Bring Your Own Keys) -+

-+ You have full control over your AI models! Navigate to chrome://settings/browseros to configure your own API keys for various providers. -+

-+

-+ Note: You can even run everything locally using Ollama! 🔒 -+

-+
-+
-+ ⌨️ Keyboard Shortcuts -+

-+ Toggle AI Agent: Press Cmd+E to quickly open or close the AI agent sidebar. 🤖 -+

-+
-+
-+ -+
-+

✨ Key Features

-+
    -+
  • 🤖 BrowserOS Agent: Your productivity agent that can manage your tabs and browsing sessions. For example: -+
      -+
    • "list tabs I have open"
    • -+
    • "close duplicate tabs"
    • -+
    • "group tabs by topic"
    • -+
    • "switch to Bookface tab"
    • -+
    • "save my current browsing session as XYZ-Research"
    • -+
    • "resume XYZ-Research browsing session"
    • -+
    • "search my browser history for all github pages I visited"
    • -+
    • "organize my entire bookmark collection"
    • -+
    -+
  • -+
  • 🧭 BrowserOS Navigator: Performs agentic tasks for you on web pages. For example: -+
      -+
    • Go to amazon.com and search for "hard disk"
    • -+
    • Navigate to specific pages and interact with content
    • -+
    • Automate repetitive browsing tasks
    • -+
    -+
  • -+
-+
-+ -+
-+

🤝 Join Our Community & Explore

-+ -+

Have questions or want to contribute? We'd love to hear from you!

-+
-+ -+ -+ -+)"; -+ std::move(callback).Run(base::MakeRefCounted(std::move(source))); -+} -+ -+class NxtscapeFirstRun; -+class NxtscapeFirstRunUIConfig : public content::DefaultWebUIConfig { -+ public: -+ NxtscapeFirstRunUIConfig() : DefaultWebUIConfig("chrome", "browseros-first-run") {} -+}; -+ -+class NxtscapeFirstRun : public content::WebUIController { -+ public: -+ NxtscapeFirstRun(content::WebUI* web_ui) : content::WebUIController(web_ui) { -+ content::URLDataSource::Add(Profile::FromWebUI(web_ui), std::make_unique()); -+ } -+ NxtscapeFirstRun(const NxtscapeFirstRun&) = delete; -+ NxtscapeFirstRun& operator=(const NxtscapeFirstRun&) = delete; -+}; -+ -+#endif // CHROME_BROWSER_UI_WEBUI_NXTSCAPE_FIRST_RUN_H_ -diff --git a/chrome/common/webui_url_constants.cc b/chrome/common/webui_url_constants.cc -index e5e724a22d015..3627df513cd04 100644 ---- a/chrome/common/webui_url_constants.cc -+++ b/chrome/common/webui_url_constants.cc -@@ -72,6 +72,7 @@ bool IsSystemWebUIHost(std::string_view host) { - // These hosts will also be suggested by BuiltinProvider. - base::span ChromeURLHosts() { - static constexpr auto kChromeURLHosts = std::to_array({ -+ "browseros-first-run", - kChromeUIAboutHost, - kChromeUIAccessibilityHost, - #if !BUILDFLAG(IS_ANDROID) --- -2.49.0 - diff --git a/patches/nxtscape/importer-updates.patch b/patches/nxtscape/importer-updates.patch deleted file mode 100644 index 5d9c29c99..000000000 --- a/patches/nxtscape/importer-updates.patch +++ /dev/null @@ -1,249 +0,0 @@ -From 854e49826e860826723c15c539d72d8c2a7e1b8a Mon Sep 17 00:00:00 2001 -From: Nikhil Sonti -Date: Mon, 28 Jul 2025 12:01:51 -0700 -Subject: [PATCH] remove keychain request - ---- - chrome/utility/importer/chrome_importer.cc | 167 ++------------------- - chrome/utility/importer/chrome_importer.h | 13 +- - 2 files changed, 12 insertions(+), 168 deletions(-) - -diff --git a/chrome/utility/importer/chrome_importer.cc b/chrome/utility/importer/chrome_importer.cc -index 9026dcc1ec6b1..5a7c392fd775a 100644 ---- a/chrome/utility/importer/chrome_importer.cc -+++ b/chrome/utility/importer/chrome_importer.cc -@@ -20,9 +20,6 @@ - #include "chrome/common/importer/importer_url_row.h" - #include "chrome/grit/generated_resources.h" - #include "chrome/utility/importer/favicon_reencode.h" --#include "components/os_crypt/sync/os_crypt.h" --#include "components/password_manager/core/browser/password_form.h" --#include "components/password_manager/core/browser/password_store/login_database.h" - #include "sql/database.h" - #include "sql/statement.h" - #include "ui/base/l10n/l10n_util.h" -@@ -406,123 +403,15 @@ base::Time ChromeImporter::ChromeTimeToBaseTime(int64_t time) { - } - - void ChromeImporter::ImportPasswords() { -- LOG(INFO) << "ChromeImporter: Starting passwords import"; -- -- // Set up encryption keys for decrypting passwords -- base::FilePath source_path = source_path_; --#if BUILDFLAG(IS_WIN) -- // On Windows, the path is different -- source_path = source_path_.DirName(); --#endif -- -- // Initialize encryption using the appropriate source path -- if (!SetEncryptionKey(source_path)) { -- LOG(ERROR) << "ChromeImporter: Failed to set encryption key for passwords"; -- return; -- } -- -- // First try the main Login Data file -- ImportPasswordsFromFile(base::FilePath(FILE_PATH_LITERAL("Login Data"))); -- -- // Then try the account-specific Login Data file if it exists -- ImportPasswordsFromFile(base::FilePath(FILE_PATH_LITERAL("Login Data For Account"))); -- -- LOG(INFO) << "ChromeImporter: Passwords import complete"; -+ // Password import is disabled - users should use CSV import from chrome://password-manager/passwords -+ LOG(INFO) << "ChromeImporter: Password import is disabled. " -+ << "Please use CSV import from chrome://password-manager/passwords"; -+ return; - } - - void ChromeImporter::ImportPasswordsFromFile(const base::FilePath& password_filename) { -- base::FilePath passwords_path = source_path_.Append(password_filename); -- if (!base::PathExists(passwords_path)) { -- LOG(INFO) << "ChromeImporter: " << password_filename.value() << " file not found"; -- return; -- } -- -- // Create temporary directory for copying the database -- base::FilePath temp_directory; -- if (!base::CreateNewTempDirectory(base::FilePath::StringType(), &temp_directory)) { -- LOG(ERROR) << "ChromeImporter: Failed to create temp directory for passwords"; -- return; -- } -- -- // Copy the database file to avoid lock issues -- base::FilePath temp_passwords_path = temp_directory.Append(password_filename.BaseName()); -- if (!base::CopyFile(passwords_path, temp_passwords_path)) { -- LOG(ERROR) << "ChromeImporter: Failed to copy " << password_filename.value() << " file"; -- base::DeletePathRecursively(temp_directory); -- return; -- } -- -- // Open the database using password manager's login database -- password_manager::LoginDatabase database( -- temp_passwords_path, password_manager::IsAccountStore(false)); -- -- if (!database.Init(base::NullCallback(), nullptr)) { -- LOG(ERROR) << "ChromeImporter: Failed to initialize login database"; -- base::DeletePathRecursively(temp_directory); -- return; -- } -- -- // Process regular logins -- std::vector forms; -- bool success = database.GetAutofillableLogins(&forms); -- if (success) { -- LOG(INFO) << "ChromeImporter: Found " << forms.size() << " passwords"; -- for (const auto& form : forms) { -- importer::ImportedPasswordForm imported_form; -- if (PasswordFormToImportedPasswordForm(form, imported_form)) { -- bridge_->SetPasswordForm(imported_form); -- } -- } -- } -- -- // Process blocklisted logins -- std::vector blocklist; -- success = database.GetBlocklistLogins(&blocklist); -- if (success && !blocklist.empty()) { -- LOG(INFO) << "ChromeImporter: Found " << blocklist.size() << " blocklisted passwords"; -- for (const auto& form : blocklist) { -- importer::ImportedPasswordForm imported_form; -- if (PasswordFormToImportedPasswordForm(form, imported_form)) { -- bridge_->SetPasswordForm(imported_form); -- } -- } -- } -- -- // Clean up temporary files -- base::DeletePathRecursively(temp_directory); --} -- --bool ChromeImporter::PasswordFormToImportedPasswordForm( -- const password_manager::PasswordForm& form, -- importer::ImportedPasswordForm& imported_form) { -- // Skip forms with invalid schemes -- if (form.scheme != password_manager::PasswordForm::Scheme::kHtml && -- form.scheme != password_manager::PasswordForm::Scheme::kBasic) { -- return false; -- } -- -- // Set the scheme appropriately -- imported_form.scheme = form.scheme == password_manager::PasswordForm::Scheme::kHtml -- ? importer::ImportedPasswordForm::Scheme::kHtml -- : importer::ImportedPasswordForm::Scheme::kBasic; -- -- // Skip inconsistent blocked forms that have credentials -- if (form.blocked_by_user && -- (!form.username_value.empty() || !form.password_value.empty())) { -- return false; -- } -- -- // Copy over all the relevant form fields -- imported_form.signon_realm = form.signon_realm; -- imported_form.url = form.url; -- imported_form.action = form.action; -- imported_form.username_element = form.username_element; -- imported_form.username_value = form.username_value; -- imported_form.password_element = form.password_element; -- imported_form.password_value = form.password_value; -- imported_form.blocked_by_user = form.blocked_by_user; -- -- return true; -+ // Password import is disabled - this function is kept as a no-op for compatibility -+ return; - } - - void ChromeImporter::ImportAutofillFormData() { -@@ -592,44 +481,10 @@ void ChromeImporter::ImportAutofillFormData() { - LOG(INFO) << "ChromeImporter: Autofill form data import complete"; - } - --bool ChromeImporter::SetEncryptionKey(const base::FilePath& source_path) { --#if BUILDFLAG(IS_LINUX) -- // Set up crypt config for Linux -- std::unique_ptr config(new os_crypt::Config()); -- config->product_name = l10n_util::GetStringUTF8(IDS_PRODUCT_NAME); -- config->should_use_preference = false; -- config->user_data_path = source_path; -- OSCrypt::SetConfig(std::move(config)); -- return true; --#elif BUILDFLAG(IS_WIN) -- // On Windows, we need to obtain the encryption key from Local State -- base::FilePath local_state_path = source_path.Append(FILE_PATH_LITERAL("Local State")); -- if (!base::PathExists(local_state_path)) { -- LOG(ERROR) << "ChromeImporter: Local State file not found"; -- return false; -- } -- -- std::string local_state_content; -- if (!base::ReadFileToString(local_state_path, &local_state_content)) { -- LOG(ERROR) << "ChromeImporter: Failed to read Local State file"; -- return false; -- } -- -- std::optional local_state = -- base::JSONReader::ReadDict(local_state_content); -- if (!local_state) { -- LOG(ERROR) << "ChromeImporter: Failed to parse Local State JSON"; -- return false; -- } -- -- // For Mac and other platforms, we don't need to do anything special -- // as keychain is used automatically -- return true; --#else -- // For Mac, keychain is used automatically by OSCrypt -- return true; --#endif --} -+// Encryption key setup is disabled since password import is disabled -+// bool ChromeImporter::SetEncryptionKey(const base::FilePath& source_path) { -+// return false; -+// } - - void ChromeImporter::ImportExtensions() { - LOG(INFO) << "ChromeImporter: Starting extensions import"; -@@ -733,4 +588,4 @@ std::vector ChromeImporter::GetExtensionsFromPreferencesFile( - } - - return extension_ids; --} -\ No newline at end of file -+} -diff --git a/chrome/utility/importer/chrome_importer.h b/chrome/utility/importer/chrome_importer.h -index 06317c522d5d0..25b49c7028e1c 100644 ---- a/chrome/utility/importer/chrome_importer.h -+++ b/chrome/utility/importer/chrome_importer.h -@@ -25,13 +25,6 @@ namespace sql { - class Database; - } - --namespace password_manager { --struct PasswordForm; --} -- --namespace importer { --struct ImportedPasswordForm; --} - - class ChromeImporter : public Importer { - public: -@@ -53,10 +46,6 @@ class ChromeImporter : public Importer { - void ImportAutofillFormData(); - void ImportExtensions(); - void ImportPasswordsFromFile(const base::FilePath& password_filename); -- bool PasswordFormToImportedPasswordForm( -- const password_manager::PasswordForm& form, -- importer::ImportedPasswordForm& imported_form); -- bool SetEncryptionKey(const base::FilePath& source_path); - - // Helper function to convert Chrome's time format to base::Time - base::Time ChromeTimeToBaseTime(int64_t time); -@@ -88,4 +77,4 @@ class ChromeImporter : public Importer { - base::FilePath source_path_; - }; - --#endif // CHROME_UTILITY_IMPORTER_CHROME_IMPORTER_H_ -\ No newline at end of file -+#endif // CHROME_UTILITY_IMPORTER_CHROME_IMPORTER_H_ --- -2.49.0 - diff --git a/patches/nxtscape/new-snapshot-API.patch b/patches/nxtscape/new-snapshot-API.patch deleted file mode 100644 index 5051fd64e..000000000 --- a/patches/nxtscape/new-snapshot-API.patch +++ /dev/null @@ -1,1221 +0,0 @@ -From b374e4a9168438e861e3e5aba8eb88d24a8db676 Mon Sep 17 00:00:00 2001 -From: Nikhil Sonti -Date: Thu, 17 Jul 2025 13:25:19 -0700 -Subject: [PATCH 2/2] New content API - ---- - chrome/browser/extensions/BUILD.gn | 2 + - .../api/browser_os/browser_os_api.cc | 91 +++ - .../api/browser_os/browser_os_api.h | 19 + - .../browser_os_content_processor.cc | 727 ++++++++++++++++++ - .../browser_os/browser_os_content_processor.h | 173 +++++ - .../browser_os_snapshot_processor.cc | 1 + - chrome/common/extensions/api/browser_os.idl | 86 +++ - .../extension_function_histogram_value.h | 1 + - 8 files changed, 1100 insertions(+) - create mode 100644 chrome/browser/extensions/api/browser_os/browser_os_content_processor.cc - create mode 100644 chrome/browser/extensions/api/browser_os/browser_os_content_processor.h - -diff --git a/chrome/browser/extensions/BUILD.gn b/chrome/browser/extensions/BUILD.gn -index bbb5a16ccd5d0..7b601eb14acab 100644 ---- a/chrome/browser/extensions/BUILD.gn -+++ b/chrome/browser/extensions/BUILD.gn -@@ -522,6 +522,8 @@ source_set("extensions") { - "api/browser_os/browser_os_api_helpers.h", - "api/browser_os/browser_os_api_utils.cc", - "api/browser_os/browser_os_api_utils.h", -+ "api/browser_os/browser_os_content_processor.cc", -+ "api/browser_os/browser_os_content_processor.h", - "api/browser_os/browser_os_snapshot_processor.cc", - "api/browser_os/browser_os_snapshot_processor.h", - "api/chrome_device_permissions_prompt.h", -diff --git a/chrome/browser/extensions/api/browser_os/browser_os_api.cc b/chrome/browser/extensions/api/browser_os/browser_os_api.cc -index c39e67d88e1de..6bee5ce12bb9e 100644 ---- a/chrome/browser/extensions/api/browser_os/browser_os_api.cc -+++ b/chrome/browser/extensions/api/browser_os/browser_os_api.cc -@@ -18,6 +18,7 @@ - #include "base/values.h" - #include "chrome/browser/extensions/api/browser_os/browser_os_api_helpers.h" - #include "chrome/browser/extensions/api/browser_os/browser_os_api_utils.h" -+#include "chrome/browser/extensions/api/browser_os/browser_os_content_processor.h" - #include "chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.h" - #include "chrome/browser/extensions/extension_tab_util.h" - #include "chrome/browser/extensions/window_controller.h" -@@ -756,5 +757,95 @@ void BrowserOSCaptureScreenshotFunction::OnScreenshotCaptured( - browser_os::CaptureScreenshot::Results::Create(data_url))); - } - -+// BrowserOSGetSnapshotFunction implementation -+ExtensionFunction::ResponseAction BrowserOSGetSnapshotFunction::Run() { -+ auto params = browser_os::GetSnapshot::Params::Create(args()); -+ EXTENSION_FUNCTION_VALIDATE(params); -+ -+ // Get the target tab -+ std::string error_message; -+ auto tab_info = GetTabFromOptionalId(params->tab_id, browser_context(), -+ include_incognito_information(), -+ &error_message); -+ if (!tab_info) { -+ return RespondNow(Error(error_message)); -+ } -+ -+ content::WebContents* web_contents = tab_info->web_contents; -+ -+ // Request accessibility tree snapshot -+ web_contents->RequestAXTreeSnapshot( -+ base::BindOnce(&BrowserOSGetSnapshotFunction::OnAccessibilityTreeReceived, -+ this), -+ ui::AXMode(ui::AXMode::kWebContents | ui::AXMode::kExtendedProperties), -+ /* max_nodes= */ 0, // No limit -+ /* timeout= */ base::TimeDelta(), -+ content::WebContents::AXTreeSnapshotPolicy::kAll); -+ -+ return RespondLater(); -+} -+ -+void BrowserOSGetSnapshotFunction::OnAccessibilityTreeReceived( -+ ui::AXTreeUpdate& tree_update) { -+ if (!has_callback()) { -+ return; -+ } -+ -+ // Get parameters again -+ auto params = browser_os::GetSnapshot::Params::Create(args()); -+ if (!params) { -+ Respond(Error("Invalid parameters")); -+ return; -+ } -+ -+ // Get tab info again for viewport size -+ std::string error_message; -+ auto tab_info = GetTabFromOptionalId(params->tab_id, browser_context(), -+ include_incognito_information(), -+ &error_message); -+ if (!tab_info) { -+ Respond(Error(error_message)); -+ return; -+ } -+ -+ // Get viewport size -+ gfx::Size viewport_size; -+ content::WebContents* web_contents = tab_info->web_contents; -+ content::RenderWidgetHostView* rwhv = web_contents->GetRenderWidgetHostView(); -+ if (rwhv) { -+ viewport_size = rwhv->GetVisibleViewportSize(); -+ } -+ -+ // Extract options -+ browser_os::SnapshotContext context = browser_os::SnapshotContext::kVisible; -+ std::vector include_sections; -+ -+ if (params->options) { -+ context = params->options->context; -+ if (params->options->include_sections.has_value()) { -+ include_sections = params->options->include_sections.value(); -+ } -+ } -+ -+ // Process the accessibility tree -+ ContentProcessor::ProcessAccessibilityTree( -+ tree_update, -+ params->type, -+ context, -+ include_sections, -+ viewport_size, -+ base::BindOnce(&BrowserOSGetSnapshotFunction::OnContentProcessed, this)); -+} -+ -+void BrowserOSGetSnapshotFunction::OnContentProcessed( -+ api::ContentProcessingResult result) { -+ if (!has_callback()) { -+ return; -+ } -+ -+ Respond(ArgumentList( -+ browser_os::GetSnapshot::Results::Create(result.snapshot))); -+} -+ - } // namespace api - } // namespace extensions -diff --git a/chrome/browser/extensions/api/browser_os/browser_os_api.h b/chrome/browser/extensions/api/browser_os/browser_os_api.h -index 58acc663f0170..6090d2fbeb6a4 100644 ---- a/chrome/browser/extensions/api/browser_os/browser_os_api.h -+++ b/chrome/browser/extensions/api/browser_os/browser_os_api.h -@@ -9,6 +9,7 @@ - - #include "base/values.h" - #include "chrome/browser/extensions/api/browser_os/browser_os_api_utils.h" -+#include "chrome/browser/extensions/api/browser_os/browser_os_content_processor.h" - #include "chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.h" - #include "extensions/browser/extension_function.h" - #include "third_party/skia/include/core/SkBitmap.h" -@@ -190,6 +191,24 @@ class BrowserOSCaptureScreenshotFunction : public ExtensionFunction { - void OnScreenshotCaptured(const SkBitmap& bitmap); - }; - -+class BrowserOSGetSnapshotFunction : public ExtensionFunction { -+ public: -+ DECLARE_EXTENSION_FUNCTION("browserOS.getSnapshot", BROWSER_OS_GETSNAPSHOT) -+ -+ BrowserOSGetSnapshotFunction() = default; -+ -+ protected: -+ ~BrowserOSGetSnapshotFunction() override = default; -+ -+ // ExtensionFunction: -+ ResponseAction Run() override; -+ -+ private: -+ void OnAccessibilityTreeReceived(ui::AXTreeUpdate& tree_update); -+ void OnContentProcessed( -+ api::ContentProcessingResult result); -+}; -+ - } // namespace api - } // namespace extensions - -diff --git a/chrome/browser/extensions/api/browser_os/browser_os_content_processor.cc b/chrome/browser/extensions/api/browser_os/browser_os_content_processor.cc -new file mode 100644 -index 0000000000000..7a35c0fea9de8 ---- /dev/null -+++ b/chrome/browser/extensions/api/browser_os/browser_os_content_processor.cc -@@ -0,0 +1,727 @@ -+// Copyright 2024 The Chromium Authors -+// Use of this source code is governed by a BSD-style license that can be -+// found in the LICENSE file. -+ -+#include "chrome/browser/extensions/api/browser_os/browser_os_content_processor.h" -+ -+#include -+#include -+#include -+ -+#include "base/functional/bind.h" -+#include "base/logging.h" -+#include "base/strings/string_util.h" -+#include "base/task/thread_pool.h" -+#include "base/time/time.h" -+#include "ui/accessibility/ax_enum_util.h" -+#include "ui/accessibility/ax_node_data.h" -+#include "ui/accessibility/ax_role_properties.h" -+#include "ui/accessibility/ax_tree_update.h" -+#include "ui/gfx/geometry/rect_conversions.h" -+ -+namespace extensions { -+namespace api { -+ -+namespace { -+ -+// Constants for safety limits -+constexpr size_t kMaxLinksPerSection = 1000; -+constexpr size_t kMaxTextLength = 100000; -+ -+// Helper to clean text for output -+std::string CleanTextForOutput(const std::string& text) { -+ std::string cleaned = std::string(base::TrimWhitespaceASCII(text, base::TRIM_ALL)); -+ -+ // Replace multiple spaces with single space -+ std::string result; -+ bool prev_space = false; -+ for (char c : cleaned) { -+ if (std::isspace(c)) { -+ if (!prev_space) { -+ result += ' '; -+ prev_space = true; -+ } -+ } else { -+ result += c; -+ prev_space = false; -+ } -+ } -+ -+ return result; -+} -+ -+// Helper to determine if URL is external -+bool IsExternalUrl(const std::string& url) { -+ if (url.empty()) return false; -+ -+ // Check for common external URL patterns -+ return url.find("http://") == 0 || -+ url.find("https://") == 0 || -+ url.find("//") == 0; -+} -+ -+// Convert SectionType enum to string -+std::string SectionTypeToString(browser_os::SectionType type) { -+ switch (type) { -+ case browser_os::SectionType::kMain: -+ return "main"; -+ case browser_os::SectionType::kNavigation: -+ return "navigation"; -+ case browser_os::SectionType::kFooter: -+ return "footer"; -+ case browser_os::SectionType::kHeader: -+ return "header"; -+ case browser_os::SectionType::kArticle: -+ return "article"; -+ case browser_os::SectionType::kAside: -+ return "aside"; -+ case browser_os::SectionType::kComplementary: -+ return "complementary"; -+ case browser_os::SectionType::kContentinfo: -+ return "contentinfo"; -+ case browser_os::SectionType::kForm: -+ return "form"; -+ case browser_os::SectionType::kSearch: -+ return "search"; -+ case browser_os::SectionType::kRegion: -+ return "region"; -+ case browser_os::SectionType::kOther: -+ default: -+ return "other"; -+ } -+} -+ -+} // namespace -+ -+// NodeInfo implementation -+ContentProcessor::NodeInfo::NodeInfo() = default; -+ContentProcessor::NodeInfo::NodeInfo(const NodeInfo&) = default; -+ContentProcessor::NodeInfo::NodeInfo(NodeInfo&&) = default; -+ContentProcessor::NodeInfo& ContentProcessor::NodeInfo::operator=(const NodeInfo&) = default; -+ContentProcessor::NodeInfo& ContentProcessor::NodeInfo::operator=(NodeInfo&&) = default; -+ContentProcessor::NodeInfo::~NodeInfo() = default; -+ -+// SectionInfo implementation -+ContentProcessor::SectionInfo::SectionInfo() = default; -+ContentProcessor::SectionInfo::SectionInfo(SectionInfo&&) = default; -+ContentProcessor::SectionInfo& ContentProcessor::SectionInfo::operator=(SectionInfo&&) = default; -+ContentProcessor::SectionInfo::~SectionInfo() = default; -+ -+// ProcessingContext implementation -+ContentProcessor::ProcessingContext::ProcessingContext() = default; -+ContentProcessor::ProcessingContext::~ProcessingContext() = default; -+ -+// ============================================================================ -+// Section Detection and Caching Implementation -+// ============================================================================ -+ -+// Get section type from node attributes (for section roots) -+browser_os::SectionType ContentProcessor::GetSectionTypeFromNode( -+ const ui::AXNodeData& node) { -+ // Check ARIA landmark roles -+ const std::string& role = ui::ToString(node.role); -+ if (role == "navigation") { -+ return browser_os::SectionType::kNavigation; -+ } else if (role == "main") { -+ return browser_os::SectionType::kMain; -+ } else if (role == "complementary" || role == "aside") { -+ return browser_os::SectionType::kAside; -+ } else if (role == "contentinfo" || role == "footer") { -+ return browser_os::SectionType::kFooter; -+ } else if (role == "banner" || role == "header") { -+ return browser_os::SectionType::kHeader; -+ } else if (role == "article") { -+ return browser_os::SectionType::kArticle; -+ } else if (role == "form") { -+ return browser_os::SectionType::kForm; -+ } else if (role == "search") { -+ return browser_os::SectionType::kSearch; -+ } else if (role == "region") { -+ return browser_os::SectionType::kRegion; -+ } -+ -+ // Check HTML tags -+ if (node.HasStringAttribute(ax::mojom::StringAttribute::kHtmlTag)) { -+ const std::string& tag = node.GetStringAttribute(ax::mojom::StringAttribute::kHtmlTag); -+ if (tag == "nav") { -+ return browser_os::SectionType::kNavigation; -+ } else if (tag == "main") { -+ return browser_os::SectionType::kMain; -+ } else if (tag == "aside") { -+ return browser_os::SectionType::kAside; -+ } else if (tag == "footer") { -+ return browser_os::SectionType::kFooter; -+ } else if (tag == "header") { -+ return browser_os::SectionType::kHeader; -+ } else if (tag == "article") { -+ return browser_os::SectionType::kArticle; -+ } else if (tag == "form") { -+ return browser_os::SectionType::kForm; -+ } -+ } -+ -+ return browser_os::SectionType::kNone; // Not a section root -+} -+ -+// Cache a node's section for fast lookup -+void ContentProcessor::CacheNodeSection( -+ int32_t node_id, -+ browser_os::SectionType section_type, -+ scoped_refptr context) { -+ base::AutoLock lock(context->section_cache_lock); -+ context->node_to_section_cache[node_id] = section_type; -+} -+ -+// Determine which section a node belongs to with caching -+browser_os::SectionType ContentProcessor::DetermineNodeSection( -+ int32_t node_id, -+ const std::unordered_map& node_map, -+ scoped_refptr context) { -+ -+ // Fast path: check cache first -+ { -+ base::AutoLock lock(context->section_cache_lock); -+ auto cached_it = context->node_to_section_cache.find(node_id); -+ if (cached_it != context->node_to_section_cache.end()) { -+ return cached_it->second; -+ } -+ } -+ -+ // Find the node -+ auto node_it = node_map.find(node_id); -+ if (node_it == node_map.end()) { -+ return browser_os::SectionType::kOther; -+ } -+ -+ // Check if this node itself defines a section -+ browser_os::SectionType node_section = GetSectionTypeFromNode(node_it->second); -+ if (node_section != browser_os::SectionType::kNone) { -+ // This is a section root - cache it -+ CacheNodeSection(node_id, node_section, context); -+ { -+ base::AutoLock lock(context->section_cache_lock); -+ context->section_root_nodes[node_id] = node_section; -+ } -+ return node_section; -+ } -+ -+ // Walk up the tree to find section -+ std::vector path; -+ path.reserve(20); // Pre-allocate for typical depth -+ -+ int32_t current_id = node_id; -+ const int kMaxDepth = 100; -+ int depth = 0; -+ -+ while (current_id >= 0 && depth < kMaxDepth) { -+ path.push_back(current_id); -+ -+ // Check cache during walk -+ { -+ base::AutoLock lock(context->section_cache_lock); -+ auto cached_it = context->node_to_section_cache.find(current_id); -+ if (cached_it != context->node_to_section_cache.end()) { -+ // Found cached ancestor - cache entire path -+ browser_os::SectionType section = cached_it->second; -+ for (int32_t path_node_id : path) { -+ context->node_to_section_cache[path_node_id] = section; -+ } -+ return section; -+ } -+ -+ // Check if this is a known section root -+ auto root_it = context->section_root_nodes.find(current_id); -+ if (root_it != context->section_root_nodes.end()) { -+ // Found section root - cache entire path -+ browser_os::SectionType section = root_it->second; -+ for (int32_t path_node_id : path) { -+ context->node_to_section_cache[path_node_id] = section; -+ } -+ return section; -+ } -+ } -+ -+ // Move to parent -+ auto current_it = node_map.find(current_id); -+ if (current_it == node_map.end()) { -+ break; -+ } -+ -+ current_id = current_it->second.relative_bounds.offset_container_id; -+ depth++; -+ } -+ -+ // Default to "other" section and cache the path -+ browser_os::SectionType default_section = browser_os::SectionType::kOther; -+ { -+ base::AutoLock lock(context->section_cache_lock); -+ for (int32_t path_node_id : path) { -+ context->node_to_section_cache[path_node_id] = default_section; -+ } -+ } -+ -+ return default_section; -+} -+ -+// Helper to get section type from node -+browser_os::SectionType ContentProcessor::GetSectionType(const NodeInfo& node) { -+ // Check ARIA landmark roles -+ if (node.role == "navigation") { -+ return browser_os::SectionType::kNavigation; -+ } else if (node.role == "main") { -+ return browser_os::SectionType::kMain; -+ } else if (node.role == "complementary" || node.role == "aside") { -+ return browser_os::SectionType::kAside; -+ } else if (node.role == "contentinfo" || node.role == "footer") { -+ return browser_os::SectionType::kFooter; -+ } else if (node.role == "banner" || node.role == "header") { -+ return browser_os::SectionType::kHeader; -+ } else if (node.role == "article") { -+ return browser_os::SectionType::kArticle; -+ } else if (node.role == "form") { -+ return browser_os::SectionType::kForm; -+ } else if (node.role == "search") { -+ return browser_os::SectionType::kSearch; -+ } else if (node.role == "region") { -+ return browser_os::SectionType::kRegion; -+ } -+ -+ // Check HTML tags from attributes -+ auto tag_it = node.attributes.find("html-tag"); -+ if (tag_it != node.attributes.end()) { -+ const std::string& tag = tag_it->second; -+ if (tag == "nav") { -+ return browser_os::SectionType::kNavigation; -+ } else if (tag == "main") { -+ return browser_os::SectionType::kMain; -+ } else if (tag == "aside") { -+ return browser_os::SectionType::kAside; -+ } else if (tag == "footer") { -+ return browser_os::SectionType::kFooter; -+ } else if (tag == "header") { -+ return browser_os::SectionType::kHeader; -+ } else if (tag == "article") { -+ return browser_os::SectionType::kArticle; -+ } else if (tag == "form") { -+ return browser_os::SectionType::kForm; -+ } -+ } -+ -+ return browser_os::SectionType::kOther; -+} -+ -+// ============================================================================ -+// Thread-Safe Section Content Management -+// ============================================================================ -+ -+// Add text content to a section (thread-safe) -+void ContentProcessor::AddTextToSection( -+ browser_os::SectionType section_type, -+ const std::string& text, -+ scoped_refptr context) { -+ -+ if (text.empty()) { -+ return; -+ } -+ -+ base::AutoLock lock(context->sections_lock); -+ -+ // Get or create section -+ auto& section_ptr = context->sections[section_type]; -+ if (!section_ptr) { -+ section_ptr = std::make_unique(); -+ section_ptr->type = section_type; -+ } -+ -+ // Add text with newline separator if needed -+ if (!section_ptr->text_content.empty()) { -+ section_ptr->text_content += "\n"; -+ } -+ section_ptr->text_content += text; -+ -+ // Enforce size limit -+ if (section_ptr->text_content.length() > kMaxTextLength) { -+ section_ptr->text_content.resize(kMaxTextLength); -+ } -+} -+ -+// Add link to a section (thread-safe) -+void ContentProcessor::AddLinkToSection( -+ browser_os::SectionType section_type, -+ browser_os::LinkInfo link, -+ scoped_refptr context) { -+ -+ base::AutoLock lock(context->sections_lock); -+ -+ // Get or create section -+ auto& section_ptr = context->sections[section_type]; -+ if (!section_ptr) { -+ section_ptr = std::make_unique(); -+ section_ptr->type = section_type; -+ } -+ -+ // Add link with limit check -+ if (section_ptr->links.size() < kMaxLinksPerSection) { -+ section_ptr->links.push_back(std::move(link)); -+ } -+} -+ -+// Helper to check if node is visible -+bool ContentProcessor::IsNodeVisible(const NodeInfo& node, const gfx::Rect& viewport_bounds) { -+ if (viewport_bounds.IsEmpty()) { -+ return true; // No viewport restriction -+ } -+ -+ // Check if node bounds intersect with viewport -+ return viewport_bounds.Intersects(node.bounds); -+} -+ -+// Helper to extract text from node -+std::string ContentProcessor::ExtractNodeText(const NodeInfo& node) { -+ std::vector text_parts; -+ -+ // Get name -+ if (!node.name.empty()) { -+ text_parts.push_back(node.name); -+ } -+ -+ // Get value for input elements -+ if (!node.value.empty()) { -+ text_parts.push_back(node.value); -+ } -+ -+ // Get placeholder -+ auto placeholder_it = node.attributes.find("placeholder"); -+ if (placeholder_it != node.attributes.end() && !placeholder_it->second.empty()) { -+ text_parts.push_back(placeholder_it->second); -+ } -+ -+ // Join all text parts -+ std::string result = base::JoinString(text_parts, " "); -+ return CleanTextForOutput(result); -+} -+ -+// Helper to extract link info -+browser_os::LinkInfo ContentProcessor::ExtractLinkInfo(const NodeInfo& node) { -+ browser_os::LinkInfo link; -+ -+ // Get URL -+ link.url = node.url; -+ -+ // Get link text (name or inner text) -+ link.text = node.name; -+ -+ // Get title attribute -+ auto title_it = node.attributes.find("title"); -+ if (title_it != node.attributes.end()) { -+ link.title = title_it->second; -+ } -+ -+ // Determine if external -+ link.is_external = IsExternalUrl(link.url); -+ -+ // Add additional attributes -+ browser_os::LinkInfo::Attributes attrs; -+ attrs.additional_properties.Set("role", node.role); -+ if (node.attributes.find("html-tag") != node.attributes.end()) { -+ attrs.additional_properties.Set("tag", node.attributes.at("html-tag")); -+ } -+ link.attributes = std::move(attrs); -+ -+ return link; -+} -+ -+// Helper to check if node is a link -+bool ContentProcessor::IsLink(const NodeInfo& node) { -+ return (node.role == "link" || !node.url.empty()) && -+ node.url != "#"; // Skip empty fragment links -+} -+ -+// Helper to check if node has text content -+bool ContentProcessor::IsTextNode(const NodeInfo& node) { -+ // Include nodes with text content -+ return !node.name.empty() || !node.value.empty() || -+ node.attributes.find("placeholder") != node.attributes.end(); -+} -+ -+ -+// ============================================================================ -+// Parallel Batch Processing with Integrated Section Detection -+// ============================================================================ -+ -+// Process a batch of nodes in parallel with section detection -+void ContentProcessor::ProcessNodeBatchParallel( -+ const std::vector& batch, -+ scoped_refptr context) { -+ -+ // Process each node in the batch -+ for (const auto& ax_node : batch) { -+ // Skip invisible or ignored nodes -+ if (ax_node.IsInvisibleOrIgnored()) { -+ continue; -+ } -+ -+ // Skip if visibility filtering is enabled and node is not visible -+ if (context->snapshot_context == browser_os::SnapshotContext::kVisible) { -+ gfx::Rect viewport_bounds(context->viewport_size); -+ gfx::Rect node_bounds = gfx::ToEnclosingRect(ax_node.relative_bounds.bounds); -+ if (!viewport_bounds.IsEmpty() && !viewport_bounds.Intersects(node_bounds)) { -+ continue; -+ } -+ } -+ -+ // Determine which section this node belongs to -+ browser_os::SectionType section_type = DetermineNodeSection( -+ ax_node.id, context->node_map, context); -+ -+ // Check if we should include this section -+ if (!context->include_sections.empty()) { -+ bool should_include = false; -+ for (const auto& included : context->include_sections) { -+ if (included == section_type) { -+ should_include = true; -+ break; -+ } -+ } -+ if (!should_include) { -+ continue; -+ } -+ } -+ -+ // Process based on snapshot type -+ if (context->snapshot_type == browser_os::SnapshotType::kText) { -+ // Extract text content -+ std::string text = ExtractTextFromAXNode(ax_node); -+ if (!text.empty()) { -+ AddTextToSection(section_type, text, context); -+ } -+ } else if (context->snapshot_type == browser_os::SnapshotType::kLinks) { -+ // Check if this is a link -+ if (IsLinkNode(ax_node)) { -+ browser_os::LinkInfo link = ExtractLinkFromAXNode(ax_node); -+ // Only add links that have a non-empty URL -+ if (!link.url.empty()) { -+ AddLinkToSection(section_type, std::move(link), context); -+ } -+ } -+ } -+ } -+} -+ -+// Helper to extract text from AXNodeData -+std::string ContentProcessor::ExtractTextFromAXNode(const ui::AXNodeData& node) { -+ std::vector text_parts; -+ -+ // Get name -+ if (node.HasStringAttribute(ax::mojom::StringAttribute::kName)) { -+ text_parts.push_back(node.GetStringAttribute(ax::mojom::StringAttribute::kName)); -+ } -+ -+ // Get value for input elements -+ if (node.HasStringAttribute(ax::mojom::StringAttribute::kValue)) { -+ text_parts.push_back(node.GetStringAttribute(ax::mojom::StringAttribute::kValue)); -+ } -+ -+ // Get placeholder -+ if (node.HasStringAttribute(ax::mojom::StringAttribute::kPlaceholder)) { -+ text_parts.push_back(node.GetStringAttribute(ax::mojom::StringAttribute::kPlaceholder)); -+ } -+ -+ // Join all text parts -+ std::string result = base::JoinString(text_parts, " "); -+ return CleanTextForOutput(result); -+} -+ -+// Helper to check if node is a link -+bool ContentProcessor::IsLinkNode(const ui::AXNodeData& node) { -+ // Use the official IsLink function from ax_role_properties -+ if (!ui::IsLink(node.role)) { -+ return false; -+ } -+ -+ // Also check for valid URL (skip empty fragment links) -+ if (node.HasStringAttribute(ax::mojom::StringAttribute::kUrl)) { -+ const std::string& url = node.GetStringAttribute(ax::mojom::StringAttribute::kUrl); -+ return !url.empty() && url != "#"; -+ } -+ -+ // Link role without URL is still a valid link (might have onclick handler) -+ return true; -+} -+ -+// Helper to extract link info from AXNodeData -+browser_os::LinkInfo ContentProcessor::ExtractLinkFromAXNode(const ui::AXNodeData& node) { -+ browser_os::LinkInfo link; -+ -+ // Get URL -+ if (node.HasStringAttribute(ax::mojom::StringAttribute::kUrl)) { -+ link.url = node.GetStringAttribute(ax::mojom::StringAttribute::kUrl); -+ } -+ -+ // Get link text -+ if (node.HasStringAttribute(ax::mojom::StringAttribute::kName)) { -+ link.text = node.GetStringAttribute(ax::mojom::StringAttribute::kName); -+ } -+ -+ // Get title attribute -+ if (node.HasStringAttribute(ax::mojom::StringAttribute::kTooltip)) { -+ link.title = node.GetStringAttribute(ax::mojom::StringAttribute::kTooltip); -+ } -+ -+ // Determine if external -+ link.is_external = IsExternalUrl(link.url); -+ -+ // Add additional attributes -+ browser_os::LinkInfo::Attributes attrs; -+ attrs.additional_properties.Set("role", ui::ToString(node.role)); -+ if (node.HasStringAttribute(ax::mojom::StringAttribute::kHtmlTag)) { -+ attrs.additional_properties.Set("tag", -+ node.GetStringAttribute(ax::mojom::StringAttribute::kHtmlTag)); -+ } -+ link.attributes = std::move(attrs); -+ -+ return link; -+} -+ -+ -+// Callback when batch is processed -+void ContentProcessor::OnBatchProcessed( -+ scoped_refptr context) { -+ -+ // Decrement pending batches atomically -+ int remaining = context->pending_batches.fetch_sub(1) - 1; -+ -+ // Check if all batches are complete -+ if (remaining == 0) { -+ OnAllBatchesComplete(context); -+ } -+} -+ -+// Called when all batches are complete -+void ContentProcessor::OnAllBatchesComplete(scoped_refptr context) { -+ // All processing is already done in parallel batches! -+ // Just need to convert sections to API format -+ -+ // Build result snapshot -+ browser_os::Snapshot snapshot; -+ snapshot.type = context->snapshot_type; -+ snapshot.context = context->snapshot_context; -+ snapshot.timestamp = base::Time::Now().InMillisecondsFSinceUnixEpoch(); -+ -+ // Convert sections to API format -+ { -+ base::AutoLock lock(context->sections_lock); -+ for (const auto& [section_type, section_ptr] : context->sections) { -+ if (!section_ptr) continue; -+ -+ browser_os::SnapshotSection api_section; -+ api_section.type = SectionTypeToString(section_type); -+ -+ // Always create both results (one will be empty) -+ browser_os::TextSnapshotResult text_result; -+ browser_os::LinksSnapshotResult links_result; -+ -+ // Populate based on type -+ if (context->snapshot_type == browser_os::SnapshotType::kText) { -+ text_result.text = std::move(section_ptr->text_content); -+ text_result.character_count = text_result.text.length(); -+ } else if (context->snapshot_type == browser_os::SnapshotType::kLinks) { -+ links_result.links = std::move(section_ptr->links); -+ } -+ -+ api_section.text_result = std::move(text_result); -+ api_section.links_result = std::move(links_result); -+ -+ snapshot.sections.push_back(std::move(api_section)); -+ } -+ } -+ -+ // Calculate processing time -+ base::TimeDelta processing_time = base::Time::Now() - context->start_time; -+ snapshot.processing_time_ms = processing_time.InMilliseconds(); -+ -+ LOG(INFO) << "[PERF] Content snapshot processed in " -+ << processing_time.InMilliseconds() << " ms" -+ << " (sections: " << snapshot.sections.size() << ")"; -+ -+ // Create result -+ ContentProcessingResult result; -+ result.snapshot = std::move(snapshot); -+ result.nodes_processed = context->node_map.size(); -+ result.processing_time_ms = processing_time.InMilliseconds(); -+ -+ // Run callback -+ std::move(context->callback).Run(std::move(result)); -+} -+ -+// Main processing function -+void ContentProcessor::ProcessAccessibilityTree( -+ const ui::AXTreeUpdate& tree_update, -+ browser_os::SnapshotType type, -+ browser_os::SnapshotContext context, -+ const std::vector& include_sections, -+ const gfx::Size& viewport_size, -+ base::OnceCallback callback) { -+ -+ // Create processing context -+ auto processing_context = base::MakeRefCounted(); -+ processing_context->tree_update = tree_update; -+ processing_context->snapshot_type = type; -+ processing_context->snapshot_context = context; -+ processing_context->include_sections = include_sections; -+ processing_context->viewport_size = viewport_size; -+ processing_context->callback = std::move(callback); -+ processing_context->start_time = base::Time::Now(); -+ -+ // Build node map upfront (read-only after this) -+ for (const auto& node : tree_update.nodes) { -+ processing_context->node_map[node.id] = node; -+ } -+ -+ // Pre-identify section roots for faster lookup -+ for (const auto& node : tree_update.nodes) { -+ browser_os::SectionType section_type = GetSectionTypeFromNode(node); -+ if (section_type != browser_os::SectionType::kNone) { -+ base::AutoLock lock(processing_context->section_cache_lock); -+ processing_context->section_root_nodes[node.id] = section_type; -+ processing_context->node_to_section_cache[node.id] = section_type; -+ } -+ } -+ -+ // Handle empty case -+ if (tree_update.nodes.empty()) { -+ ContentProcessingResult result; -+ result.snapshot.type = type; -+ result.snapshot.context = context; -+ result.snapshot.timestamp = base::Time::Now().InMillisecondsFSinceUnixEpoch(); -+ result.snapshot.processing_time_ms = 0; -+ result.nodes_processed = 0; -+ std::move(processing_context->callback).Run(std::move(result)); -+ return; -+ } -+ -+ // Process nodes in batches -+ const size_t batch_size = 100; -+ size_t num_batches = (tree_update.nodes.size() + batch_size - 1) / batch_size; -+ processing_context->pending_batches = num_batches; -+ -+ for (size_t i = 0; i < tree_update.nodes.size(); i += batch_size) { -+ size_t end = std::min(i + batch_size, tree_update.nodes.size()); -+ std::vector batch( -+ tree_update.nodes.begin() + i, -+ tree_update.nodes.begin() + end); -+ -+ // Post task to ThreadPool with reply -+ base::ThreadPool::PostTaskAndReply( -+ FROM_HERE, -+ {base::TaskPriority::USER_VISIBLE}, -+ base::BindOnce(&ContentProcessor::ProcessNodeBatchParallel, -+ std::move(batch), -+ processing_context), -+ base::BindOnce(&ContentProcessor::OnBatchProcessed, -+ processing_context)); -+ } -+} -+ -+} // namespace api -+} // namespace extensions -\ No newline at end of file -diff --git a/chrome/browser/extensions/api/browser_os/browser_os_content_processor.h b/chrome/browser/extensions/api/browser_os/browser_os_content_processor.h -new file mode 100644 -index 0000000000000..e553cd8e5ddb9 ---- /dev/null -+++ b/chrome/browser/extensions/api/browser_os/browser_os_content_processor.h -@@ -0,0 +1,173 @@ -+// Copyright 2024 The Chromium Authors -+// Use of this source code is governed by a BSD-style license that can be -+// found in the LICENSE file. -+ -+#ifndef CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_CONTENT_PROCESSOR_H_ -+#define CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_CONTENT_PROCESSOR_H_ -+ -+#include -+#include -+#include -+#include -+#include -+#include -+ -+#include "base/functional/callback.h" -+#include "base/memory/ref_counted.h" -+#include "chrome/common/extensions/api/browser_os.h" -+#include "ui/accessibility/ax_tree_update.h" -+#include "ui/gfx/geometry/rect.h" -+ -+namespace ui { -+struct AXNodeData; -+} // namespace ui -+ -+namespace extensions { -+namespace api { -+ -+// Result of content processing -+struct ContentProcessingResult { -+ browser_os::Snapshot snapshot; -+ int nodes_processed = 0; -+ int64_t processing_time_ms = 0; -+}; -+ -+// Processes accessibility trees to extract content (text/links) with parallel processing -+class ContentProcessor { -+ public: -+ // Node information for batch processing -+ struct NodeInfo { -+ NodeInfo(); -+ NodeInfo(const NodeInfo&); -+ NodeInfo(NodeInfo&&); -+ NodeInfo& operator=(const NodeInfo&); -+ NodeInfo& operator=(NodeInfo&&); -+ ~NodeInfo(); -+ -+ int32_t id; -+ std::string role; -+ std::string name; -+ std::string value; -+ std::string url; -+ gfx::Rect bounds; -+ std::vector child_ids; -+ // Additional attributes -+ std::unordered_map attributes; -+ }; -+ -+ // Section information -+ struct SectionInfo { -+ SectionInfo(); -+ SectionInfo(const SectionInfo&) = delete; -+ SectionInfo(SectionInfo&&); -+ SectionInfo& operator=(const SectionInfo&) = delete; -+ SectionInfo& operator=(SectionInfo&&); -+ ~SectionInfo(); -+ -+ browser_os::SectionType type; -+ std::string label; -+ // Text content for this section -+ std::string text_content; -+ // Links found in this section -+ std::vector links; -+ }; -+ -+ ContentProcessor() = default; -+ ~ContentProcessor() = default; -+ -+ // Main processing function - handles all threading internally -+ static void ProcessAccessibilityTree( -+ const ui::AXTreeUpdate& tree_update, -+ browser_os::SnapshotType type, -+ browser_os::SnapshotContext context, -+ const std::vector& include_sections, -+ const gfx::Size& viewport_size, -+ base::OnceCallback callback); -+ -+ -+ private: -+ // Internal processing context for thread safety -+ struct ProcessingContext : public base::RefCountedThreadSafe { -+ ProcessingContext(); -+ -+ // Input data -+ ui::AXTreeUpdate tree_update; -+ browser_os::SnapshotType snapshot_type; -+ browser_os::SnapshotContext snapshot_context; -+ std::vector include_sections; -+ gfx::Size viewport_size; -+ base::OnceCallback callback; -+ -+ // Processing state -+ std::atomic pending_batches{0}; -+ base::Time start_time; -+ -+ // Thread-safe section management -+ mutable base::Lock sections_lock; -+ std::unordered_map> sections; -+ -+ // Thread-safe caching for section detection -+ mutable base::Lock section_cache_lock; -+ std::unordered_map node_to_section_cache; -+ std::unordered_map section_root_nodes; -+ -+ // Node map built from tree_update (read-only after construction) -+ std::unordered_map node_map; -+ -+ private: -+ friend class base::RefCountedThreadSafe; -+ ~ProcessingContext(); -+ }; -+ -+ // Helper functions -+ static browser_os::SectionType GetSectionType(const NodeInfo& node); -+ static bool IsNodeVisible(const NodeInfo& node, const gfx::Rect& viewport_bounds); -+ static std::string ExtractNodeText(const NodeInfo& node); -+ static browser_os::LinkInfo ExtractLinkInfo(const NodeInfo& node); -+ static bool IsLink(const NodeInfo& node); -+ static bool IsTextNode(const NodeInfo& node); -+ -+ // Section detection and caching -+ static browser_os::SectionType DetermineNodeSection( -+ int32_t node_id, -+ const std::unordered_map& node_map, -+ scoped_refptr context); -+ static void CacheNodeSection( -+ int32_t node_id, -+ browser_os::SectionType section_type, -+ scoped_refptr context); -+ static browser_os::SectionType GetSectionTypeFromNode( -+ const ui::AXNodeData& node); -+ -+ // Thread-safe section content processing -+ static void AddTextToSection( -+ browser_os::SectionType section_type, -+ const std::string& text, -+ scoped_refptr context); -+ static void AddLinkToSection( -+ browser_os::SectionType section_type, -+ browser_os::LinkInfo link, -+ scoped_refptr context); -+ -+ // Batch processing with integrated section detection -+ static void ProcessNodeBatchParallel( -+ const std::vector& batch, -+ scoped_refptr context); -+ -+ // Helper functions for parallel processing -+ static std::string ExtractTextFromAXNode(const ui::AXNodeData& node); -+ static bool IsLinkNode(const ui::AXNodeData& node); -+ static browser_os::LinkInfo ExtractLinkFromAXNode(const ui::AXNodeData& node); -+ -+ // Batch processing callbacks -+ static void OnBatchProcessed(scoped_refptr context); -+ static void OnAllBatchesComplete(scoped_refptr context); -+ -+ ContentProcessor(const ContentProcessor&) = delete; -+ ContentProcessor& operator=(const ContentProcessor&) = delete; -+}; -+ -+} // namespace api -+} // namespace extensions -+ -+#endif // CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_CONTENT_PROCESSOR_H_ -\ No newline at end of file -diff --git a/chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.cc b/chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.cc -index c84fe62d9e76d..7523bf7881787 100644 ---- a/chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.cc -+++ b/chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.cc -@@ -467,6 +467,7 @@ std::vector SnapshotProcessor::ProcessNodeBatc - data.attributes["depth"] = std::to_string(depth); - - // Check if node is in viewport -+ // TODO: Fix this logic. still not accurate in terms of saying if in view port or not - bool in_viewport = false; - if (!doc_viewport_bounds.IsEmpty()) { - // Convert absolute bounds to integer rect for intersection test -diff --git a/chrome/common/extensions/api/browser_os.idl b/chrome/common/extensions/api/browser_os.idl -index f088a5d1041f0..c34c96484ccbf 100644 ---- a/chrome/common/extensions/api/browser_os.idl -+++ b/chrome/common/extensions/api/browser_os.idl -@@ -84,6 +84,81 @@ namespace browserOS { - callback SendKeysCallback = void(); - callback CaptureScreenshotCallback = void(DOMString dataUrl); - -+ // Snapshot extraction types -+ enum SnapshotType { -+ text, -+ links -+ }; -+ -+ // Context for snapshot extraction -+ enum SnapshotContext { -+ visible, -+ full -+ }; -+ -+ // Section types based on ARIA landmarks -+ enum SectionType { -+ main, -+ navigation, -+ footer, -+ header, -+ article, -+ aside, -+ complementary, -+ contentinfo, -+ form, -+ search, -+ region, -+ other -+ }; -+ -+ // Text snapshot result for a section -+ dictionary TextSnapshotResult { -+ DOMString text; -+ long characterCount; -+ }; -+ -+ // Link information -+ dictionary LinkInfo { -+ DOMString text; -+ DOMString url; -+ DOMString? title; -+ object? attributes; -+ boolean isExternal; -+ }; -+ -+ // Links snapshot result for a section -+ dictionary LinksSnapshotResult { -+ LinkInfo[] links; -+ }; -+ -+ // Section with all possible snapshot results -+ dictionary SnapshotSection { -+ DOMString type; -+ // Text result - only populated for text snapshots -+ TextSnapshotResult textResult; -+ // Links result - only populated for links snapshots -+ LinksSnapshotResult linksResult; -+ }; -+ -+ // Main snapshot result -+ dictionary Snapshot { -+ SnapshotType type; -+ SnapshotContext context; -+ double timestamp; -+ SnapshotSection[] sections; -+ long processingTimeMs; -+ }; -+ -+ // Options for getSnapshot -+ dictionary SnapshotOptions { -+ // Defaults to visible if not specified -+ SnapshotContext context; -+ SectionType[]? includeSections; -+ }; -+ -+ callback GetSnapshotCallback = void(Snapshot snapshot); -+ - interface Functions { - // Gets the full accessibility tree for a tab - // |tabId|: The tab to get the accessibility tree for. Defaults to active tab. -@@ -189,6 +264,17 @@ namespace browserOS { - static void captureScreenshot( - optional long tabId, - CaptureScreenshotCallback callback); -+ -+ // Gets a content snapshot of the specified type from the page -+ // |tabId|: The tab to get the snapshot from. Defaults to active tab. -+ // |type|: The type of snapshot to extract (text or links). -+ // |options|: Options for the snapshot extraction. -+ // |callback|: Called with the snapshot data. -+ static void getSnapshot( -+ optional long tabId, -+ SnapshotType type, -+ optional SnapshotOptions options, -+ GetSnapshotCallback callback); - }; - }; - -diff --git a/extensions/browser/extension_function_histogram_value.h b/extensions/browser/extension_function_histogram_value.h -index 880d5f4812347..965512eee1a46 100644 ---- a/extensions/browser/extension_function_histogram_value.h -+++ b/extensions/browser/extension_function_histogram_value.h -@@ -2009,6 +2009,7 @@ enum HistogramValue { - BROWSER_OS_SENDKEYS = 1946, - BROWSER_OS_GETPAGESTRUCTURE = 1947, - BROWSER_OS_CAPTURESCREENSHOT = 1948, -+ BROWSER_OS_GETSNAPSHOT = 1949, - // Last entry: Add new entries above, then run: - // tools/metrics/histograms/update_extension_histograms.py - ENUM_BOUNDARY --- -2.49.0 - diff --git a/patches/nxtscape/nxtscape-settings-ui.patch b/patches/nxtscape/nxtscape-settings-ui.patch deleted file mode 100644 index c0de8b74d..000000000 --- a/patches/nxtscape/nxtscape-settings-ui.patch +++ /dev/null @@ -1,1170 +0,0 @@ -From 231cb919a8a1b0ad03280a1e3330f4711d29f847 Mon Sep 17 00:00:00 2001 -From: Nikhil Sonti -Date: Tue, 22 Jul 2025 21:33:35 -0700 -Subject: [PATCH 01/20] patch(M): nxtscape-settings-ui - ---- - .../api/settings_private/prefs_util.cc | 26 + - chrome/browser/prefs/browser_prefs.cc | 29 + - chrome/browser/prefs/browser_prefs.h | 2 + - chrome/browser/resources/settings/BUILD.gn | 1 + - .../settings/nxtscape_page/nxtscape_page.html | 641 ++++++++++++++++++ - .../settings/nxtscape_page/nxtscape_page.ts | 267 ++++++++ - chrome/browser/resources/settings/route.ts | 1 + - chrome/browser/resources/settings/router.ts | 1 + - chrome/browser/resources/settings/settings.ts | 1 + - .../settings/settings_main/settings_main.html | 6 + - .../settings/settings_main/settings_main.ts | 15 +- - .../settings/settings_menu/settings_menu.html | 6 + - 12 files changed, 992 insertions(+), 4 deletions(-) - create mode 100644 chrome/browser/resources/settings/nxtscape_page/nxtscape_page.html - create mode 100644 chrome/browser/resources/settings/nxtscape_page/nxtscape_page.ts - -diff --git a/chrome/browser/extensions/api/settings_private/prefs_util.cc b/chrome/browser/extensions/api/settings_private/prefs_util.cc -index c27e0e96e4bce..1869a54c5b4e4 100644 ---- a/chrome/browser/extensions/api/settings_private/prefs_util.cc -+++ b/chrome/browser/extensions/api/settings_private/prefs_util.cc -@@ -580,6 +580,32 @@ const PrefsUtil::TypedPrefMap& PrefsUtil::GetAllowlistedKeys() { - (*s_allowlist)[::prefs::kCaretBrowsingEnabled] = - settings_api::PrefType::kBoolean; - -+ // Nxtscape AI provider preferences -+ (*s_allowlist)["nxtscape.default_provider"] = settings_api::PrefType::kString; -+ -+ // Nxtscape provider settings -+ (*s_allowlist)["nxtscape.nxtscape_model"] = settings_api::PrefType::kString; -+ -+ // OpenAI provider settings -+ (*s_allowlist)["nxtscape.openai_api_key"] = settings_api::PrefType::kString; -+ (*s_allowlist)["nxtscape.openai_model"] = settings_api::PrefType::kString; -+ (*s_allowlist)["nxtscape.openai_base_url"] = settings_api::PrefType::kString; -+ -+ // Anthropic provider settings -+ (*s_allowlist)["nxtscape.anthropic_api_key"] = settings_api::PrefType::kString; -+ (*s_allowlist)["nxtscape.anthropic_model"] = settings_api::PrefType::kString; -+ (*s_allowlist)["nxtscape.anthropic_base_url"] = settings_api::PrefType::kString; -+ -+ // Gemini provider settings -+ (*s_allowlist)["nxtscape.gemini_api_key"] = settings_api::PrefType::kString; -+ (*s_allowlist)["nxtscape.gemini_model"] = settings_api::PrefType::kString; -+ (*s_allowlist)["nxtscape.gemini_base_url"] = settings_api::PrefType::kString; -+ -+ // Ollama provider settings -+ (*s_allowlist)["nxtscape.ollama_api_key"] = settings_api::PrefType::kString; -+ (*s_allowlist)["nxtscape.ollama_base_url"] = settings_api::PrefType::kString; -+ (*s_allowlist)["nxtscape.ollama_model"] = settings_api::PrefType::kString; -+ - #if BUILDFLAG(IS_CHROMEOS) - // Accounts / Users / People. - (*s_allowlist)[ash::kAccountsPrefAllowGuest] = -diff --git a/chrome/browser/prefs/browser_prefs.cc b/chrome/browser/prefs/browser_prefs.cc -index 9a00400829ae1..5a5b278252383 100644 ---- a/chrome/browser/prefs/browser_prefs.cc -+++ b/chrome/browser/prefs/browser_prefs.cc -@@ -1939,6 +1939,7 @@ void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry, - regional_capabilities::prefs::RegisterProfilePrefs(registry); - RegisterBrowserUserPrefs(registry); - RegisterGeminiSettingsPrefs(registry); -+ RegisterNxtscapePrefs(registry); - RegisterPrefersDefaultScrollbarStylesPrefs(registry); - RegisterSafetyHubProfilePrefs(registry); - #if BUILDFLAG(IS_CHROMEOS) -@@ -2322,6 +2323,34 @@ void RegisterGeminiSettingsPrefs(user_prefs::PrefRegistrySyncable* registry) { - registry->RegisterIntegerPref(prefs::kGeminiSettings, 0); - } - -+void RegisterNxtscapePrefs(user_prefs::PrefRegistrySyncable* registry) { -+ // Nxtscape AI provider preferences -+ registry->RegisterStringPref("nxtscape.default_provider", "nxtscape"); -+ -+ // Nxtscape provider settings -+ registry->RegisterStringPref("nxtscape.nxtscape_model", ""); -+ -+ // OpenAI provider settings -+ registry->RegisterStringPref("nxtscape.openai_api_key", ""); -+ registry->RegisterStringPref("nxtscape.openai_model", "gpt-4o"); -+ registry->RegisterStringPref("nxtscape.openai_base_url", ""); -+ -+ // Anthropic provider settings -+ registry->RegisterStringPref("nxtscape.anthropic_api_key", ""); -+ registry->RegisterStringPref("nxtscape.anthropic_model", "claude-3-5-sonnet-latest"); -+ registry->RegisterStringPref("nxtscape.anthropic_base_url", ""); -+ -+ // Gemini provider settings -+ registry->RegisterStringPref("nxtscape.gemini_api_key", ""); -+ registry->RegisterStringPref("nxtscape.gemini_model", "gemini-1.5-pro"); -+ registry->RegisterStringPref("nxtscape.gemini_base_url", ""); -+ -+ // Ollama provider settings -+ registry->RegisterStringPref("nxtscape.ollama_api_key", ""); -+ registry->RegisterStringPref("nxtscape.ollama_base_url", "http://localhost:11434"); -+ registry->RegisterStringPref("nxtscape.ollama_model", ""); -+} -+ - #if BUILDFLAG(IS_CHROMEOS) - void RegisterSigninProfilePrefs(user_prefs::PrefRegistrySyncable* registry, - std::string_view country) { -diff --git a/chrome/browser/prefs/browser_prefs.h b/chrome/browser/prefs/browser_prefs.h -index 3a1c48b14b37f..5600baa2143e0 100644 ---- a/chrome/browser/prefs/browser_prefs.h -+++ b/chrome/browser/prefs/browser_prefs.h -@@ -32,6 +32,8 @@ void RegisterScreenshotPrefs(PrefRegistrySimple* registry); - - void RegisterGeminiSettingsPrefs(user_prefs::PrefRegistrySyncable* registry); - -+void RegisterNxtscapePrefs(user_prefs::PrefRegistrySyncable* registry); -+ - // Register all prefs that will be used via a PrefService attached to a user - // Profile using the locale of |g_browser_process|. - void RegisterUserProfilePrefs(user_prefs::PrefRegistrySyncable* registry); -diff --git a/chrome/browser/resources/settings/BUILD.gn b/chrome/browser/resources/settings/BUILD.gn -index 6eb2b37837e97..1a8cd69860514 100644 ---- a/chrome/browser/resources/settings/BUILD.gn -+++ b/chrome/browser/resources/settings/BUILD.gn -@@ -56,6 +56,7 @@ build_webui("build") { - web_component_files = [ - "a11y_page/a11y_page.ts", - "about_page/about_page.ts", -+ "nxtscape_page/nxtscape_page.ts", - "ai_page/ai_compare_subpage.ts", - "ai_page/ai_info_card.ts", - "ai_page/ai_logging_info_bullet.ts", -diff --git a/chrome/browser/resources/settings/nxtscape_page/nxtscape_page.html b/chrome/browser/resources/settings/nxtscape_page/nxtscape_page.html -new file mode 100644 -index 0000000000000..28e18b5f69f95 ---- /dev/null -+++ b/chrome/browser/resources/settings/nxtscape_page/nxtscape_page.html -@@ -0,0 +1,641 @@ -+ -+ -+ -+
-+
-+ -+
-+
-+

AI Provider Settings

-+
-+ Configure your preferred AI provider and model settings. Your selection will be used across all BrowserOS AI features. -+
-+
-+
-+ -+ -+
-+
-+ -+
-+
-+ -+
-+

Your API keys are secure

-+

-+ All API keys are stored locally on your device. Your credentials remain private and encrypted on your computer. -+

-+
-+
-+
-+ -+
-+ -+
-+
-+
-+ N -+
-+
-+

BrowserOS AI

-+
-+ -+ [[getProviderStatus_('nxtscape', prefs.nxtscape.default_provider.value)]] -+
-+
-+ Powered by BrowserOS's AI service. -+
-+
-+
-+
-+ -+
-+
-+ -+ -+
-+
-+
-+ AI -+
-+
-+

OpenAI

-+
-+ -+ [[getProviderStatus_('openai', prefs.nxtscape.default_provider.value)]] -+
-+
-+
-+
-+
-+ -+ -+
Your OpenAI API key (required)
-+
-+
-+ -+ -+
Override the OpenAI API base URL (leave empty for default)
-+
-+
-+ -+ -+
Select the OpenAI model to use for AI operations
-+
-+
-+
-+ -+ -+
-+
-+
-+ C -+
-+
-+

Anthropic

-+
-+ -+ [[getProviderStatus_('anthropic', prefs.nxtscape.default_provider.value)]] -+
-+
-+
-+
-+
-+ -+ -+
Your Anthropic API key (required)
-+
-+
-+ -+ -+
Override the Anthropic API base URL (leave empty for default)
-+
-+
-+ -+ -+
Choose your preferred Claude model
-+
-+
-+
-+ -+ -+
-+
-+
-+ G -+
-+
-+

Google Gemini

-+
-+ -+ [[getProviderStatus_('gemini', prefs.nxtscape.default_provider.value)]] -+
-+
-+
-+
-+
-+ -+ -+
Your Google Gemini API key (required)
-+
-+
-+ -+ -+
Override the Gemini API base URL (leave empty for default)
-+
-+
-+ -+ -+
Select the Gemini model to use for AI operations
-+
-+
-+
-+ -+ -+
-+
-+
-+ O -+
-+
-+

Ollama

-+
-+ -+ [[getProviderStatus_('ollama', prefs.nxtscape.default_provider.value)]] -+
-+
-+
-+
-+
-+ -+ -+
Only required if your Ollama instance uses authentication
-+
-+
-+ -+ -+
URL of your Ollama server
-+
-+
-+ -+ -+
Name of the model installed in your Ollama instance
-+
-+
-+
How to run Ollama:
-+
-+ # Pull a model
-+ollama pull qwen3:14b

-+# Serve with CORS enabled
-+OLLAMA_ORIGINS="*" ollama serve
-+
-+
-+ Note: Set OLLAMA_ORIGINS to allow BrowserOS to access your Ollama server. -+
-+
-+
-+
-+
-+ -+
-+ -+ Settings saved successfully -+
-\ No newline at end of file -diff --git a/chrome/browser/resources/settings/nxtscape_page/nxtscape_page.ts b/chrome/browser/resources/settings/nxtscape_page/nxtscape_page.ts -new file mode 100644 -index 0000000000000..8c4421ef76e45 ---- /dev/null -+++ b/chrome/browser/resources/settings/nxtscape_page/nxtscape_page.ts -@@ -0,0 +1,267 @@ -+// Copyright 2024 The Chromium Authors -+// Use of this source code is governed by a BSD-style license that can be -+// found in the LICENSE file. -+ -+/** -+ * @fileoverview 'settings-nxtscape-page' contains AI provider-specific settings. -+ */ -+ -+import '../settings_page/settings_section.js'; -+import '../settings_page_styles.css.js'; -+import '../settings_shared.css.js'; -+import '../controls/settings_toggle_button.js'; -+import 'chrome://resources/cr_elements/cr_button/cr_button.js'; -+import 'chrome://resources/cr_elements/cr_icon/cr_icon.js'; -+import 'chrome://resources/cr_elements/icons.html.js'; -+import 'chrome://resources/cr_elements/cr_shared_style.css.js'; -+ -+import {PrefsMixin} from '/shared/settings/prefs/prefs_mixin.js'; -+import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; -+ -+import {getTemplate} from './nxtscape_page.html.js'; -+ -+const SettingsNxtscapePageElementBase = PrefsMixin(PolymerElement); -+ -+export class SettingsNxtscapePageElement extends SettingsNxtscapePageElementBase { -+ static get is() { -+ return 'settings-nxtscape-page'; -+ } -+ -+ static get template() { -+ return getTemplate(); -+ } -+ -+ static get properties() { -+ return { -+ /** -+ * Preferences state. -+ */ -+ prefs: { -+ type: Object, -+ notify: true, -+ }, -+ }; -+ } -+ -+ // Declare prefs property to satisfy ESLint -+ declare prefs: any; -+ -+ /** -+ * Get the CSS class for a provider card based on selection -+ */ -+ private getProviderCardClass_(provider: string, selectedProvider: string): string { -+ return provider === selectedProvider ? 'provider-card selected' : 'provider-card'; -+ } -+ -+ /** -+ * Get the status text for a provider -+ */ -+ private getProviderStatus_(provider: string, selectedProvider: string): string { -+ return provider === selectedProvider ? 'Active' : 'Inactive'; -+ } -+ -+ /** -+ * Handle default provider selection change -+ */ -+ private onDefaultProviderChange_(e: Event) { -+ const select = e.target as HTMLSelectElement; -+ const value = select.value; -+ -+ // Update the preference using PrefsMixin -+ // @ts-ignore: setPrefValue exists at runtime from PrefsMixin -+ this.setPrefValue('nxtscape.default_provider', value); -+ this.showStatusMessage_(); -+ } -+ -+ /** -+ * Handle Nxtscape model selection change -+ */ -+ private onNxtscapeModelChange_(e: Event) { -+ const select = e.target as HTMLSelectElement; -+ const value = select.value; -+ -+ // Update the preference using PrefsMixin -+ // @ts-ignore: setPrefValue exists at runtime from PrefsMixin -+ this.setPrefValue('nxtscape.nxtscape_model', value); -+ this.showStatusMessage_(); -+ } -+ -+ /** -+ * Handle OpenAI model selection change -+ */ -+ private onOpenAIModelChange_(e: Event) { -+ const select = e.target as HTMLSelectElement; -+ const value = select.value; -+ -+ // Update the preference using PrefsMixin -+ // @ts-ignore: setPrefValue exists at runtime from PrefsMixin -+ this.setPrefValue('nxtscape.openai_model', value); -+ this.showStatusMessage_(); -+ } -+ -+ /** -+ * Handle OpenAI API key change -+ */ -+ private onOpenAIApiKeyChange_(e: Event) { -+ const input = e.target as HTMLInputElement; -+ const value = input.value; -+ -+ // Update the preference using PrefsMixin -+ // @ts-ignore: setPrefValue exists at runtime from PrefsMixin -+ this.setPrefValue('nxtscape.openai_api_key', value); -+ this.showStatusMessage_(); -+ } -+ -+ /** -+ * Handle OpenAI base URL change -+ */ -+ private onOpenAIBaseUrlChange_(e: Event) { -+ const input = e.target as HTMLInputElement; -+ const value = input.value; -+ -+ // Update the preference using PrefsMixin -+ // @ts-ignore: setPrefValue exists at runtime from PrefsMixin -+ this.setPrefValue('nxtscape.openai_base_url', value); -+ this.showStatusMessage_(); -+ } -+ -+ /** -+ * Handle Anthropic model selection change -+ */ -+ private onAnthropicModelChange_(e: Event) { -+ const select = e.target as HTMLSelectElement; -+ const value = select.value; -+ -+ // Update the preference using PrefsMixin -+ // @ts-ignore: setPrefValue exists at runtime from PrefsMixin -+ this.setPrefValue('nxtscape.anthropic_model', value); -+ this.showStatusMessage_(); -+ } -+ -+ /** -+ * Handle Anthropic API key change -+ */ -+ private onAnthropicApiKeyChange_(e: Event) { -+ const input = e.target as HTMLInputElement; -+ const value = input.value; -+ -+ // Update the preference using PrefsMixin -+ // @ts-ignore: setPrefValue exists at runtime from PrefsMixin -+ this.setPrefValue('nxtscape.anthropic_api_key', value); -+ this.showStatusMessage_(); -+ } -+ -+ /** -+ * Handle Anthropic base URL change -+ */ -+ private onAnthropicBaseUrlChange_(e: Event) { -+ const input = e.target as HTMLInputElement; -+ const value = input.value; -+ -+ // Update the preference using PrefsMixin -+ // @ts-ignore: setPrefValue exists at runtime from PrefsMixin -+ this.setPrefValue('nxtscape.anthropic_base_url', value); -+ this.showStatusMessage_(); -+ } -+ -+ /** -+ * Handle Gemini model selection change -+ */ -+ private onGeminiModelChange_(e: Event) { -+ const select = e.target as HTMLSelectElement; -+ const value = select.value; -+ -+ // Update the preference using PrefsMixin -+ // @ts-ignore: setPrefValue exists at runtime from PrefsMixin -+ this.setPrefValue('nxtscape.gemini_model', value); -+ this.showStatusMessage_(); -+ } -+ -+ /** -+ * Handle Gemini API key change -+ */ -+ private onGeminiApiKeyChange_(e: Event) { -+ const input = e.target as HTMLInputElement; -+ const value = input.value; -+ -+ // Update the preference using PrefsMixin -+ // @ts-ignore: setPrefValue exists at runtime from PrefsMixin -+ this.setPrefValue('nxtscape.gemini_api_key', value); -+ this.showStatusMessage_(); -+ } -+ -+ /** -+ * Handle Gemini base URL change -+ */ -+ private onGeminiBaseUrlChange_(e: Event) { -+ const input = e.target as HTMLInputElement; -+ const value = input.value; -+ -+ // Update the preference using PrefsMixin -+ // @ts-ignore: setPrefValue exists at runtime from PrefsMixin -+ this.setPrefValue('nxtscape.gemini_base_url', value); -+ this.showStatusMessage_(); -+ } -+ -+ /** -+ * Handle Ollama API key change -+ */ -+ private onOllamaApiKeyChange_(e: Event) { -+ const input = e.target as HTMLInputElement; -+ const value = input.value; -+ -+ // Update the preference using PrefsMixin -+ // @ts-ignore: setPrefValue exists at runtime from PrefsMixin -+ this.setPrefValue('nxtscape.ollama_api_key', value); -+ this.showStatusMessage_(); -+ } -+ -+ /** -+ * Handle Ollama base URL change -+ */ -+ private onOllamaBaseUrlChange_(e: Event) { -+ const input = e.target as HTMLInputElement; -+ const value = input.value; -+ -+ // Update the preference using PrefsMixin -+ // @ts-ignore: setPrefValue exists at runtime from PrefsMixin -+ this.setPrefValue('nxtscape.ollama_base_url', value); -+ this.showStatusMessage_(); -+ } -+ -+ /** -+ * Handle Ollama model change -+ */ -+ private onOllamaModelChange_(e: Event) { -+ const input = e.target as HTMLInputElement; -+ const value = input.value; -+ -+ // Update the preference using PrefsMixin -+ // @ts-ignore: setPrefValue exists at runtime from PrefsMixin -+ this.setPrefValue('nxtscape.ollama_model', value); -+ this.showStatusMessage_(); -+ } -+ -+ /** -+ * Show status message briefly -+ */ -+ private showStatusMessage_() { -+ // @ts-ignore: shadowRoot exists at runtime -+ const statusMessage = this.shadowRoot!.querySelector('#statusMessage'); -+ if (statusMessage) { -+ statusMessage.classList.add('show'); -+ setTimeout(() => { -+ statusMessage.classList.remove('show'); -+ }, 2000); -+ } -+ } -+} -+ -+declare global { -+ interface HTMLElementTagNameMap { -+ 'settings-nxtscape-page': SettingsNxtscapePageElement; -+ } -+} -+ -+customElements.define( -+ SettingsNxtscapePageElement.is, SettingsNxtscapePageElement); -\ No newline at end of file -diff --git a/chrome/browser/resources/settings/route.ts b/chrome/browser/resources/settings/route.ts -index 2458ecb3791b0..e8dd01dc3e7b6 100644 ---- a/chrome/browser/resources/settings/route.ts -+++ b/chrome/browser/resources/settings/route.ts -@@ -183,6 +183,7 @@ function createRoutes(): SettingsRoutes { - // Root pages. - r.BASIC = new Route('/'); - r.ABOUT = new Route('/help', loadTimeData.getString('aboutPageTitle')); -+ r.NXTSCAPE = new Route('/browseros-ai', 'BrowserOS AI Settings'); - - r.SEARCH = r.BASIC.createSection( - '/search', 'search', loadTimeData.getString('searchPageTitle')); -diff --git a/chrome/browser/resources/settings/router.ts b/chrome/browser/resources/settings/router.ts -index 236c564f9b909..46c2093278ceb 100644 ---- a/chrome/browser/resources/settings/router.ts -+++ b/chrome/browser/resources/settings/router.ts -@@ -14,6 +14,7 @@ import {loadTimeData} from './i18n_setup.js'; - export interface SettingsRoutes { - ABOUT: Route; - ACCESSIBILITY: Route; -+ NXTSCAPE: Route; - ADDRESSES: Route; - ADVANCED: Route; - AI: Route; -diff --git a/chrome/browser/resources/settings/settings.ts b/chrome/browser/resources/settings/settings.ts -index 85e1db9929325..dbd5e82c285f9 100644 ---- a/chrome/browser/resources/settings/settings.ts -+++ b/chrome/browser/resources/settings/settings.ts -@@ -32,6 +32,7 @@ export {OpenWindowProxy, OpenWindowProxyImpl} from 'chrome://resources/js/open_w - export {PluralStringProxyImpl as SettingsPluralStringProxyImpl} from 'chrome://resources/js/plural_string_proxy.js'; - export {getTrustedHTML} from 'chrome://resources/js/static_types.js'; - export {SettingsAboutPageElement} from './about_page/about_page.js'; -+export {SettingsNxtscapePageElement} from './nxtscape_page/nxtscape_page.js'; - export {ControlledRadioButtonElement} from './controls/controlled_radio_button.js'; - export {SettingsDropdownMenuElement} from './controls/settings_dropdown_menu.js'; - export {SettingsToggleButtonElement} from './controls/settings_toggle_button.js'; -diff --git a/chrome/browser/resources/settings/settings_main/settings_main.html b/chrome/browser/resources/settings/settings_main/settings_main.html -index 329e9552760de..403f2f2258fb8 100644 ---- a/chrome/browser/resources/settings/settings_main/settings_main.html -+++ b/chrome/browser/resources/settings/settings_main/settings_main.html -@@ -49,3 +49,9 @@ - prefs="{{prefs}}"> - - -+ -diff --git a/chrome/browser/resources/settings/settings_main/settings_main.ts b/chrome/browser/resources/settings/settings_main/settings_main.ts -index 43fd55ea0b83c..433afef3be384 100644 ---- a/chrome/browser/resources/settings/settings_main/settings_main.ts -+++ b/chrome/browser/resources/settings/settings_main/settings_main.ts -@@ -14,6 +14,7 @@ import 'chrome://resources/cr_elements/icons.html.js'; - import 'chrome://resources/js/search_highlight_utils.js'; - import 'chrome://resources/cr_elements/cr_icon/cr_icon.js'; - import '../about_page/about_page.js'; -+import '../nxtscape_page/nxtscape_page.js'; - import '../basic_page/basic_page.js'; - import '../search_settings.js'; - import '../settings_shared.css.js'; -@@ -32,6 +33,7 @@ import {getTemplate} from './settings_main.html.js'; - interface MainPageVisibility { - about: boolean; - settings: boolean; -+ nxtscape: boolean; - } - - export interface SettingsMainElement { -@@ -68,7 +70,7 @@ export class SettingsMainElement extends SettingsMainElementBase { - showPages_: { - type: Object, - value() { -- return {about: false, settings: false}; -+ return {about: false, settings: false, nxtscape: false}; - }, - }, - -@@ -114,9 +116,14 @@ export class SettingsMainElement extends SettingsMainElementBase { - * current route. - */ - override currentRouteChanged() { -- const inAbout = -- routes.ABOUT.contains(Router.getInstance().getCurrentRoute()); -- this.showPages_ = {about: inAbout, settings: !inAbout}; -+ const currentRoute = Router.getInstance().getCurrentRoute(); -+ const inAbout = routes.ABOUT.contains(currentRoute); -+ const inNxtscape = routes.NXTSCAPE.contains(currentRoute); -+ this.showPages_ = { -+ about: inAbout, -+ settings: !inAbout && !inNxtscape, -+ nxtscape: inNxtscape -+ }; - } - - private onShowingSubpage_() { -diff --git a/chrome/browser/resources/settings/settings_menu/settings_menu.html b/chrome/browser/resources/settings/settings_menu/settings_menu.html -index 79aad7032abc7..8f123acf9b322 100644 ---- a/chrome/browser/resources/settings/settings_menu/settings_menu.html -+++ b/chrome/browser/resources/settings/settings_menu/settings_menu.html -@@ -57,6 +57,12 @@ - $i18n{peoplePageTitle} - - -+ -+ -+ BrowserOS AI -+ -+ -