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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Win-CodexBar is a Windows system-tray app for keeping AI coding-tool usage visib
- **Tray-first workflow** with a compact provider grid, usage cards, refresh action, settings shortcut, and quit control.
- **Provider settings** for source selection, credentials, cookie import, token accounts, API keys, regions, and tray-display preferences.
- **Windows credential protection** for app-managed API keys, manual cookies, and token accounts, using user-scoped DPAPI where available.
- **Browser cookie import** for Chrome, Edge, Brave, and Firefox, kept opt-in per provider.
- **Browser cookie import** for Chrome Stable, Beta, Dev, Canary, Chrome for Testing, Chromium, Edge, Brave, and Firefox, kept opt-in per provider.
- **Installed local CLI** for scripting usage, cost, config, diagnostics, and loopback integrations.
- **Installer + portable builds** with WebView2 runtime bootstrap, VC++ runtime bootstrap, and SHA-256 checksum files.

Expand Down
4 changes: 4 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/browser_import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ fn browser_type_key(bt: codexbar::browser::detection::BrowserType) -> &'static s
use codexbar::browser::detection::BrowserType;
match bt {
BrowserType::Chrome => "chrome",
BrowserType::ChromeBeta => "chrome-beta",
BrowserType::ChromeDev => "chrome-dev",
BrowserType::ChromeCanary => "chrome-canary",
BrowserType::ChromeForTesting => "chrome-for-testing",
BrowserType::Edge => "edge",
BrowserType::Brave => "brave",
BrowserType::Arc => "arc",
Expand Down
2 changes: 1 addition & 1 deletion docs/PROVIDERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Auth resolution helpers in `rust/src/providers/` commonly try: explicit settings

## Cookie-backed providers

Windows browser import: Chrome, Edge, Brave (DPAPI + AES-GCM), Firefox (SQLite).
Windows browser import: Chrome Stable, Beta, Dev, Canary, Chrome for Testing, Chromium, Edge, Brave (DPAPI + AES-GCM), and Firefox (SQLite). Chrome channels are detected separately and keep distinct import choices.
Settings → **Providers** → provider detail → choose browser → Import.
Manual cookie header paste is the fallback (required under WSL for Chromium DPAPI).
Details: [COOKIES.md](./COOKIES.md).
Expand Down
119 changes: 107 additions & 12 deletions rust/src/browser/detection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ use crate::wsl;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BrowserType {
Chrome,
ChromeBeta,
ChromeDev,
ChromeCanary,
ChromeForTesting,
Edge,
Brave,
Arc,
Expand All @@ -28,6 +32,10 @@ impl BrowserType {
pub fn all() -> &'static [BrowserType] {
&[
BrowserType::Chrome,
BrowserType::ChromeBeta,
BrowserType::ChromeDev,
BrowserType::ChromeCanary,
BrowserType::ChromeForTesting,
BrowserType::Edge,
BrowserType::Brave,
BrowserType::Arc,
Expand All @@ -45,13 +53,55 @@ impl BrowserType {
pub fn display_name(&self) -> &'static str {
match self {
BrowserType::Chrome => "Google Chrome",
BrowserType::ChromeBeta => "Google Chrome Beta",
BrowserType::ChromeDev => "Google Chrome Dev",
BrowserType::ChromeCanary => "Google Chrome Canary",
BrowserType::ChromeForTesting => "Chrome for Testing",
BrowserType::Edge => "Microsoft Edge",
BrowserType::Brave => "Brave",
BrowserType::Arc => "Arc",
BrowserType::Firefox => "Firefox",
BrowserType::Chromium => "Chromium",
}
}

/// Resolve a browser's Windows AppData/Local profile root.
pub fn user_data_dir_under(&self, appdata_local: &Path) -> Option<PathBuf> {
match self {
BrowserType::Chrome => Some(
appdata_local
.join("Google")
.join("Chrome")
.join("User Data"),
),
BrowserType::ChromeBeta => Some(
appdata_local
.join("Google")
.join("Chrome Beta")
.join("User Data"),
),
BrowserType::ChromeDev => Some(
appdata_local
.join("Google")
.join("Chrome Dev")
.join("User Data"),
),
BrowserType::ChromeCanary => Some(
appdata_local
.join("Google")
.join("Chrome SxS")
.join("User Data"),
),
BrowserType::ChromeForTesting => Some(
appdata_local
.join("Google")
.join("Chrome for Testing")
.join("User Data"),
),
BrowserType::Chromium => Some(appdata_local.join("Chromium").join("User Data")),
_ => None,
}
}
}

/// A detected browser installation
Expand Down Expand Up @@ -142,12 +192,12 @@ impl BrowserDetector {
&& let Some(appdata_local) = wsl::windows_appdata_local()
{
let path = match browser_type {
BrowserType::Chrome => Some(
appdata_local
.join("Google")
.join("Chrome")
.join("User Data"),
),
BrowserType::Chrome
| BrowserType::ChromeBeta
| BrowserType::ChromeDev
| BrowserType::ChromeCanary
| BrowserType::ChromeForTesting
| BrowserType::Chromium => browser_type.user_data_dir_under(&appdata_local),
BrowserType::Edge => Some(
appdata_local
.join("Microsoft")
Expand All @@ -161,7 +211,6 @@ impl BrowserDetector {
.join("User Data"),
),
BrowserType::Arc => Some(appdata_local.join("Arc").join("User Data")),
BrowserType::Chromium => Some(appdata_local.join("Chromium").join("User Data")),
BrowserType::Firefox => wsl::windows_appdata_roaming()
.map(|roaming| roaming.join("Mozilla").join("Firefox").join("Profiles")),
};
Expand All @@ -176,10 +225,14 @@ impl BrowserDetector {
let app_data = dirs::data_dir()?;

let path = match browser_type {
BrowserType::Chrome => local_app_data
.join("Google")
.join("Chrome")
.join("User Data"),
BrowserType::Chrome
| BrowserType::ChromeBeta
| BrowserType::ChromeDev
| BrowserType::ChromeCanary
| BrowserType::ChromeForTesting
| BrowserType::Chromium => browser_type
.user_data_dir_under(&local_app_data)
.expect("matched a Chromium browser"),
BrowserType::Edge => local_app_data
.join("Microsoft")
.join("Edge")
Expand All @@ -189,7 +242,6 @@ impl BrowserDetector {
.join("Brave-Browser")
.join("User Data"),
BrowserType::Arc => local_app_data.join("Arc").join("User Data"),
BrowserType::Chromium => local_app_data.join("Chromium").join("User Data"),
BrowserType::Firefox => app_data.join("Mozilla").join("Firefox").join("Profiles"),
};

Expand Down Expand Up @@ -268,6 +320,49 @@ impl BrowserDetector {
mod tests {
use super::*;

#[test]
fn chrome_channels_resolve_to_distinct_windows_profile_roots() {
let appdata = Path::new("C:/Users/test/AppData/Local");
let channels = [
(
BrowserType::Chrome,
"Google/Chrome/User Data",
"Google Chrome",
),
(
BrowserType::ChromeBeta,
"Google/Chrome Beta/User Data",
"Google Chrome Beta",
),
(
BrowserType::ChromeDev,
"Google/Chrome Dev/User Data",
"Google Chrome Dev",
),
(
BrowserType::ChromeCanary,
"Google/Chrome SxS/User Data",
"Google Chrome Canary",
),
(
BrowserType::ChromeForTesting,
"Google/Chrome for Testing/User Data",
"Chrome for Testing",
),
(BrowserType::Chromium, "Chromium/User Data", "Chromium"),
];

for (browser, relative_path, display_name) in channels {
assert_eq!(
browser.user_data_dir_under(appdata).unwrap(),
appdata.join(relative_path),
"wrong profile root for {display_name}"
);
assert_eq!(browser.display_name(), display_name);
assert!(BrowserType::all().contains(&browser));
}
}

#[test]
fn test_browser_detection() {
let browsers = BrowserDetector::detect_all();
Expand Down
99 changes: 65 additions & 34 deletions rust/src/browser/wsl_paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use super::detection::{BrowserProfile, BrowserType, DetectedBrowser};
pub struct WslBrowserDetector;

impl WslBrowserDetector {
/// Detect Windows browsers visible from WSL via /mnt/c/ paths.
pub fn detect_all() -> Vec<DetectedBrowser> {
if !wsl::is_wsl() {
return Vec::new();
Expand All @@ -29,45 +30,38 @@ impl WslBrowserDetector {

let mut browsers = Vec::new();

let candidates: &[(BrowserType, PathBuf)] = &[
(
BrowserType::Chrome,
appdata_local
.join("Google")
.join("Chrome")
.join("User Data"),
),
(
BrowserType::Edge,
appdata_local
.join("Microsoft")
.join("Edge")
.join("User Data"),
),
(
BrowserType::Brave,
appdata_local
.join("BraveSoftware")
.join("Brave-Browser")
.join("User Data"),
),
(
BrowserType::Arc,
appdata_local.join("Arc").join("User Data"),
),
(
BrowserType::Chromium,
appdata_local.join("Chromium").join("User Data"),
),
];
let candidates = windows_browser_candidates(&appdata_local);
let candidates: Vec<(BrowserType, PathBuf)> = candidates
.into_iter()
.chain([
(
BrowserType::Edge,
appdata_local
.join("Microsoft")
.join("Edge")
.join("User Data"),
),
(
BrowserType::Brave,
appdata_local
.join("BraveSoftware")
.join("Brave-Browser")
.join("User Data"),
),
(
BrowserType::Arc,
appdata_local.join("Arc").join("User Data"),
),
])
.collect();

for (browser_type, user_data_dir) in candidates {
if user_data_dir.exists() {
let profiles = detect_chromium_profiles(user_data_dir);
let profiles = detect_chromium_profiles(&user_data_dir);
if !profiles.is_empty() {
browsers.push(DetectedBrowser {
browser_type: *browser_type,
user_data_dir: user_data_dir.clone(),
browser_type,
user_data_dir,
profiles,
});
}
Expand Down Expand Up @@ -95,6 +89,22 @@ impl WslBrowserDetector {
}
}

/// Collect profile roots for every browser that resolves one under the given
/// AppData/Local directory. Browsers without a shared-root mapping (Edge,
/// Brave, Arc, Firefox) are skipped here and added separately by the caller.
fn windows_browser_candidates(appdata_local: &std::path::Path) -> Vec<(BrowserType, PathBuf)> {
BrowserType::all()
.iter()
.copied()
.filter_map(|browser_type| {
browser_type
.user_data_dir_under(appdata_local)
.map(|path| (browser_type, path))
})
.collect()
}

/// Detect Chromium-based browser profiles
fn detect_chromium_profiles(user_data_dir: &PathBuf) -> Vec<BrowserProfile> {
let mut profiles = Vec::new();

Expand Down Expand Up @@ -151,6 +161,27 @@ fn detect_firefox_profiles(profiles_dir: &PathBuf) -> Vec<BrowserProfile> {
mod tests {
use super::*;

#[test]
fn windows_profile_roots_include_every_chrome_channel_and_chromium() {
let appdata = PathBuf::from("/mnt/c/Users/test/AppData/Local");
let expected = [
(BrowserType::Chrome, "Google/Chrome/User Data"),
(BrowserType::ChromeBeta, "Google/Chrome Beta/User Data"),
(BrowserType::ChromeDev, "Google/Chrome Dev/User Data"),
(BrowserType::ChromeCanary, "Google/Chrome SxS/User Data"),
(
BrowserType::ChromeForTesting,
"Google/Chrome for Testing/User Data",
),
(BrowserType::Chromium, "Chromium/User Data"),
];

let actual = windows_browser_candidates(&appdata);
for (browser_type, relative_path) in expected {
assert!(actual.contains(&(browser_type, appdata.join(relative_path))));
}
}

#[test]
fn test_wsl_browser_detection() {
let browsers = WslBrowserDetector::detect_all();
Expand Down