Skip to content
Merged
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 4 additions & 2 deletions docs/multi-account-support-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
17 changes: 17 additions & 0 deletions src/cli/account-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,23 @@ export function addAccount(
);
assertSafeDir(dir);
const isDefault = isDefaultDir(dir);
// Fresh registry + adding an EXTRA account: seed the anchor (~/.claude)
// account first, so the machine's existing default install stays registered
// and keeps the global default (instead of it silently moving to a
// brand-new, not-yet-logged-in account).
if (reg.accounts.length === 0 && !isDefault) {
const anchorLabel = label === 'personal' ? 'default' : 'personal';
const anchorIdentity = path.join(os.homedir(), '.claude.json');
reg.accounts.push({
label: anchorLabel,
dir: defaultDir(),
identityFile: anchorIdentity,
configDirEnv: null,
isDefault: true,
...readIdentity(anchorIdentity),
});
reg.defaultAccount = anchorLabel;
}
const identityFile = isDefault
? path.join(os.homedir(), '.claude.json')
: path.join(dir, '.claude.json');
Expand Down
31 changes: 31 additions & 0 deletions src/electron-api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>;
// 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;
}>;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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<boolean>;
setBannerSeen: () => void;
invokeVSCode: (path: string, option: string) => void;
Expand Down
80 changes: 80 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -625,6 +627,84 @@ 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 added = accountManager.addAccount(label);
invalidateAccountsCache();
// Return the enriched (listed) shape so it matches CodevAccountInfo
// (isCurrentDefault etc.), not the raw registry entry. The fallback
// derives isCurrentDefault too, keeping the IPC contract intact.
const account = accountManager
.listAccounts()
.find((a) => a.label === added.label) || {
...added,
isCurrentDefault:
accountManager.resolveDefaultLabel(accountManager.readRegistry()) ===
added.label,
};
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 {
// Validate: a malformed payload must fail, not silently uninstall.
if (action !== 'install' && action !== 'uninstall') {
return {
ok: false,
error: `Unknown shell-hook action "${String(action)}"`,
};
}
const result =
action === 'install'
? accountManager.installShellHook()
: accountManager.uninstallShellHook();
Comment thread
grimmerk marked this conversation as resolved.
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();
});
Expand Down
Loading
Loading