From bd603fb4ef8c7636f1b89a96254fd405ec716dd5 Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Thu, 9 Jul 2026 03:16:35 +0800 Subject: [PATCH 1/4] feat(multi-account): Accounts tab in Settings (Batch 2b part 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New Settings → Accounts tab: list accounts (identity + default marker), add (with log-in hint), remove, set the global default, and install/uninstall the ~/.zshrc shell integration. - main.ts: accounts-get/add/remove/set-default/shell-hook IPC handlers — thin wrappers over the shared manager (src/cli/account-manager.ts), the exact module the `codev account` CLI uses (one generator, no drift); mutations invalidate the accounts cache so the Sessions tab picks changes up - popup.tsx: the Accounts tab UI; preload.ts + electron-api.d.ts wiring - Rename deferred (labels name dirs; remove+add covers it). Remaining 2b item: a real `codev` binary on PATH. - Bump 1.0.78 + CHANGELOG + design-doc §7 tsc 0 errors; 13/13 tests. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_017kCK4t5JSVynVZxGcrTGGp --- CHANGELOG.md | 7 + docs/multi-account-support-design.md | 6 +- package.json | 2 +- src/electron-api.d.ts | 31 +++++ src/main.ts | 62 +++++++++ src/popup.tsx | 193 ++++++++++++++++++++++++++- src/preload.ts | 8 ++ 7 files changed, 303 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ed630e..102a306 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 1.0.78 + +- Feat: multi-account Batch 2b (part 1) — Accounts tab in Settings + - List accounts (identity, default marker), add (with log-in hint), remove, and set the global default — via IPC over the same shared generator the `codev account` CLI uses + - Shell-integration install/uninstall (`~/.zshrc` source block) from the UI + - Remaining 2b item: a real `codev` binary on PATH + ## 1.0.77 - Feat: multi-account Batch 2a + 2e — `codev account` CLI + configurable global-default diff --git a/docs/multi-account-support-design.md b/docs/multi-account-support-design.md index 7744f4a..5c230e6 100644 --- a/docs/multi-account-support-design.md +++ b/docs/multi-account-support-design.md @@ -433,8 +433,10 @@ and **(d)** for CodeV, backed by the same `projectMap` in the registry so both a - **Batch 2 — in progress (order 2a→2e):** - *2a — ✅ Done* (branch `feat/codev-account-cli`): `codev account` CLI generates the registry + `accounts.sh` from one shared generator (§6.A/B); Vitest + CI added. - - *2b:* manage-accounts Settings tab — list/add/rename/remove + shell-integration - toggle (§6.D), plus a real `codev` binary on PATH. + - *2b (UI ✅, branch `feat/accounts-settings-ui`):* Accounts tab in Settings — + list/add/remove + set-default + shell-integration toggle (§6.D), as IPC wrappers + over the shared manager (one generator with the CLI). Rename deferred (labels name + dirs; remove+add covers it). Remaining 2b item: a real `codev` binary on PATH. - *2c:* account picker / per-launch override (§6.G, 4.2). - *2d:* pyenv-style folder auto-switch + terminal-side resume-to-right-account (§6.H). - *2e:* configurable global-default (bare `claude` → chosen account) — last, since it diff --git a/package.json b/package.json index 0e00ef2..6c513c8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "CodeV", "productName": "CodeV", - "version": "1.0.77", + "version": "1.0.78", "description": "Quick switcher for VS Code, Cursor, and Claude Code sessions", "repository": { "type": "git", diff --git a/src/electron-api.d.ts b/src/electron-api.d.ts index f021ec9..3fa2c34 100644 --- a/src/electron-api.d.ts +++ b/src/electron-api.d.ts @@ -2,9 +2,40 @@ type IpcCallback = (event: Electron.IpcRendererEvent, ...args: any[]) => void; +/** One configured Claude Code account (codev multi-account). */ +interface CodevAccountInfo { + label: string; + dir: string; + isDefault: boolean; // the anchor ~/.claude account + isCurrentDefault: boolean; // what bare `claude` resolves to + email?: string; + org?: string; + loggedIn?: boolean; +} + interface IElectronAPI { // App actions getHomeDir: () => Promise; + // codev multi-account (Settings → Accounts) + getAccounts: () => Promise<{ + ok: boolean; + error?: string; + accounts?: CodevAccountInfo[]; + shellInstalled?: boolean; + }>; + addAccount: (label: string) => Promise<{ + ok: boolean; + error?: string; + account?: CodevAccountInfo; + }>; + removeAccount: (label: string) => Promise<{ ok: boolean; error?: string }>; + setDefaultAccount: (label: string) => Promise<{ ok: boolean; error?: string }>; + setAccountsShellHook: (action: 'install' | 'uninstall') => Promise<{ + ok: boolean; + error?: string; + changed?: boolean; + path?: string; + }>; getBannerSeen: () => Promise; setBannerSeen: () => void; invokeVSCode: (path: string, option: string) => void; diff --git a/src/main.ts b/src/main.ts index 6238ae7..0c46076 100644 --- a/src/main.ts +++ b/src/main.ts @@ -43,6 +43,8 @@ import { readVSCodeIndex, SessionStatus, } from './session-status-hooks'; +import * as accountManager from './cli/account-manager'; +import { invalidateAccountsCache } from './accounts'; import { deleteRecentProjectRecord, openVSCodeBasedIDE, @@ -625,6 +627,66 @@ ipcMain.handle('get-home-dir', () => { return require('os').homedir(); }); +// --- codev multi-account management (Settings → Accounts, Batch 2b) --- +// Thin IPC wrappers over the shared manager (src/cli/account-manager.ts), +// the same module the `codev account` CLI uses — one generator, no drift. +ipcMain.handle('accounts-get', () => { + try { + return { + ok: true, + accounts: accountManager.listAccounts(), + shellInstalled: accountManager.isShellHookInstalled(), + }; + } catch (error) { + return { ok: false, error: (error as Error).message }; + } +}); + +ipcMain.handle('accounts-add', (_event, label: string) => { + try { + const account = accountManager.addAccount(label); + invalidateAccountsCache(); + return { ok: true, account }; + } catch (error) { + return { ok: false, error: (error as Error).message }; + } +}); + +ipcMain.handle('accounts-remove', (_event, label: string) => { + try { + accountManager.removeAccount(label); + invalidateAccountsCache(); + return { ok: true }; + } catch (error) { + return { ok: false, error: (error as Error).message }; + } +}); + +ipcMain.handle('accounts-set-default', (_event, label: string) => { + try { + accountManager.setDefault(label); + invalidateAccountsCache(); + return { ok: true }; + } catch (error) { + return { ok: false, error: (error as Error).message }; + } +}); + +ipcMain.handle( + 'accounts-shell-hook', + (_event, action: 'install' | 'uninstall') => { + try { + const result = + action === 'install' + ? accountManager.installShellHook() + : accountManager.uninstallShellHook(); + return { ok: true, changed: result.changed, path: result.path }; + } catch (error) { + return { ok: false, error: (error as Error).message }; + } + }, +); + ipcMain.on('hide-app', (event) => { hideSwitcherWindow(); }); diff --git a/src/popup.tsx b/src/popup.tsx index 825e16c..8ff4956 100644 --- a/src/popup.tsx +++ b/src/popup.tsx @@ -41,6 +41,17 @@ const labelStyle: React.CSSProperties = { color: THEME.text.primary, }; +const smallButtonStyle: React.CSSProperties = { + backgroundColor: '#333', + color: THEME.text.primary, + border: '1px solid #555', + borderRadius: '4px', + padding: '3px 10px', + fontSize: '12px', + cursor: 'pointer', + outline: 'none', +}; + const PopupDefaultExample = ({ workingFolderPath, saveCallback, @@ -53,7 +64,7 @@ const PopupDefaultExample = ({ saveCallback?: (key: string, value: string) => void; openCallback?: any; switcherMode?: string; - openToTab?: 'general' | 'sessions' | 'shortcuts' | null; + openToTab?: 'general' | 'sessions' | 'accounts' | 'shortcuts' | null; onOpenToTabConsumed?: () => void; }) => { const [isOpen, setIsOpen] = useState(false); @@ -69,7 +80,7 @@ const PopupDefaultExample = ({ const [isMASBuild, setIsMASBuild] = useState(false); const [sessionStatusHooks, setSessionStatusHooks] = useState(true); const [appModeState, setAppModeState] = useState('normal'); - const [settingsTab, setSettingsTab] = useState<'general' | 'sessions' | 'shortcuts'>('general'); + const [settingsTab, setSettingsTab] = useState<'general' | 'sessions' | 'accounts' | 'shortcuts'>('general'); const [ideDataAccessGranted, setIdeDataAccessGranted] = useState(false); const [shortcuts, setShortcuts] = useState({ quickSwitcher: 'Command+Control+R', @@ -83,6 +94,100 @@ const PopupDefaultExample = ({ const [updateReleaseName, setUpdateReleaseName] = useState(''); const [updateTimer, setUpdateTimer] = useState | null>(null); + // --- Accounts tab (codev multi-account, Batch 2b) --- + type AccountRow = { + label: string; + dir: string; + isDefault: boolean; + isCurrentDefault: boolean; + email?: string; + loggedIn?: boolean; + }; + const [accounts, setAccounts] = useState([]); + const [accountsShellInstalled, setAccountsShellInstalled] = useState(false); + const [accountsError, setAccountsError] = useState(''); + const [accountsNotice, setAccountsNotice] = useState(''); + const [newAccountLabel, setNewAccountLabel] = useState(''); + + const refreshAccounts = () => { + window.electronAPI.getAccounts().then((r) => { + if (r.ok) { + setAccounts((r.accounts as AccountRow[]) || []); + setAccountsShellInstalled(!!r.shellInstalled); + } else { + setAccountsError(r.error || 'Failed to load accounts'); + } + }); + }; + + useEffect(() => { + if (isOpen && settingsTab === 'accounts') { + refreshAccounts(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isOpen, settingsTab]); + + const handleAddAccount = async () => { + const label = newAccountLabel.trim(); + if (!label) return; + setAccountsError(''); + const r = await window.electronAPI.addAccount(label); + if (r.ok) { + setNewAccountLabel(''); + setAccountsNotice( + `Added "${label}". Log in with: claude ${label} — then open a new shell (or source ~/.zshrc).`, + ); + refreshAccounts(); + } else { + setAccountsNotice(''); + setAccountsError(r.error || 'Failed to add account'); + } + }; + + const handleRemoveAccount = async (label: string) => { + setAccountsError(''); + const r = await window.electronAPI.removeAccount(label); + if (r.ok) { + setAccountsNotice(`Removed "${label}" (its folder stays on disk).`); + refreshAccounts(); + } else { + setAccountsNotice(''); + setAccountsError(r.error || 'Failed to remove account'); + } + }; + + const handleSetDefaultAccount = async (label: string) => { + setAccountsError(''); + const r = await window.electronAPI.setDefaultAccount(label); + if (r.ok) { + setAccountsNotice( + `Bare claude now resolves to "${label}" — open a new shell (or source ~/.zshrc).`, + ); + refreshAccounts(); + } else { + setAccountsNotice(''); + setAccountsError(r.error || 'Failed to set default'); + } + }; + + const handleAccountsShellToggle = async () => { + setAccountsError(''); + const r = await window.electronAPI.setAccountsShellHook( + accountsShellInstalled ? 'uninstall' : 'install', + ); + if (r.ok) { + setAccountsNotice( + accountsShellInstalled + ? 'Removed the CodeV block from ~/.zshrc (registry and account folders kept).' + : 'Added the source block to ~/.zshrc — open a new shell to use claude