From 8c2b2574b17da11c66af9b74a3d720f29db0ca8a Mon Sep 17 00:00:00 2001 From: Nick Gurney Date: Mon, 16 Mar 2026 22:57:35 -0600 Subject: [PATCH 1/2] Add profile picker to cookie import UI Chromium-based browsers support multiple profiles, each with its own cookie store. The previous implementation always read from the 'Default' profile, making it impossible to import cookies from non-default profiles (e.g. a work or freelance profile in Arc or Chrome). Changes: - cookie-import-browser.ts: add `listProfiles()` which scans a browser's data directory for profile subdirs (Default, Profile N) that have a Cookies file, reads the display name from each profile's Preferences JSON, and returns `ProfileInfo[]` sorted Default-first - cookie-picker-routes.ts: expose GET /cookie-picker/profiles?browser= endpoint; thread an optional `profile` query/body param through the /domains and /import routes - cookie-picker-ui.ts: after selecting a browser, load its profiles and render a row of monospace profile pills (hidden when only one profile exists); switching profiles reloads the domain list; the selected profile id is sent with import requests - write-commands.ts: support --profile flag in direct CLI import mode (cookie-import-browser --domain --profile "Profile 1") Co-Authored-By: Claude Sonnet 4.6 --- browse/src/cookie-import-browser.ts | 53 ++++++++++++++++++ browse/src/cookie-picker-routes.ts | 19 +++++-- browse/src/cookie-picker-ui.ts | 85 +++++++++++++++++++++++++++-- browse/src/write-commands.ts | 4 +- 4 files changed, 151 insertions(+), 10 deletions(-) diff --git a/browse/src/cookie-import-browser.ts b/browse/src/cookie-import-browser.ts index 29d9db3e..a39ca26e 100644 --- a/browse/src/cookie-import-browser.ts +++ b/browse/src/cookie-import-browser.ts @@ -52,6 +52,11 @@ export interface DomainEntry { count: number; } +export interface ProfileInfo { + id: string; // directory name: 'Default', 'Profile 1', etc. + name: string; // display name from Preferences, falls back to id +} + export interface ImportResult { cookies: PlaywrightCookie[]; count: number; @@ -111,6 +116,54 @@ export function findInstalledBrowsers(): BrowserInfo[] { }); } +/** + * List available profiles for a browser (subdirs with a Cookies file). + * Returns ProfileInfo[] sorted Default-first, then Profile N numerically. + * Display names are read from Preferences JSON; fall back to dir name. + */ +export function listProfiles(browserName: string): ProfileInfo[] { + const browser = resolveBrowser(browserName); + const appSupport = path.join(os.homedir(), 'Library', 'Application Support'); + const dataDir = path.join(appSupport, browser.dataDir); + const profiles: ProfileInfo[] = []; + + try { + const entries = fs.readdirSync(dataDir, { withFileTypes: true }); + for (const entry of entries) { + if (!entry.isDirectory()) continue; + const id = entry.name; + if (id !== 'Default' && !id.match(/^Profile \d+$/)) continue; + const cookiePath = path.join(dataDir, id, 'Cookies'); + if (!fs.existsSync(cookiePath)) continue; + const name = readProfileName(path.join(dataDir, id)) || id; + profiles.push({ id, name }); + } + } catch { + return [{ id: 'Default', name: 'Default' }]; + } + + profiles.sort((a, b) => { + if (a.id === 'Default') return -1; + if (b.id === 'Default') return 1; + const nA = parseInt(a.id.replace('Profile ', ''), 10); + const nB = parseInt(b.id.replace('Profile ', ''), 10); + return nA - nB; + }); + + return profiles.length > 0 ? profiles : [{ id: 'Default', name: 'Default' }]; +} + +function readProfileName(profileDir: string): string | null { + try { + const prefsPath = path.join(profileDir, 'Preferences'); + const raw = fs.readFileSync(prefsPath, 'utf-8'); + const prefs = JSON.parse(raw); + return prefs?.profile?.name ?? null; + } catch { + return null; + } +} + /** * List unique cookie domains + counts from a browser's DB. No decryption. */ diff --git a/browse/src/cookie-picker-routes.ts b/browse/src/cookie-picker-routes.ts index 6a4a4319..9cbce5cb 100644 --- a/browse/src/cookie-picker-routes.ts +++ b/browse/src/cookie-picker-routes.ts @@ -14,7 +14,7 @@ */ import type { BrowserManager } from './browser-manager'; -import { findInstalledBrowsers, listDomains, importCookies, CookieImportError, type PlaywrightCookie } from './cookie-import-browser'; +import { findInstalledBrowsers, listDomains, listProfiles, importCookies, CookieImportError, type PlaywrightCookie, type ProfileInfo } from './cookie-import-browser'; import { getCookiePickerHTML } from './cookie-picker-ui'; // ─── State ────────────────────────────────────────────────────── @@ -90,13 +90,24 @@ export async function handleCookiePickerRoute( }, { port }); } + // GET /cookie-picker/profiles?browser= — list profiles for a browser + if (pathname === '/cookie-picker/profiles' && req.method === 'GET') { + const browserName = url.searchParams.get('browser'); + if (!browserName) { + return errorResponse("Missing 'browser' parameter", 'missing_param', { port }); + } + const profiles: ProfileInfo[] = listProfiles(browserName); + return jsonResponse({ profiles }, { port }); + } + // GET /cookie-picker/domains?browser= — list domains + counts if (pathname === '/cookie-picker/domains' && req.method === 'GET') { const browserName = url.searchParams.get('browser'); if (!browserName) { return errorResponse("Missing 'browser' parameter", 'missing_param', { port }); } - const result = listDomains(browserName); + const profile = url.searchParams.get('profile') || 'Default'; + const result = listDomains(browserName, profile); return jsonResponse({ browser: result.browser, domains: result.domains, @@ -112,14 +123,14 @@ export async function handleCookiePickerRoute( return errorResponse('Invalid JSON body', 'bad_request', { port }); } - const { browser, domains } = body; + const { browser, domains, profile: importProfile } = body; if (!browser) return errorResponse("Missing 'browser' field", 'missing_param', { port }); if (!domains || !Array.isArray(domains) || domains.length === 0) { return errorResponse("Missing or empty 'domains' array", 'missing_param', { port }); } // Decrypt cookies from the browser DB - const result = await importCookies(browser, domains); + const result = await importCookies(browser, domains, importProfile || 'Default'); if (result.cookies.length === 0) { return jsonResponse({ diff --git a/browse/src/cookie-picker-ui.ts b/browse/src/cookie-picker-ui.ts index 010c2dd7..ff0fd64d 100644 --- a/browse/src/cookie-picker-ui.ts +++ b/browse/src/cookie-picker-ui.ts @@ -101,6 +101,32 @@ export function getCookiePickerHTML(serverPort: number): string { background: #4ade80; } + /* ─── Profile Pills ──────────────────── */ + .profile-pills { + display: flex; + gap: 6px; + padding: 0 20px 12px; + flex-wrap: wrap; + } + .profile-pill { + padding: 4px 10px; + border-radius: 5px; + border: 1px solid #2a2a2a; + background: #141414; + color: #888; + font-size: 11px; + font-family: 'SF Mono', 'Fira Code', monospace; + cursor: pointer; + transition: all 0.15s; + } + .profile-pill:hover { border-color: #444; color: #ccc; } + .profile-pill.active { + border-color: #60a5fa; + background: #0a1828; + color: #60a5fa; + } + .profile-pills:empty { display: none; } + /* ─── Search ──────────────────────────── */ .search-wrap { padding: 0 20px 12px; @@ -268,6 +294,7 @@ export function getCookiePickerHTML(serverPort: number): string {
Source Browser
+
@@ -291,11 +318,13 @@ export function getCookiePickerHTML(serverPort: number): string { (function() { const BASE = '${baseUrl}'; let activeBrowser = null; + let activeProfile = 'Default'; let allDomains = []; let importedSet = {}; // domain → count let inflight = {}; // domain → true (prevents double-click) const $pills = document.getElementById('browser-pills'); + const $profilePills = document.getElementById('profile-pills'); const $search = document.getElementById('search'); const $sourceDomains = document.getElementById('source-domains'); const $importedDomains = document.getElementById('imported-domains'); @@ -364,6 +393,7 @@ export function getCookiePickerHTML(serverPort: number): string { browsers.forEach(b => { const pill = document.createElement('button'); pill.className = 'pill'; + pill.dataset.browser = b.name; pill.innerHTML = '' + escHtml(b.name); pill.onclick = () => selectBrowser(b.name); $pills.appendChild(pill); @@ -380,22 +410,67 @@ export function getCookiePickerHTML(serverPort: number): string { // ─── Select Browser ──────────────────── async function selectBrowser(name) { activeBrowser = name; + activeProfile = 'Default'; - // Update pills + // Update browser pills $pills.querySelectorAll('.pill').forEach(p => { - p.classList.toggle('active', p.textContent === name); + p.classList.toggle('active', p.dataset.browser === name); }); + $profilePills.innerHTML = ''; + $sourceDomains.innerHTML = '
Loading profiles...
'; + $sourceFooter.textContent = ''; + $search.value = ''; + + try { + const profileData = await api('/profiles?browser=' + encodeURIComponent(name)); + const profiles = profileData.profiles || ['Default']; + renderProfilePills(name, profiles); + await loadDomains(name, (profiles[0] && (profiles[0].id || profiles[0])) || 'Default'); + } catch (err) { + showBanner(err.message, 'error', err.action === 'retry' ? () => selectBrowser(name) : null); + $sourceDomains.innerHTML = '
Failed to load profiles
'; + } + } + + // ─── Render Profile Pills ────────────── + function renderProfilePills(browser, profiles) { + if (profiles.length <= 1) { + $profilePills.innerHTML = ''; + return; + } + $profilePills.innerHTML = ''; + profiles.forEach(p => { + const id = p.id || p; + const label = p.name || p; + const btn = document.createElement('button'); + btn.className = 'profile-pill' + (id === activeProfile ? ' active' : ''); + btn.textContent = label; + btn.dataset.profileId = id; + btn.onclick = () => { + activeProfile = id; + $profilePills.querySelectorAll('.profile-pill').forEach(el => { + el.classList.toggle('active', el.dataset.profileId === id); + }); + loadDomains(browser, id); + }; + $profilePills.appendChild(btn); + }); + } + + // ─── Load Domains for Profile ────────── + async function loadDomains(browser, profile) { + activeProfile = profile; $sourceDomains.innerHTML = '
Loading domains...
'; $sourceFooter.textContent = ''; $search.value = ''; try { - const data = await api('/domains?browser=' + encodeURIComponent(name)); + const data = await api('/domains?browser=' + encodeURIComponent(browser) + '&profile=' + encodeURIComponent(profile)); allDomains = data.domains; renderSourceDomains(); } catch (err) { - showBanner(err.message, 'error', err.action === 'retry' ? () => selectBrowser(name) : null); + showBanner(err.message, 'error', err.action === 'retry' ? () => loadDomains(browser, profile) : null); $sourceDomains.innerHTML = '
Failed to load domains
'; } } @@ -453,7 +528,7 @@ export function getCookiePickerHTML(serverPort: number): string { const data = await api('/import', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ browser: activeBrowser, domains: [domain] }), + body: JSON.stringify({ browser: activeBrowser, domains: [domain], profile: activeProfile }), }); if (data.domainCounts) { diff --git a/browse/src/write-commands.ts b/browse/src/write-commands.ts index 2b384920..4238c909 100644 --- a/browse/src/write-commands.ts +++ b/browse/src/write-commands.ts @@ -315,7 +315,9 @@ export async function handleWriteCommand( // Direct import mode — no UI const domain = args[domainIdx + 1]; const browser = browserArg || 'comet'; - const result = await importCookies(browser, [domain]); + const profileIdx = args.indexOf('--profile'); + const directProfile = profileIdx !== -1 && profileIdx + 1 < args.length ? args[profileIdx + 1] : 'Default'; + const result = await importCookies(browser, [domain], directProfile); if (result.cookies.length > 0) { await page.context().addCookies(result.cookies); } From 8018919b1103b03213e20947a6f9700a6b804035 Mon Sep 17 00:00:00 2001 From: Nick Gurney Date: Mon, 16 Mar 2026 23:01:30 -0600 Subject: [PATCH 2/2] Use account email as profile display name when available For Chromium profiles signed into a Google account, prefer account_info[0].email over the generic profile.name (e.g. 'Person 2'). The email is more distinctive when a user has multiple profiles signed into different accounts. Falls back to profile.name, then directory id. Co-Authored-By: Claude Sonnet 4.6 --- browse/src/cookie-import-browser.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/browse/src/cookie-import-browser.ts b/browse/src/cookie-import-browser.ts index a39ca26e..a0f35fc0 100644 --- a/browse/src/cookie-import-browser.ts +++ b/browse/src/cookie-import-browser.ts @@ -158,6 +158,10 @@ function readProfileName(profileDir: string): string | null { const prefsPath = path.join(profileDir, 'Preferences'); const raw = fs.readFileSync(prefsPath, 'utf-8'); const prefs = JSON.parse(raw); + // Prefer signed-in account email (most distinctive for multi-account setups) + const email = prefs?.account_info?.[0]?.email; + if (email) return email; + // Fall back to profile display name return prefs?.profile?.name ?? null; } catch { return null;