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
17 changes: 2 additions & 15 deletions apps/desktop-tauri/src-tauri/src/commands/browser_import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub fn list_detected_browsers() -> Vec<DetectedBrowserBridge> {
BrowserDetector::detect_all()
.into_iter()
.map(|b| DetectedBrowserBridge {
browser_type: browser_type_key(b.browser_type).to_string(),
browser_type: b.browser_type.key().to_string(),
display_name: b.browser_type.display_name().to_string(),
profile_count: b.profiles.len(),
})
Expand Down Expand Up @@ -62,7 +62,7 @@ pub fn import_browser_cookies(
let browsers = BrowserDetector::detect_all();
let browser = browsers
.into_iter()
.find(|b| browser_type_key(b.browser_type) == browser_type.as_str())
.find(|b| b.browser_type.key() == browser_type.as_str())
.ok_or_else(|| format!("Browser '{browser_type}' not found or not installed"))?;

// Extract the cookie header.
Expand All @@ -86,16 +86,3 @@ pub fn import_browser_cookies(

Ok(get_manual_cookies())
}

/// Map `BrowserType` to a stable lowercase string key used in the IPC bridge.
fn browser_type_key(bt: codexbar::browser::detection::BrowserType) -> &'static str {
use codexbar::browser::detection::BrowserType;
match bt {
BrowserType::Chrome => "chrome",
BrowserType::Edge => "edge",
BrowserType::Brave => "brave",
BrowserType::Arc => "arc",
BrowserType::Firefox => "firefox",
BrowserType::Chromium => "chromium",
}
}
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
203 changes: 155 additions & 48 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,52 @@ 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",
}
}

/// Stable browser identifier shared by the core and desktop IPC bridge.
pub fn key(&self) -> &'static str {
match self {
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",
BrowserType::Firefox => "firefox",
BrowserType::Chromium => "chromium",
}
}

/// Resolve a profile root under Windows AppData. Firefox requires Roaming;
/// local-root browsers remain resolvable when Roaming is unavailable.
pub fn user_data_dir(&self, local: &Path, roaming: Option<&Path>) -> Option<PathBuf> {
match self {
BrowserType::Chrome => Some(local.join("Google/Chrome/User Data")),
BrowserType::ChromeBeta => Some(local.join("Google/Chrome Beta/User Data")),
BrowserType::ChromeDev => Some(local.join("Google/Chrome Dev/User Data")),
BrowserType::ChromeCanary => Some(local.join("Google/Chrome SxS/User Data")),
BrowserType::ChromeForTesting => {
Some(local.join("Google/Chrome for Testing/User Data"))
}
BrowserType::Edge => Some(local.join("Microsoft/Edge/User Data")),
BrowserType::Brave => Some(local.join("BraveSoftware/Brave-Browser/User Data")),
BrowserType::Arc => Some(local.join("Arc/User Data")),
BrowserType::Firefox => roaming.map(|root| root.join("Mozilla/Firefox/Profiles")),
BrowserType::Chromium => Some(local.join("Chromium/User Data")),
}
}
}

/// A detected browser installation
Expand Down Expand Up @@ -117,7 +164,23 @@ impl BrowserDetector {
/// Detect a specific browser
pub fn detect(browser_type: BrowserType) -> Option<DetectedBrowser> {
let user_data_dir = Self::get_user_data_dir(browser_type)?;
Self::detect_at_path(browser_type, user_data_dir)
}

/// Detect from explicit AppData roots, including WSL-mounted Windows roots.
pub(super) fn detect_in_roots(
browser_type: BrowserType,
local: &Path,
roaming: Option<&Path>,
) -> Option<DetectedBrowser> {
let user_data_dir = browser_type.user_data_dir(local, roaming)?;
Self::detect_at_path(browser_type, user_data_dir)
}

fn detect_at_path(
browser_type: BrowserType,
user_data_dir: PathBuf,
) -> Option<DetectedBrowser> {
if !user_data_dir.exists() {
return None;
}
Expand All @@ -141,59 +204,17 @@ impl BrowserDetector {
if wsl::is_wsl()
&& 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::Edge => Some(
appdata_local
.join("Microsoft")
.join("Edge")
.join("User Data"),
),
BrowserType::Brave => Some(
appdata_local
.join("BraveSoftware")
.join("Brave-Browser")
.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")),
};
if let Some(ref p) = path
&& p.exists()
let roaming = wsl::windows_appdata_roaming();
if let Some(path) = browser_type.user_data_dir(&appdata_local, roaming.as_deref())
&& path.exists()
{
return path;
return Some(path);
}
}

let local_app_data = dirs::data_local_dir()?;
let app_data = dirs::data_dir()?;

let path = match browser_type {
BrowserType::Chrome => local_app_data
.join("Google")
.join("Chrome")
.join("User Data"),
BrowserType::Edge => local_app_data
.join("Microsoft")
.join("Edge")
.join("User Data"),
BrowserType::Brave => local_app_data
.join("BraveSoftware")
.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"),
};

Some(path)
let app_data = dirs::data_dir();
browser_type.user_data_dir(&local_app_data, app_data.as_deref())
}

/// Detect profiles within a browser's user data directory
Expand Down Expand Up @@ -268,6 +289,92 @@ impl BrowserDetector {
mod tests {
use super::*;

#[test]
fn browser_paths_and_keys_cover_every_type() {
let local = Path::new("C:/Users/test/AppData/Local");
let roaming = Path::new("C:/Users/test/AppData/Roaming");
let expected = [
(
BrowserType::Chrome,
"Google/Chrome/User Data",
"Google Chrome",
"chrome",
),
(
BrowserType::ChromeBeta,
"Google/Chrome Beta/User Data",
"Google Chrome Beta",
"chrome-beta",
),
(
BrowserType::ChromeDev,
"Google/Chrome Dev/User Data",
"Google Chrome Dev",
"chrome-dev",
),
(
BrowserType::ChromeCanary,
"Google/Chrome SxS/User Data",
"Google Chrome Canary",
"chrome-canary",
),
(
BrowserType::ChromeForTesting,
"Google/Chrome for Testing/User Data",
"Chrome for Testing",
"chrome-for-testing",
),
(
BrowserType::Edge,
"Microsoft/Edge/User Data",
"Microsoft Edge",
"edge",
),
(
BrowserType::Brave,
"BraveSoftware/Brave-Browser/User Data",
"Brave",
"brave",
),
(BrowserType::Arc, "Arc/User Data", "Arc", "arc"),
(
BrowserType::Firefox,
"Mozilla/Firefox/Profiles",
"Firefox",
"firefox",
),
(
BrowserType::Chromium,
"Chromium/User Data",
"Chromium",
"chromium",
),
];

assert_eq!(BrowserType::all().len(), expected.len());
let mut keys = std::collections::HashSet::new();
for (browser, relative_path, display_name, key) in expected {
let root = if browser == BrowserType::Firefox {
roaming
} else {
local
};
assert_eq!(
browser.user_data_dir(local, Some(roaming)),
Some(root.join(relative_path)),
"wrong profile root for {display_name}"
);
assert_eq!(browser.display_name(), display_name);
assert_eq!(browser.key(), key);
assert!(keys.insert(browser.key()), "duplicate IPC key: {key}");
assert!(BrowserType::all().contains(&browser));
assert_eq!(
browser.user_data_dir(local, None),
(browser != BrowserType::Firefox).then(|| local.join(relative_path))
);
}
}

#[test]
fn test_browser_detection() {
let browsers = BrowserDetector::detect_all();
Expand Down
Loading