-
Notifications
You must be signed in to change notification settings - Fork 0
feat(multi-account): new-session account picker + claude tab completion (Batch 2c-lite) #126
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
67eb8e4
f7cf217
2cac70f
cf1c9dc
74826ab
9e40d71
c68fb8f
cf6933a
d20e49f
21825b9
1f3de34
c3f4064
18d5e20
2534fd9
ff41698
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -41,6 +41,69 @@ 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. | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The README should note that Prompt for AI agents
Suggested change
|
||||||
|
|
||||||
| **`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>`). | ||||||
| > | ||||||
| > **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. | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The docs claim that Prompt for AI agents |
||||||
|
|
||||||
| **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. Add a second account and your existing login is auto-registered as `personal` alongside it. | | ||||||
| | Unhappy with the auto-name `personal` | `codev account rename personal <name>` — or name it up front, before adding anything: `codev account add <name> --dir ~/.claude` registers your existing default itself (no extra account created). | | ||||||
| | 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). | | ||||||
| | Renaming any account (default or not) | `codev account rename <old> <new>` — changes only the name side of the *name → folder* mapping; folders never move, so name ≠ folder suffix is fine. | | ||||||
|
|
||||||
| > Not yet supported (tracked in [#127](https://github.com/grimmerk/codev/issues/127)): auto-detecting existing config folders for one-click registration, a UI flow for naming your existing default account, and a warning when `accounts.sh` overrides a hand-rolled `claude()` shell function. | ||||||
|
|
||||||
| 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 | | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Account picking for new sessions (⌥⌘+Enter) and account-aware resume are not fully supported across all terminal types. The VS Code and CodeV embedded terminal launch paths currently ignore the account label: Prompt for AI agents |
||||||
| | Projects tab: `⌥⌘+Enter` | Pick the account for a new session (`⌘+Enter` stays instant, under the global default) | | ||||||
|
|
||||||
| **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. | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -461,10 +461,25 @@ and **(d)** for CodeV, backed by the same `projectMap` in the registry so both a | |
| 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 | ||
|
grimmerk marked this conversation as resolved.
|
||
| default; single-account setups never see it); launches with explicit | ||
| `CLAUDE_CONFIG_DIR` + `command claude`. 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 — | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The 2e description overstates that all 'CodeV resume/launch' operations bypass the dispatcher. In fact, bare ⌘+Enter (no explicit account label) continues to use 'claude', routed through the accounts.sh dispatcher to respect the configurable global default. Only explicit account picks/resumes use 'command claude' to bypass the dispatcher. The wording should be narrowed to 'explicit account resume/launch' to avoid implying the default path also bypasses the dispatcher. Prompt for AI agents |
||
| `codev account default <label>` / Settings → Set default routes bare `claude` | ||
| through the dispatcher `*)` to the chosen account; CodeV resume/launch bypass the | ||
| dispatcher with `command claude` so explicit account choices can't be re-routed. | ||
| - **Batch 3 — later:** cross-account symlink reuse of global files | ||
| (CLAUDE.md / skills / commands / plugins / settings, §5). | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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'); | ||
|
|
@@ -1411,12 +1416,33 @@ 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); | ||
| claudeCmd = account?.configDirEnv | ||
| ? `CLAUDE_CONFIG_DIR='${account.configDirEnv.replace(/'/g, "'\\''")}' command claude` | ||
| : 'command 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); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: When Additionally, the default-account branch uses bare Prompt for AI agents |
||
| }; | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -331,11 +331,11 @@ export function generateAccountsSh(reg: Registry): string { | |||||
| L.push(' compadd account'); | ||||||
| L.push(' elif (( CURRENT == 3 )) && [ "${words[2]}" = "account" ]; then'); | ||||||
| L.push( | ||||||
| ' compadd list add default remove rm regenerate show install uninstall help', | ||||||
| ' compadd list add default remove rm rename regenerate show install uninstall help', | ||||||
| ); | ||||||
| L.push(' elif (( CURRENT == 4 )) && [ "${words[2]}" = "account" ]; then'); | ||||||
| L.push(' case "${words[3]}" in'); | ||||||
| if (allLabels) L.push(` default) compadd ${allLabels} ;;`); | ||||||
| if (allLabels) L.push(` default|rename) compadd ${allLabels} ;;`); | ||||||
| if (removable) L.push(` remove|rm) compadd ${removable} ;;`); | ||||||
| L.push(' esac'); | ||||||
| L.push(' fi'); | ||||||
|
|
@@ -346,6 +346,29 @@ export function generateAccountsSh(reg: Registry): string { | |||||
| L.push(' compdef _codev codev'); | ||||||
| L.push('fi'); | ||||||
| } | ||||||
| // Account-label completion for the `claude <label>` dispatcher. Registered | ||||||
| // only when nothing else completes `claude` (checked via _comps), so an | ||||||
| // official Claude Code completion — if one ever ships — wins automatically. | ||||||
| // Independent of appPath — the dispatcher exists whenever accounts do. | ||||||
| const allLabelsForClaude = accounts.map((a) => a.label).join(' '); | ||||||
| if (allLabelsForClaude) { | ||||||
| L.push(''); | ||||||
| L.push('# Complete account names for `claude <TAB>` (labels only; native'); | ||||||
| L.push('# claude flags/subcommands still work — just not suggested).'); | ||||||
| L.push('_claude_codev_accounts() {'); | ||||||
| L.push(' if (( CURRENT == 2 )); then'); | ||||||
| L.push(` compadd ${allLabelsForClaude}`); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The zsh completion for Use Prompt for AI agents
Suggested change
|
||||||
| L.push(' else'); | ||||||
| L.push(' # keep the default file completion for every other position'); | ||||||
| L.push(' _files'); | ||||||
| L.push(' fi'); | ||||||
| L.push('}'); | ||||||
| L.push( | ||||||
| 'if [ -n "${ZSH_VERSION:-}" ] && (( ${+functions[compdef]} )) && [ -z "${_comps[claude]:-}" ]; then', | ||||||
| ); | ||||||
| L.push(' compdef _claude_codev_accounts claude'); | ||||||
| L.push('fi'); | ||||||
| } | ||||||
| L.push(''); | ||||||
| return L.join('\n'); | ||||||
| } | ||||||
|
|
@@ -477,6 +500,31 @@ export function removeAccount(label: string): string | undefined { | |||||
| return reassignedDefault; | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Rename an account's label (the folder is NOT moved — `dir` is stored in the | ||||||
| * registry, so extra accounts keep working even when label ≠ folder suffix). | ||||||
| * This is also the only way to rename the anchor (~/.claude) account, whose | ||||||
| * auto-seeded "personal" label can't be changed via remove+add (the anchor is | ||||||
| * protected from removal). | ||||||
| */ | ||||||
| export function renameAccount(oldLabel: string, newLabel: string): void { | ||||||
| if (!newLabel || !LABEL_RE.test(newLabel)) { | ||||||
| throw new Error( | ||||||
| `Invalid label "${newLabel}" — use a letter followed by letters/digits/-/_`, | ||||||
| ); | ||||||
| } | ||||||
| const reg = readRegistry(); | ||||||
| const acct = reg.accounts.find((a) => a.label === oldLabel); | ||||||
| if (!acct) throw new Error(`No account "${oldLabel}"`); | ||||||
| if (reg.accounts.some((a) => a.label === newLabel)) { | ||||||
| throw new Error(`Account "${newLabel}" already exists`); | ||||||
| } | ||||||
| acct.label = newLabel; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Consider adding a directory-collision check in Prompt for AI agents |
||||||
| if (reg.defaultAccount === oldLabel) reg.defaultAccount = newLabel; | ||||||
| writeRegistry(reg); | ||||||
| regenerate(reg); | ||||||
| } | ||||||
|
|
||||||
| export function setDefault(label: string): void { | ||||||
| const reg = readRegistry(); | ||||||
| if (!reg.accounts.some((a) => a.label === label)) { | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,7 @@ Usage: | |
| codev account add <label> [--dir D] Register a new account (default dir ~/.claude-<label>) | ||
| codev account default <label> Set which account bare \`claude\` resolves to | ||
| codev account remove <label> Unregister an account (leaves its dir on disk) | ||
| codev account rename <old> <new> Rename an account's label (folder is not moved) | ||
| codev account regenerate Rewrite ~/.config/codev/accounts.sh from the registry | ||
| codev account show Print the generated accounts.sh (dry run, writes nothing) | ||
| codev account install Add the source line to ~/.zshrc (idempotent) | ||
|
|
@@ -102,6 +103,14 @@ function main(): number { | |
| return 0; | ||
| } | ||
|
|
||
| case 'rename': { | ||
| const [oldLabel, newLabel] = rest; | ||
| manager.renameAccount(oldLabel, newLabel); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: The Prompt for AI agents |
||
| console.log(`✓ Renamed "${oldLabel}" → "${newLabel}" (folder unchanged)`); | ||
| reloadHint(); | ||
| return 0; | ||
| } | ||
|
|
||
| case 'remove': | ||
| case 'rm': { | ||
| const label = rest[0]; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: The changelog overstates the launch behavior for default-account selections. When the user picks the default/anchor account from the picker, the implementation intentionally omits
CLAUDE_CONFIG_DIR(falling back to barecommand claudebecause the default account's identity lives at~/.claude.json, not under aCLAUDE_CONFIG_DIR). The release note should clarify that only non-default accounts launch with explicitCLAUDE_CONFIG_DIR, or reword to avoid implying the default account behaves the same way.Prompt for AI agents