Skip to content
Open
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# 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); the chosen account launches via explicit `CLAUDE_CONFIG_DIR` + `command claude` (correct on iTerm2/Terminal.app/Ghostty/cmux)

Copy link
Copy Markdown

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 bare command claude because the default account's identity lives at ~/.claude.json, not under a CLAUDE_CONFIG_DIR). The release note should clarify that only non-default accounts launch with explicit CLAUDE_CONFIG_DIR, or reword to avoid implying the default account behaves the same way.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At CHANGELOG.md, line 6:

<comment>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 bare `command claude` because the default account's identity lives at `~/.claude.json`, not under a `CLAUDE_CONFIG_DIR`). The release note should clarify that only non-default accounts launch with explicit `CLAUDE_CONFIG_DIR`, or reword to avoid implying the default account behaves the same way.</comment>

<file context>
@@ -3,9 +3,13 @@
 
 - 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); the chosen account launches via explicit `CLAUDE_CONFIG_DIR` + `command claude`
+  - `⌥⌘+Enter` on a project opens a small account picker (↑↓ / Enter / Esc, or click); the chosen account launches via explicit `CLAUDE_CONFIG_DIR` + `command claude` (correct on iTerm2/Terminal.app/Ghostty/cmux)
   - Plain `⌘+Enter` unchanged: opens instantly under the global default; single-account (or registry-less) setups never see the picker
-  - Cross-account *resume* stays deferred to Batch 3 (copy-fork design; a session's transcript lives in its own account)
</file context>
Suggested change
- `⌥⌘+Enter` on a project opens a small account picker (↑↓ / Enter / Esc, or click); the chosen account launches via explicit `CLAUDE_CONFIG_DIR` + `command claude` (correct on iTerm2/Terminal.app/Ghostty/cmux)
- `⌥⌘+Enter` on a project opens a small account picker (↑↓ / Enter / Esc, or click); the chosen non-default account launches via explicit `CLAUDE_CONFIG_DIR` + `command claude` (correct on iTerm2/Terminal.app/Ghostty/cmux); the default account launches via bare `command 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
- 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
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The README should note that claude <TAB> account-name completion is skipped when another completion is already registered for claude, so users with an existing completion do not expect account labels that will not appear.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At README.md, line 64:

<comment>The README should note that `claude <TAB>` account-name completion is skipped when another completion is already registered for `claude`, so users with an existing completion do not expect account labels that will not appear.</comment>

<file context>
@@ -41,6 +41,69 @@ For the full same-cwd accuracy matrix (detection + switch by launch method and t
+| `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.
+
+**`codev account` CLI** (the account manager — same generator the Settings UI uses):
</file context>
Suggested change
Tab completion included (zsh): `claude <TAB>` completes account names; `codev account <TAB>` completes subcommands and labels.
Tab completion included (zsh): `claude <TAB>` completes account names when no other `claude` completion is registered; `codev account <TAB>` completes subcommands and labels.


**`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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The docs claim that codev account add <name> later reattaches everything after removal, but that only holds for accounts that used the default directory. If an account was originally registered with a custom --dir, removing it drops that directory mapping from the registry, and a plain add <name> (or UI add) will default to ~/.claude-<name> instead of the original folder. To avoid silently orphaning the old data, the documentation should mention that reattaching a custom-directory account requires supplying --dir <original-folder> again.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At README.md, line 81:

<comment>The docs claim that `codev account add <name>` later reattaches everything after removal, but that only holds for accounts that used the default directory. If an account was originally registered with a custom `--dir`, removing it drops that directory mapping from the registry, and a plain `add <name>` (or UI add) will default to `~/.claude-<name>` instead of the original folder. To avoid silently orphaning the old data, the documentation should mention that reattaching a custom-directory account requires supplying `--dir <original-folder>` again.</comment>

<file context>
@@ -41,6 +41,69 @@ For the full same-cwd accuracy matrix (detection + switch by launch method and t
+
+> **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.
+
+**Common scenarios:**
</file context>


**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 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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: launchNewClaudeSession routes VS Code launches through a URI handler with no account injection, and explicitly skips account overrides for the embedded terminal. For resume, openSession does not thread session account info into the VS Code or embedded terminal paths either. Consider adding a caveat in the Sessions/Projects UI table rows noting that full account override is available only for external terminals (iTerm2, Terminal.app, Ghostty, cmux).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At README.md, line 102:

<comment>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: `launchNewClaudeSession` routes VS Code launches through a URI handler with no account injection, and explicitly skips account overrides for the embedded terminal. For resume, `openSession` does not thread session account info into the VS Code or embedded terminal paths either. Consider adding a caveat in the Sessions/Projects UI table rows noting that full account override is available only for external terminals (iTerm2, Terminal.app, Ghostty, cmux).</comment>

<file context>
@@ -41,6 +41,69 @@ For the full same-cwd accuracy matrix (detection + switch by launch method and t
+| 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 |
+| Projects tab: `⌥⌘+Enter` | Pick the account for a new session (`⌘+Enter` stays instant, under the global default) |
+
</file context>

| 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.
Expand Down
23 changes: 19 additions & 4 deletions docs/multi-account-support-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
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 —

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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
Check if this issue is valid — if so, understand the root cause and fix it. At docs/multi-account-support-design.md, line 479:

<comment>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.</comment>

<file context>
@@ -468,9 +468,18 @@ and **(d)** for CodeV, backed by the same `projectMap` in the registry so both a
+    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 —
+    `codev account default <label>` / Settings → Set default routes bare `claude`
+    through the dispatcher `*)` to the chosen account; CodeV resume/launch bypass the
</file context>

`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).

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
30 changes: 28 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,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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When launchNewClaudeSession is called with an explicit accountLabel, a stale or missing label silently falls back to the default account. Because getAccountByLabel always returns an account (falling back to default), and configDirEnv is null for the default account, the picker can end up launching the wrong account with no error or log. Consider validating that the returned account actually matches the requested label and aborting/logging when it does not.

Additionally, the default-account branch uses bare command claude without env -u CLAUDE_CONFIG_DIR, so an inherited CLAUDE_CONFIG_DIR from the parent environment could still route the session to the wrong account. This contradicts the multi-account semantics documented in the design notes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/claude-session-utility.ts, line 1445:

<comment>When `launchNewClaudeSession` is called with an explicit `accountLabel`, a stale or missing label silently falls back to the default account. Because `getAccountByLabel` always returns an account (falling back to default), and `configDirEnv` is `null` for the default account, the picker can end up launching the wrong account with no error or log. Consider validating that the returned account actually matches the requested label and aborting/logging when it does not.

Additionally, the default-account branch uses bare `command claude` without `env -u CLAUDE_CONFIG_DIR`, so an inherited `CLAUDE_CONFIG_DIR` from the parent environment could still route the session to the wrong account. This contradicts the multi-account semantics documented in the design notes.</comment>

<file context>
@@ -1439,7 +1439,10 @@ export const launchNewClaudeSession = (
+  // 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);
 };
 
</file context>

};

/**
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
52 changes: 50 additions & 2 deletions src/cli/account-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The zsh completion for claude at the first argument position only calls compadd for account labels. When the user types a path prefix like claude ./<TAB>, no account label matches ./, so compadd returns non-zero and no completions appear because _files is never reached. This removes file/path completion for the first argument even though the dispatcher legitimately passes non-label arguments through to Claude.

Use compadd ${allLabelsForClaude} || _files at CURRENT == 2 so that file completion falls back automatically when the current word doesn't match any account label.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/cli/account-manager.ts, line 360:

<comment>The zsh completion for `claude` at the first argument position only calls `compadd` for account labels. When the user types a path prefix like `claude ./<TAB>`, no account label matches `./`, so `compadd` returns non-zero and no completions appear because `_files` is never reached. This removes file/path completion for the first argument even though the dispatcher legitimately passes non-label arguments through to Claude.

Use `compadd ${allLabelsForClaude} || _files` at `CURRENT == 2` so that file completion falls back automatically when the current word doesn't match any account label.</comment>

<file context>
@@ -346,6 +346,29 @@ export function generateAccountsSh(reg: Registry): string {
+    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}`);
+    L.push('  else');
+    L.push('    # keep the default file completion for every other position');
</file context>
Suggested change
L.push(` compadd ${allLabelsForClaude}`);
L.push(` compadd ${allLabelsForClaude} || _files`);

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');
}
Expand Down Expand Up @@ -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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: renameAccount changes the account label but leaves dir untouched. Because addAccount derives the default directory from the label without checking for collisions, renaming workoffice and later adding work back silently creates two accounts sharing ~/.claude-work. This violates the documented model of one config dir per account and can cause confusing shared identity/session behavior.

Consider adding a directory-collision check in addAccount (or guarding renameAccount) to prevent two labels from mapping to the same dir/configDirEnv.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/cli/account-manager.ts, line 522:

<comment>`renameAccount` changes the account label but leaves `dir` untouched. Because `addAccount` derives the default directory from the label without checking for collisions, renaming `work` → `office` and later adding `work` back silently creates two accounts sharing `~/.claude-work`. This violates the documented model of one config dir per account and can cause confusing shared identity/session behavior.

Consider adding a directory-collision check in `addAccount` (or guarding `renameAccount`) to prevent two labels from mapping to the same `dir`/`configDirEnv`.</comment>

<file context>
@@ -477,6 +500,31 @@ export function removeAccount(label: string): string | undefined {
+  if (reg.accounts.some((a) => a.label === newLabel)) {
+    throw new Error(`Account "${newLabel}" already exists`);
+  }
+  acct.label = newLabel;
+  if (reg.defaultAccount === oldLabel) reg.defaultAccount = newLabel;
+  writeRegistry(reg);
</file context>

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)) {
Expand Down
9 changes: 9 additions & 0 deletions src/cli/codev-account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -102,6 +103,14 @@ function main(): number {
return 0;
}

case 'rename': {
const [oldLabel, newLabel] = rest;
manager.renameAccount(oldLabel, newLabel);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The rename command reuses reloadHint(), which suggests source ~/.zshrc as an alternative to opening a new shell. After a rename, however, sourcing the regenerated accounts.sh defines the new claude-<newLabel> function but leaves the old claude-<oldLabel> function callable in the current shell session — shells do not automatically unset functions that disappear from a sourced file. This can mislead users into thinking the rename took full effect when the old label still works. Consider either warning users that a new shell is required for rename to fully take effect, or unsetting the old function during generation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/cli/codev-account.ts, line 108:

<comment>The `rename` command reuses `reloadHint()`, which suggests `source ~/.zshrc` as an alternative to opening a new shell. After a rename, however, sourcing the regenerated `accounts.sh` defines the new `claude-<newLabel>` function but leaves the old `claude-<oldLabel>` function callable in the current shell session — shells do not automatically unset functions that disappear from a sourced file. This can mislead users into thinking the rename took full effect when the old label still works. Consider either warning users that a new shell is required for rename to fully take effect, or unsetting the old function during generation.</comment>

<file context>
@@ -102,6 +103,14 @@ function main(): number {
 
+    case 'rename': {
+      const [oldLabel, newLabel] = rest;
+      manager.renameAccount(oldLabel, newLabel);
+      console.log(`✓ Renamed "${oldLabel}" → "${newLabel}" (folder unchanged)`);
+      reloadHint();
</file context>

console.log(`✓ Renamed "${oldLabel}" → "${newLabel}" (folder unchanged)`);
reloadHint();
return 0;
}

case 'remove':
case 'rm': {
const label = rest[0];
Expand Down
2 changes: 1 addition & 1 deletion src/electron-api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ interface IElectronAPI {
scanClosedVSCodeSessions: (activeSessionIds: string[]) => Promise<any[]>;
refreshSessionPreview: (sessions: any[]) => Promise<Record<string, { lastUserMessage: string; lastAssistantMessage: string }>>;
openClaudeSession: (sessionId: string, projectPath: string, isActive: boolean, activePid?: number, customTitle?: string) => void;
launchNewClaudeSession: (projectPath: string) => void;
launchNewClaudeSession: (projectPath: string, accountLabel?: string) => void;
launchNewClaudeSessionInCodev: (projectPath: string) => void;
copyClaudeSessionCommand: (sessionId: string, projectPath: string) => void;
loadSessionEnrichment: (sessions: any[]) => Promise<{ titles: Record<string, string>; branches: Record<string, string>; prLinks: Record<string, { prNumber: number; prUrl: string }> }>;
Expand Down
4 changes: 2 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2276,14 +2276,14 @@ ipcMain.on('open-claude-session', async (_event, sessionId: string, projectPath:
openSession(sessionId, projectPath, isActive, activePid, terminalApp, terminalMode, customTitle);
});

ipcMain.on('launch-new-claude-session', async (_event, projectPath: string) => {
ipcMain.on('launch-new-claude-session', async (_event, projectPath: string, accountLabel?: string) => {
if (!existsSync(projectPath)) {
console.log('[launch-new-claude-session] path does not exist:', projectPath);
return;
}
const terminalApp = ((await settings.get('session-terminal-app')) || 'iterm2') as string;
const terminalMode = ((await settings.get('session-terminal-mode')) || 'tab') as string;
launchNewClaudeSession(projectPath, terminalApp, terminalMode);
launchNewClaudeSession(projectPath, terminalApp, terminalMode, accountLabel);
});

ipcMain.on('launch-new-claude-session-in-codev', (_event, projectPath: string) => {
Expand Down
Loading
Loading