Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
67eb8e4
feat(multi-account): new-session account picker (Batch 2c-lite)
grimmerk Jul 9, 2026
f7cf217
feat(multi-account): show the picker hint only for multi-account
grimmerk Jul 9, 2026
2cac70f
docs(multi-account): drop 2d (folder auto-switch)
grimmerk Jul 9, 2026
cf1c9dc
fix(multi-account): picker fixes — ghostty/cmux account, live hint
grimmerk Jul 9, 2026
74826ab
feat(multi-account): complete account labels for claude <TAB>
grimmerk Jul 9, 2026
9e40d71
fix(multi-account): keep file completion off the label position
grimmerk Jul 9, 2026
c68fb8f
docs(multi-account): README section + expand 1.0.80 changelog
grimmerk Jul 9, 2026
cf6933a
docs(multi-account): remove is non-destructive — say so everywhere
grimmerk Jul 9, 2026
d20e49f
docs(multi-account): surface the name -> config-folder mapping
grimmerk Jul 9, 2026
21825b9
docs(multi-account): footer shows both launch forms
grimmerk Jul 9, 2026
1f3de34
docs(multi-account): footer example uses a real account name
grimmerk Jul 9, 2026
c3f4064
fix(multi-account): hide the footer example when no account exists
grimmerk Jul 9, 2026
18d5e20
feat(multi-account): codev account rename <old> <new>
grimmerk Jul 9, 2026
2534fd9
docs(multi-account): common-scenarios table in README
grimmerk Jul 9, 2026
ff41698
docs(multi-account): link not-yet-supported onboarding items to #127
grimmerk Jul 9, 2026
6143f2d
fix(multi-account): rename-aware remove notice + duplicate-dir guard
grimmerk Jul 10, 2026
b7c2793
feat(multi-account): surface the name/folder model in the UI
grimmerk Jul 10, 2026
b6b199e
docs(multi-account): note 'personal' is an auto-assigned label in UI
grimmerk Jul 10, 2026
279ad4d
docs(multi-account): README — name/folder model, dup-dir guard, launc…
grimmerk Jul 10, 2026
d855f90
fix(multi-account): address cubic round on rename batch (9 findings)
grimmerk Jul 10, 2026
5fd6601
feat(multi-account): name the anchor at first add + UI rename
grimmerk Jul 10, 2026
e3ce81b
fix(multi-account): anchor-name UX consistency + doc accuracy (cubic)
grimmerk Jul 10, 2026
0434d2e
docs(multi-account): note the primary fallback in the 2c-lite entry
grimmerk Jul 10, 2026
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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Changelog

## 1.0.80

- Feat: multi-account Batch 2c-lite — pick the account when launching a new session
- `⌥⌘+Enter` on a project opens a small account picker (↑↓ / Enter / Esc, or click) — external terminals (iTerm2/Terminal.app/Ghostty/cmux); a non-default pick launches via explicit `CLAUDE_CONFIG_DIR` + `command claude`, a default-account pick via `env -u CLAUDE_CONFIG_DIR claude`
- Plain `⌘+Enter` unchanged: opens instantly under the global default; single-account (or registry-less) setups never see the picker
- Search-bar hint shows `⌥⌘+Enter: pick account` only for multi-account setups, and updates live after account changes (no app restart)
- Tab completion for `claude <name>`: `claude <TAB>` completes account names (polite: skipped if another completion owns `claude`; other positions keep file completion)
- Removed the stray `|` (react-select's default separator) before the search-bar hint
- Accounts UI: per-row **Rename**; the first-ever add asks what to name your existing `~/.claude` login (defaults to `main`) instead of assuming `personal`; `default` is now a reserved account name
- `codev account rename <old> <new>` (CLI) — labels are renameable; folders never move (credentials are keyed by the folder path)
- README gains a Multi-Account section (shell commands, `codev account` CLI, UI map)
- Roadmap: 2d (folder→account auto-switch) dropped — one folder legitimately hosts sessions from multiple accounts; cross-account *resume* stays deferred to Batch 3 (copy-fork design)

## 1.0.79

- Feat: multi-account Batch 2b (part 2) — `codev` command in your shell
Expand Down
67 changes: 67 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,73 @@ For the full same-cwd accuracy matrix (detection + switch by launch method and t
| cmux | Title match → TTY fallback | CLI new-workspace | Same as iTerm2 (requires cmux v0.63+); requires socket access in cmux Settings (`automation` or `allowAll`) |
| VS Code | URI handler (session-level) | `open -b` + URI handler | Requires Claude Code VS Code extension v2.1.72+; `[VSCODE]` badge on active sessions; adaptive resume via IDE lock file polling (~0.5s if project already open) |

### Multi-Account Support (Claude Code)

Run multiple Claude Code accounts (e.g. personal + work) on one machine. Each account gets its own config dir (via `CLAUDE_CONFIG_DIR`); the default account stays at `~/.claude` untouched. The Sessions tab aggregates sessions from every account (non-default ones get a purple account badge), and each session always resumes under the account it belongs to.

**Setup** — Settings → Accounts:

1. **Add** an account (e.g. `work`) — writes the registry (`~/.config/codev/accounts.json`) and generates `~/.config/codev/accounts.sh`
2. **Install** Shell integration — appends one marker-guarded `source` line to `~/.zshrc` (zsh; the generated file is bash-sourceable manually, fish unsupported)
3. Open a new shell and log in: `claude work`

**Shell commands** (from the generated `accounts.sh`):

| Command | What it does |
|---------|--------------|
| `claude` | Launches the **global default** account (configurable, see below) |
| `claude <name> […]` | Launches that account, full passthrough (e.g. `claude work -r`) |
| `claude-<name> […]` | Function form of the same (e.g. `claude-work mcp list`) |
| `claude-whoami` | Which account bare `claude` resolves to here, plus its auth status |
| `claude-accounts` | Quick list of configured accounts |

Tab completion included (zsh): `claude <TAB>` completes account names; `codev account <TAB>` completes subcommands and labels. (The `claude` completion is registered only when nothing else completes `claude` — if you already have one, account names won't be injected into it.)

The **default account** can be launched three equivalent ways: bare `claude`, `claude <its-name>`, or `claude-<its-name>` (the UI's row shows this).

**`codev account` CLI** (the account manager — same generator the Settings UI uses):

| Command | What it does |
|---------|--------------|
| `codev account list` (`ls`) | Accounts + identity; `*` marks the bare-`claude` default |
| `codev account add <name> [--dir D]` | Register an account (default dir `~/.claude-<name>`) |
| `codev account default <name>` | Point bare `claude` at an account |
| `codev account remove <name>` (`rm`) | Unregister; keeps its folder on disk |
| `codev account rename <old> <new>` | Rename a label — the folder is not moved; also the way to rename the auto-seeded default account (`personal`) |
| `codev account regenerate` (`regen`) | Rewrite `accounts.sh` from the registry |
| `codev account show` (`preview`) | Dry-run print of the generated `accounts.sh` |
| `codev account install` / `uninstall` | Add / remove the `~/.zshrc` source block |

> **Add is just a mapping.** `add <name>` registers *name → config folder* (default `~/.claude-<name>` — that folder **is** the account's `CLAUDE_CONFIG_DIR`). Claude Code itself creates and fills the folder on first login (`claude <name>`). One folder = one account: registering an already-registered folder under a second name is rejected.
>
> **Name vs folder.** The name is a mutable label; the folder is fixed at creation. They can diverge (after a rename) and the UI shows `· folder: …` on the row when they do. Folders deliberately never move on rename: Claude Code keys the account's credentials by the config-dir **path**, so moving the folder would orphan the login.
>
> **Remove is non-destructive.** It only unregisters that mapping: the account's folder, login, and session history all stay on disk. `codev account add <name>` (or the UI) later reattaches everything — identity and sessions reappear immediately, no re-login needed. **Caveat after a rename:** the folder keeps its *original* suffix (rename never moves folders), so re-attach with the folder-matching name — e.g. an account created as `work`, renamed to `ff`, then removed, re-attaches via `add work` (or any name with `--dir ~/.claude-work`), **not** `add ff`. Likewise, an account originally registered with a custom `--dir` always needs `--dir <original-folder>` again on re-add.

**Common scenarios:**

| You are… | What happens / what to do |
|----------|---------------------------|
| An existing Claude Code user opening CodeV for the first time | Accounts tab is **empty** (zero footprint — no registry is created until you act); your `~/.claude` login keeps working as before. When you add a second account, **you name your existing login at the same time** (a field appears; empty = `main`) and both get registered together. |
| Renaming any account later | The **Rename** button on each row, or `codev account rename <old> <new>`. To pre-name your existing default before adding anything: `codev account add <name> --dir ~/.claude` (registers the default itself; no extra account created). `default` is a reserved name. |
| Starting fresh: add first, log in later | Add a name (UI or CLI) → it shows "not logged in" → in a new shell, `claude <name>` runs Claude Code's login and creates the folder. |
| Already running a second account by hand (own shell function + custom folder) | Register it with `codev account add <name> --dir <your-folder>` — **any folder works**; `~/.claude-<name>` is only the default. Identity and sessions attach immediately, no re-login. If your hand-rolled wrapper was named `claude`, retire it (it would fight the generated dispatcher). |
| Wondering why a name maps to a different folder | Renames change only the name side of the *name → folder* mapping; folders never move (credentials are keyed by the folder path) — the UI shows `· folder: …` on the row when they diverge. |

> Not yet supported: auto-detecting existing config folders for one-click registration, and a warning when `accounts.sh` overrides a hand-rolled `claude()` shell function ([#127](https://github.com/grimmerk/codev/issues/127)); continuing an existing conversation under a *different* account — "copy-fork" — needs transcript-level workarounds first ([#128](https://github.com/grimmerk/codev/issues/128)).

The `codev` command runs the CLI **bundled inside CodeV.app** (`ELECTRON_RUN_AS_NODE`) — no system Node, no sudo, no PATH edits. CodeV refreshes `accounts.sh` on every launch, so moving or renaming the app self-heals. (Not available in MAS builds — sandboxed.)

**In the CodeV UI:**

| Where | What |
|-------|------|
| Settings → Accounts | List/add/remove accounts, set the global default, install shell integration |
| Sessions tab | Sessions from all accounts with account badges; resume uses each session's own account |
Comment thread
grimmerk marked this conversation as resolved.
| Projects tab: `⌥⌘+Enter` | Pick the account for a new session (`⌘+Enter` stays instant, under the global default). Account override applies to external terminals (iTerm2, Terminal.app, Ghostty, cmux); VS Code ([#121](https://github.com/grimmerk/codev/issues/121)) and the embedded Term tab ignore it |

**Gotcha:** inside a Claude Code session, `!claude auth status` reports the *global default* (the shell snapshot carries the dispatcher function), not the session's account — use `!command claude auth status` instead. Full design + details: [docs/multi-account-support-design.md](docs/multi-account-support-design.md).

### Embedded Terminal

CodeV includes a built-in terminal tab (powered by xterm.js + node-pty, same technology as VS Code's integrated terminal). Press `⌃+⌘+T` from anywhere (global shortcut) or `⌘+3` when CodeV is in foreground to open it.
Expand Down
35 changes: 29 additions & 6 deletions docs/multi-account-support-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -453,18 +453,41 @@ and **(d)** for CodeV, backed by the same `projectMap` in the registry so both a
registry + `accounts.sh` from one shared generator (§6.A/B); Vitest + CI added.
- *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).
over the shared manager (one generator with the CLI). (Rename was deferred here,
then shipped in 2c-lite below — CLI + per-row UI.)
- *2b part 2 (✅, branch `feat/codev-path-binary`):* the `codev` command — a
`codev()` function in accounts.sh runs the CLI bundled inside CodeV.app via
`ELECTRON_RUN_AS_NODE` (no system Node; no sudo/PATH edits). The app records its
real `.app` location in the registry (`appPath`) on every launch and refreshes
accounts.sh, so app moves and generator updates self-heal. CLI is tsc-compiled
into `resources/cli` (extraResource) at build time.
- *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
also needs the CLI/UI to manage.
- *2c-lite (✅, branch `feat/new-session-account-picker`):* NEW-session account
picker — ⌥⌘+Enter on a project opens a picker (⌘+Enter unchanged → global
Comment thread
grimmerk marked this conversation as resolved.
default; single-account setups never see it); launches with explicit
`CLAUDE_CONFIG_DIR` + `command claude` (an anchor-account pick uses
`env -u CLAUDE_CONFIG_DIR claude` instead — §3.4). Post-review naming UX: per-row UI Rename +
`codev account rename` (label-only — folders never move; credentials are keyed by
Comment thread
grimmerk marked this conversation as resolved.
the config-dir path); the first-ever add asks what to name the existing ~/.claude
login (defaults to `main` — or `primary` when the new account itself is named
`main` — replacing the presumptuous auto-`personal`); `default` is
a reserved label (it names the dispatcher-target concept). Cross-account *resume*
(the other half of §6.G) stays deferred to Batch 3 as explicit copy-fork — a transcript lives in
its own account's projects dir, so "resume under another account" must copy, never
link. "This repo always opens under X" belongs to 2d's projectMap.
- *2d — ❌ dropped (2026-07-10):* pyenv-style folder auto-switch (§6.H). Rationale:
in practice one folder hosts sessions from MULTIPLE accounts (we test exactly this
way), so a folder→account default would guess wrong precisely where the account
choice matters; stacking a third decision layer (global default → folder default →
picker) adds "why did bare claude open work here?" confusion for thin value. The
need is covered by 2e (global default) + 2c-lite (explicit per-launch picker).
Technically it was feasible (a default, not a constraint — resume always follows
the session's own account); dropped on value, not feasibility.
- *2e — ✅ shipped with 2a (PR #123, v1.0.77):* configurable global-default —
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
`codev account default <label>` / Settings → Set default routes bare `claude`
through the dispatcher `*)` to the chosen account. CodeV bypasses the dispatcher
only for *account-explicit* operations (resuming a session under its own account,
picker launches) via `command claude` / explicit env; the plain ⌘+Enter new-session
path deliberately stays on bare `claude` so the dispatcher's global default applies.
- **Batch 3 — later:** cross-account symlink reuse of global files
(CLAUDE.md / skills / commands / plugins / settings, §5).

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.79",
"version": "1.0.80",
"description": "Quick switcher for VS Code, Cursor, and Claude Code sessions",
"repository": {
"type": "git",
Expand Down
42 changes: 40 additions & 2 deletions src/claude-session-utility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import * as path from 'path';
import * as os from 'os';
import * as fs from 'fs';
import { getCurrentIDEBundleId } from './vscode-based-ide-utility';
import { getScannableAccounts, getProjectsDir } from './accounts';
import {
getScannableAccounts,
getProjectsDir,
getAccountByLabel,
} from './accounts';

export interface ClaudeSession {
sessionId: string;
Expand Down Expand Up @@ -1389,6 +1393,7 @@ export const launchNewClaudeSession = (
projectPath: string,
terminalApp: string = 'iterm2',
terminalMode: string = 'tab',
accountLabel?: string,
): void => {
if (terminalApp === 'vscode') {
const { execFile } = require('child_process');
Expand All @@ -1411,12 +1416,45 @@ export const launchNewClaudeSession = (
return;
}
if (terminalApp === 'codev') {
// Embedded terminal spawns the user's shell; the account override isn't
// threaded through it yet (the 2c-lite picker targets external terminals).
if (accountLabel) {
console.log(
'[launchNewClaudeSession] account override ignored for codev terminal:',
accountLabel,
);
}
if (launchInCodevTerminalCallback) {
launchInCodevTerminalCallback(projectPath);
}
return;
}
runCommandInTerminal(`cd "${projectPath}" && claude`, 'claude', projectPath, terminalApp, terminalMode);
// No accountLabel: bare `claude` → the accounts.sh dispatcher → the user's
// global default (2e). With an explicit account (2c-lite picker): bypass the
// dispatcher via `command claude` + explicit env, like buildResumeCommand.
let claudeCmd = 'claude';
if (accountLabel) {
const account = getAccountByLabel(accountLabel);
if (account?.label !== accountLabel) {
// Stale/unknown label (e.g. the account was removed after the picker
// loaded) — getAccountByLabel falls back to the default account, so
// abort instead of silently launching a different identity.
console.error(
'[launchNewClaudeSession] unknown account label, aborting:',
accountLabel,
);
return;
}
// Explicit pick of the default account: clear any inherited
// CLAUDE_CONFIG_DIR too (matches the generated accounts.sh launchers).
claudeCmd = account.configDirEnv
? `CLAUDE_CONFIG_DIR='${account.configDirEnv.replace(/'/g, "'\\''")}' command claude`
: 'env -u CLAUDE_CONFIG_DIR claude';
}
// Pass claudeCmd as the 2nd arg too — Ghostty/cmux build their launch
// scripts from it (iTerm2/Terminal.app use fullCommand), so the account
// env prefix must be present in both.
runCommandInTerminal(`cd "${projectPath}" && ${claudeCmd}`, claudeCmd, projectPath, terminalApp, terminalMode);
Comment thread
grimmerk marked this conversation as resolved.
};

/**
Expand Down
15 changes: 13 additions & 2 deletions src/cli/account-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,24 @@ describe('generateAccountsSh', () => {
expect(sh).toContain('_codev() {');
expect(sh).toContain('compdef _codev codev');
expect(sh).toContain(
'compadd list add default remove rm regenerate show install uninstall help',
'compadd list add default remove rm rename regenerate show install uninstall help',
);
expect(sh).toContain('default) compadd personal work ;;');
expect(sh).toContain('default|rename) compadd personal work ;;');
// anchor (personal) is not removable
expect(sh).toContain('remove|rm) compadd work ;;');
});

it('completes account labels for `claude <TAB>` without clobbering', () => {
// Works even without appPath — the dispatcher exists whenever accounts do.
const sh = generateAccountsSh(reg());
expect(sh).toContain('_claude_codev_accounts() {');
expect(sh).toContain('compadd personal work');
// polite registration: only when nothing else completes `claude`
expect(sh).toContain('[ -z "${_comps[claude]:-}" ]');
// non-label positions keep the default file completion
expect(sh).toContain('_files');
});

it('uses the recorded appExec so a renamed .app bundle still works', () => {
const sh = generateAccountsSh(
reg({
Expand Down
Loading
Loading