Merge origin/main: T-476 per-repo-account breakdown

Union-resolved the .pql/changelog logs (append-only, idempotent INSERT … ON
CONFLICT DO UPDATE — latest-updated wins on replay), keeping both sides:
this session's T-158 changes and origin's T-476 6-child breakdown. pql.db
rebuilt clean (2785 statements).

NOTE — label collision to reconcile (record_ids intact, labels only):
the breakdown reused T-477 and T-478, already held by the window-size bug
and the Summer Night theme. Reconcile with `pql ticket relabel`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-26 11:36:03 +02:00
co-authored by Claude Opus 4.8
4 changed files with 852 additions and 0 deletions
+8
View File
@@ -35,3 +35,11 @@ INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updat
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKMV7Y13PAYKZC0WB4FQXKC', '06FBKN09R21H3AWR2Q2ZTSGNSW', '2026-06-12 03:16:40', '2026-06-12 03:16:40', NULL, '7e00348c10432c65b03f9dce36520e48', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKMXSVCE98K1H76N00TYCQR', '06FBKN2MP35NPPK1BRDYY2M428', '2026-06-12 03:16:44', '2026-06-12 03:16:44', NULL, 'a6b1c20279ac30f692a30c802cf8f3e5', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKMV7Y13PAYKZC0WB4FQXKC', '06FBKN4QVFVE51MY2N0CWCVXHM', '2026-06-12 03:16:49', '2026-06-12 03:16:49', NULL, '9063328f18ba32c0737997ac9af0911a', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBDSM0PRGYR61R0NWYAT9VDC', '06FFW49VYMPF18PYQXD9PCMHN8', '2026-06-25 10:24:38', '2026-06-25 10:24:38', NULL, 'a5146a3cfb2f6a3fd904224150836336', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49VYMPF18PYQXD9PCMHN8', '06FFW49W3V175EM4F8ZHC6JFM4', '2026-06-25 10:24:38', '2026-06-25 10:24:38', NULL, 'd57042b235b73ed2d25252d5253e6e4a', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49VYMPF18PYQXD9PCMHN8', '06FFW49W6GP535GR8XD1XEHYWG', '2026-06-25 10:24:38', '2026-06-25 10:24:38', NULL, '07fe1945cb3e60c43cce0989366af8fb', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49VYMPF18PYQXD9PCMHN8', '06FFW49W95HJK08BCRQSWFZBF4', '2026-06-25 10:24:38', '2026-06-25 10:24:38', NULL, 'e4484230e2202044baaa5ce7a2438bbb', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W3V175EM4F8ZHC6JFM4', '06FFW49W95HJK08BCRQSWFZBF4', '2026-06-25 10:24:38', '2026-06-25 10:24:38', NULL, '24cc5602737e75d30377522f38b3e7b5', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W95HJK08BCRQSWFZBF4', '06FFW49WBTT3ESK6QG0XZ1VN2G', '2026-06-25 10:24:38', '2026-06-25 10:24:38', NULL, '51f4759153b30d2a62ed6c69ab62b84d', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49VYMPF18PYQXD9PCMHN8', '06FFW49WEE5N4PESF5G1G5JRHR', '2026-06-25 10:24:38', '2026-06-25 10:24:38', NULL, 'b348cfc357b73fb28366459795752d8b', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W95HJK08BCRQSWFZBF4', '06FFW49WEE5N4PESF5G1G5JRHR', '2026-06-25 10:24:38', '2026-06-25 10:24:38', NULL, 'e114eff56fef8c92fe3b19df2b8b6e3c', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
+365
View File
@@ -6222,3 +6222,368 @@ thread _usage from claude_meta_sidebar.dart + team.section.usage i18n key,
en/nl). This resolves Q-34. Ready to pick up; not yet started.', NULL, '2026-06-26 07:01:13', '2026-06-26 07:01:13.929', '2026-06-26 07:01:13.929', NULL, 'b94a593c3db76b3a9705d8111f3d6b9b', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM62FKQQD0B9B80PFY4', 'status', 'ready', 'in_progress', NULL, '2026-06-26 07:09:18', '2026-06-26 07:09:18.285', '2026-06-26 07:09:18.285', NULL, 'fae5181a90e789f07eab3a12939d08f4', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM62FKQQD0B9B80PFY4', 'status', 'in_progress', 'done', NULL, '2026-06-26 09:31:41', '2026-06-26 09:31:41.267', '2026-06-26 09:31:41.267', NULL, 'b1bc1ec1f57601ffd2c4cee1a756b610', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDXN3ZBRS6JK7Q8G6JVSPXFC', 'status', 'backlog', 'in_progress', NULL, '2026-06-25 06:50:37', '2026-06-25 06:50:37', '2026-06-25 06:50:37', NULL, '9f0ab08dd9d0da71a333ed82c5aa96a0', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDXN3ZBRS6JK7Q8G6JVSPXFC', 'description', 'Today clide uses Claude Code''s system-wide login (the CLI''s global auth), so every repo/workspace shares one Claude account. Allow selecting/logging into a separate Claude account per repo, so different projects can run under different accounts (e.g. personal vs work). Needs: per-workspace auth storage + a way for the spawned Claude session to use that repo''s account instead of the global one; UI to sign in/switch per repo; fall back to the system login when none is set.', 'Today clide uses Claude Code''s system-wide login (the CLI''s global auth), so every repo/workspace shares one Claude account. Allow selecting/logging into a separate Claude account per repo so different projects can run under different accounts (e.g. personal vs work). Falls back to the system login when no per-repo binding is set.
## Proven mechanism: `CLAUDE_CONFIG_DIR`
Claude Code reads `CLAUDE_CONFIG_DIR` at process start and uses that directory in place of `~/.claude` credentials, settings, sessions, plugins, projects, the `ide/` discovery dir, everything. Already proven on this machine: `~/.claude-personal/` exists alongside `~/.claude/` with the same layout (`.claude.json`, `sessions/`, `plugins/`, `projects/`, `settings.json`, `ide/`). A child `claude` process spawned with `CLAUDE_CONFIG_DIR=~/.claude-personal` uses that account; clide already has the spawn-time hook to set it.
## Alternatives surveyed (online research, 2026-06)
- **No native Claude Code account switcher** (open issues anthropics/claude-code#44687, #18435, #36151).
- **claude-swap, claude-account-switcher, claude-multiprofile** third-party CLIs; all converge on `CLAUDE_CONFIG_DIR` swapping the active dir or symlinking shared bits (settings/commands/MCP) across per-account dirs.
- **direnv / shell aliases** user-shell automation that exports `CLAUDE_CONFIG_DIR` on cd-in. Not IDE-owned, only works from a terminal launch.
- **AgentsRoom** is the closest peer: "per-project pin, per-agent override, in-app sign-in flow, status badge per account" same mechanism, IDE surface. The shape clide should match.
No cleaner mechanism exists today; everything credible reduces to `CLAUDE_CONFIG_DIR`. The clide value-add is the IDE-native shell around it (per-repo binding + sign-in UX + IDE-bridge plumbing).
## Recommended approach
**Per-account named config dirs**, **per-repo named binding**, **CLI-driven sign-in**. Three concepts:
1. **Accounts** one Claude config dir per account, e.g. `~/.claude-personal/`, `~/.claude-work/`. Selecting an account = exporting `CLAUDE_CONFIG_DIR=<that dir>` for the spawned `claude`. Multiple repos can share one account (no extra sign-in to add a third repo for the same account).
2. **Repo binding** `<workspace>` `<account-name>`, stored in user-scope clide settings (the [T-356](#) relocation lands settings under user scope keyed by workspace-path hash same `fnv1a64Hex` from `lib/src/ipc/paths.dart`). Unset falls back to the global `~/.claude`.
3. **Sign-in** clide invokes `CLAUDE_CONFIG_DIR=<target> claude login` in a managed terminal pane; the CLI owns the OAuth browser bounce; credentials land in the target dir. No new auth code in clide.
Account workspace is the right axis: "work" is one identity used in 5 repos, not 5 sign-ins.
## Implementation
### Storage (where the binding lives)
- `accounts` list (name config-dir path) in **user-scope** clide settings after [T-356](#) relocates `.clide/settings.yaml` to user scope.
- `claude.account.<workspace-hash>` keys map a workspace to an account name. Workspace hash = `fnv1a64Hex(canonicalPath)` (`lib/src/ipc/paths.dart:101`), same key D-70 uses for sockets.
- Bootstrap migration: if `~/.claude-personal/` (or other `~/.claude-*/`) is detected on first run, offer to register it as an account.
### Spawn-time env injection
One resolver pattern matches `lib/src/env/shell_env.dart` (`primeLoginShellPath` / `resolvedToolPath`):
- `String? claudeConfigDirForWorkspace(String cwd)` returns the resolved config-dir path, or null when no binding (use Claude''s default).
- Inject in `lib/builtin/claude/src/agent_bootstrap.dart` (`agentBootstrap`): when the resolver returns a path, add `CLAUDE_CONFIG_DIR=<path>` to the returned `envDelta`. Single hook; every hosted session (primary/secondary/fork/teammate) inherits it because `ClaudeSessionOrchestrator._spawn` already calls `agentBootstrap(spec.cwd, base: spec.env)` at `session_orchestrator.dart:247`.
- `SpawnSpec.env` already exists (line 67) as a per-call override keep it so an explicit caller can win.
### IDE bridge discovery (the trap)
`lib/src/ipc/mcp_server.dart:344` writes the `/ide` lock at `$HOME/.claude/ide/<pid>.lock`. Claude Code with `CLAUDE_CONFIG_DIR=<X>` looks under `<X>/ide/` so today''s lock is invisible to a `claude` running with a custom dir, and clide''s IDE bridge can''t be reached.
Fix: when clide is using one or more non-default config dirs (any bound repo''s account default), write the discovery lock into **every** active config dir''s `ide/` (the default `$HOME/.claude/ide/` plus each `<account-dir>/ide/`). Same lock content; each is `0600`-scoped per [T-362](#). Cleanup tracks all written paths.
### Sign-in flow
One CLI verb (D-6 parity, mirrors [T-249](#) shape):
- `clide claude account add <name> [--dir <path>]` — registers an account. Default dir `~/.claude-<name>/`. Idempotent.
- `clide claude account login <name>` spawns `CLAUDE_CONFIG_DIR=<dir> claude login` in a managed terminal pane (`lib/builtin/terminal/`); the CLI handles OAuth.
- `clide claude account set <name>` bind the current workspace to `<name>`. Active Claude session(s) for this repo respawn under the new account (T-437 close-and-respawn pattern is already correct: `close` awaits real process death, `spawn` is idempotent per id).
- `clide claude account list` JSON: registered accounts + which (if any) is bound to this workspace.
- `clide claude account unset` clear the binding; falls back to default.
- `clide claude account remove <name>` registry remove; offers `--purge` to delete the underlying dir (off by default).
Dispatcher registration lives in `lib/src/daemon/` alongside `image_commands.dart`. Flutter-free handler; the terminal-spawn side publishes on a MessageBus channel the Claude extension subscribes to.
### UI
- **Welcome view**: list registered accounts; tap = bind current workspace; "Add account…" runs the login flow.
- **Claude pane chrome**: small badge showing the active account name. Tap opens the picker (issues `clide claude account set `).
- **Settings**: under "Claude", an Accounts row (manage registry) and a per-repo "Account for this workspace" row.
- **Workspace picker / project list**: each project shows its bound account.
### CLI parity (D-6)
Every UI action above issues a verb; every verb is observable via `clide claude account list`. No UI action lacks a CLI counterpart.
### Fallback / safety
- No binding use `Platform.environment[''CLAUDE_CONFIG_DIR'']` if the parent env already set it; otherwise leave it unset (Claude defaults to `~/.claude`). Same behaviour as today.
- Switching account = respawn the pane''s session, not a hot swap. Existing session closes (T-437 await-death). Transcript is preserved per-account in that account''s `projects/` dir; nothing is merged across accounts.
- Permission rules and `effortLevel` (per `claude_config.dart`) are config-dir-scoped already no new layering needed.
- D-64 (no phone-home): adding an account triggers exactly one CLI-initiated `claude login` browser flow, on explicit user action. No background calls.
## Acceptance
1. `clide claude account add work` registers an account at `~/.claude-work/`; `clide claude account login work` opens a terminal pane that completes OAuth and lands credentials in that dir.
2. `clide claude account set work` in a workspace binds it; respawning the Claude pane spawns `claude` with `CLAUDE_CONFIG_DIR=~/.claude-work/`. Verified by `ps eww` / `lsof` against that process showing the right env.
3. `clide claude account list` returns the registry + the current workspace''s binding (or `null`).
4. `clide`''s `/ide` bridge is reachable from the bound account''s `claude` its `~/.claude-work/ide/<clide-pid>.lock` is present and parseable; an in-session `/ide` resolves.
5. With no binding, behaviour is identical to today (regression test against the global `~/.claude`).
6. A second clide window opened on a differently-bound repo runs under a different account end-to-end; the two transcripts never cross.
7. Sign-out: removing the binding (`unset`) without `--purge` keeps the underlying dir untouched; with `--purge` removes it.
8. UI parity: every account action in the welcome view / settings / Claude pane chrome reaches the same dispatcher verb.
## Out of scope (for this story)
- Per-pane account selection within one workspace (forks/teammates on different accounts). One repo = one account in v1; revisit if needed.
- Cross-account credential sharing / symlinked settings (claude-account-switcher''s pattern). v1 keeps dirs fully isolated; revisit as a follow-up if duplication becomes painful.
- Team-mode account-budget surfacing across multiple accounts (T-158 territory).
- Windows behaviour beyond "if `CLAUDE_CONFIG_DIR` works on the platform, this works on the platform" no Windows-specific UX in v1.
- Auto-binding via `direnv` or `.envrc` integration. The IDE owns the binding; shell-side automation is a follow-up if asked for.
## References / sources
- Code: `lib/builtin/claude/src/agent_bootstrap.dart` (env-delta builder), `lib/builtin/claude/src/session_orchestrator.dart` (spawn site), `lib/src/env/shell_env.dart` (env-resolution pattern), `lib/src/ipc/mcp_server.dart:344` (IDE-lock writer), `lib/src/ipc/paths.dart:101` (`fnv1a64Hex` workspace hash), `lib/builtin/claude/src/claude_config.dart` (per-config-dir settings).
- Tickets: [T-356](#) (workspace settings user scope, dependency), [T-362](#) (`/ide` lock 0600), [T-437](#) (await-death respawn needed for switch-account-and-respawn), [T-249](#) (image-card CLI shape to mirror), D-6 (CLI parity), D-64 (no phone-home), D-70 (workspace hash), D-75 (avoid CC-internals coupling this work uses Claude Code''s *public* CLAUDE_CONFIG_DIR env, not internal state, so it is D-75-safe).
- External: anthropics/claude-code#44687 / #18435 / #36151 (no native switcher); third-party tools claude-swap, claude-account-switcher, claude-multiprofile, AgentsRoom (all use CLAUDE_CONFIG_DIR); CLAUDE_CONFIG_DIR is a documented Claude Code env knob.
', NULL, '2026-06-25 07:22:55', '2026-06-25 07:22:55', '2026-06-25 07:22:55', NULL, '5ad371d9f4ad2709b2c6ae6e076e001b', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDXN3ZBRS6JK7Q8G6JVSPXFC', 'type', 'story', 'epic', NULL, '2026-06-25 09:13:35', '2026-06-25 09:13:35', '2026-06-25 09:13:35', NULL, 'd24b9be9128ce2c4a53809e9f5db8a0a', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49VYMPF18PYQXD9PCMHN8', 'description', NULL, 'Foundation for the multi-account epic (T-476): the user-scope persistence layer that every other child consumes.
## Scope
Two durable concerns, plus a first-run migration:
### Account registry
List of `(name, configDir)` pairs in user-scope clide settings the catalog of Claude accounts this user has set up. Account names are user-chosen (`personal`, `work`, `client-acme`); `configDir` is the `CLAUDE_CONFIG_DIR` path Claude Code will read from. Default dir for a new account is `~/.claude-<name>/`, but an explicit path is allowed (so an existing `~/.claude-personal/` can be adopted as-is).
### Workspace binding
`claude.account.<workspace-hash>` `<account-name>`. Workspace hash is the same FNV-1a 64-bit hex used by D-70 sockets (`fnv1a64Hex` in `lib/src/ipc/paths.dart:101`) single source of truth. Unset binding means "use Claude''s default" (no `CLAUDE_CONFIG_DIR` injection).
### Bootstrap migration
On first run, scan `~` for directories matching `.claude-*` that look like a Claude config dir (have a `.claude.json` or `sessions/` inside). Surface the list actually offering to register them is welcome-view UX (T-481); this ticket just provides the probe.
## Where
- `SettingsStore` schema additions in `lib/kernel/src/settings.dart` (or wherever the schema-driven settings layer lands coordinate with T-356).
- A new `lib/builtin/claude/src/account_registry.dart` (or similar) holding the typed reader/writer + the migration probe.
- Flutter-free; unit-tested directly.
## API shape (sketch refine in implementation)
```
class AccountRegistry {
List<Account> get accounts;
Account? accountForWorkspace(String cwd);
Account? accountByName(String name);
void registerAccount(String name, String dir);
void removeAccount(String name);
void bindWorkspace(String cwd, String name);
void unbindWorkspace(String cwd);
}
List<DetectedAccount> probeExistingAccountDirs(String home);
```
All persistence flows through SettingsStore so the existing `.clide/settings.yaml` (or post-T-356 user-scope equivalent) reload/watch/serialize semantics carry over for free.
## Acceptance
- Schema declares `claude.accounts` (list of `{name, dir}`) and `claude.account.<workspace-hash>` (string).
- `accountForWorkspace(cwd)` resolves an account, or null when no binding.
- Registry CRUD (register/remove/bind/unbind) round-trips through SettingsStore.
- Bootstrap probe returns existing `~/.claude-*` dirs as candidates without mutating anything.
- Unit tests cover registry CRUD, workspace-hash mapping (canonicalised cwd, same hash for trailing-slash variants), probe behaviour (skips non-config dirs).
- No process spawning, no UI, no CLI those are downstream tickets.
## Depends on
- T-356 (settings user scope). The binding keys are PER-USER, not per-repo (you don''t want "which account does this repo use" committed to the repo). Land after T-356 ships its user-scope settings store.
## Consumers
- T-478 (env injection) reads `accountForWorkspace(cwd)` at spawn time.
- T-479 (IDE-lock multiplex) enumerates active accounts to write per-dir locks.
- T-480 (CLI verbs) registry mutations behind every account command.
- T-481 (UI) renders registry + binding state; calls the CRUD via T-480.
- T-482 (settings UI) schema-driven rows hit these keys.
', NULL, '2026-06-25 10:24:31', '2026-06-25 10:24:31', '2026-06-25 10:24:31', NULL, '902236ccdf152fb332a370feb2a03874', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W3V175EM4F8ZHC6JFM4', 'description', NULL, 'The load-bearing piece of the multi-account epic (T-476): get the right `CLAUDE_CONFIG_DIR` into the hosted Claude process''s environment. Once this is in, a workspace bound to an account spawns its `claude` under that account end-to-end.
## Where
One resolver + one hook pattern follows `lib/src/env/shell_env.dart` (single source of truth for env-related concerns).
### New resolver
`String? claudeConfigDirForWorkspace(String cwd)`:
1. Look up the binding via the T-477 `AccountRegistry`.
2. If bound return the account''s `configDir`.
3. If unbound return `Platform.environment[''CLAUDE_CONFIG_DIR'']` when the parent already set it (respect launcher choice); otherwise `null` (Claude defaults to `~/.claude`).
Flutter-free, pure (registry injected), unit-tested.
### Injection point
`lib/builtin/claude/src/agent_bootstrap.dart`''s `agentBootstrap(workspaceRoot, base:)`: when the resolver returns non-null, merge `''CLAUDE_CONFIG_DIR'': <path>` into the returned `envDelta` BEFORE the `base` spread, so `SpawnSpec.env` keeps its per-call-override precedence (line 67 of `session_orchestrator.dart` already passes the override as `base`).
Every hosted session (primary / secondary / fork / teammate) inherits the binding because `ClaudeSessionOrchestrator._spawn` already routes through `agentBootstrap` at `session_orchestrator.dart:247` no per-call-site work.
## Acceptance
1. A bound workspace spawns `claude` with `CLAUDE_CONFIG_DIR=<account-dir>` verifiable by `ps eww` / `lsof` against the spawned process.
2. An unbound workspace spawns identically to today no `CLAUDE_CONFIG_DIR` injected unless the parent env already had one.
3. `SpawnSpec.env` per-call override still wins (precedence: explicit override > workspace binding > parent env > unset).
4. The resolver returns `null` for unbound workspaces with no parent env; the bootstrap then omits the key (NOT writes an empty value).
5. Unit tests: resolver returns each of the four states; `envDelta` precedence is correct; agent_bootstrap unit tests already cover the merge shape extend them.
## Depends on
- T-477 (storage). The resolver needs `AccountRegistry.accountForWorkspace(cwd)`.
## Out of scope
- Restarting an already-running session on binding change that lives in T-480''s `set` verb (it owns the close-then-spawn flow).
- IDE bridge discovery for the bound account T-479.
', NULL, '2026-06-25 10:24:31', '2026-06-25 10:24:31', '2026-06-25 10:24:31', NULL, '449a8abdb2bfafa1570735cc23c0aefe', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W6GP535GR8XD1XEHYWG', 'description', NULL, 'Make clide''s `/ide` MCP bridge reachable from a `claude` running with a custom `CLAUDE_CONFIG_DIR`. Without this, the per-repo account work (T-476) ships a `claude` that can''t see clide''s IDE bridge.
## The trap
`lib/src/ipc/mcp_server.dart:344` writes the discovery lock at `$HOME/.claude/ide/<pid>.lock`. Claude Code with `CLAUDE_CONFIG_DIR=<X>` looks for `/ide` locks under `<X>/ide/` so a bound Claude session never finds clide today.
## Fix
Extend `_writeDiscoveryFile` (and the cleanup path) to write the lock into **every CURRENTLY-active config dir''s `ide/`**. "Active" means the union of:
- The default `$HOME/.claude/ide/` (always).
- Every account `configDir/ide/` for accounts bound to a workspace that is currently open in this clide process.
Same lock content for each; same `0600` perms per T-362. The MCP server tracks every path it wrote so shutdown cleans them all.
### React to binding changes
The set of active config dirs changes when a workspace is opened/closed in this process, or when a binding is set/unset (T-480 verbs). The MCP server subscribes to these changes and synchronizes the lock set incrementally write a new lock when a dir becomes active, remove it when no remaining workspace references that dir.
## Where
- `lib/src/ipc/mcp_server.dart` extend `_writeDiscoveryFile`, add a tracked-paths set, hook into the shutdown sweep.
- Wire the active-dir source through the existing app boot the MCP server is constructed in `lib/main.dart`; that''s where it can be handed an `AccountRegistry` (T-477) and a workspace-open observer.
## Acceptance
1. With no bindings, behaviour is identical to today (single lock at `$HOME/.claude/ide/<pid>.lock`).
2. With one or more workspaces bound to non-default accounts, a lock exists at `<each-account-dir>/ide/<pid>.lock` with the same content and `0600` perms.
3. Binding a workspace to a new (previously unused) account at runtime causes the corresponding lock to appear; unbinding the last workspace using that account causes it to disappear.
4. clide shutdown removes every lock this process wrote (defaultes plus account dirs); no orphans.
5. A live `claude` running under a bound account finds clide''s `/ide` endpoint end-to-end (manual confirmation in a real session).
6. Tests cover the multi-dir write/cleanup + the dynamic add/remove path (mock the filesystem).
## Depends on
- T-477 (storage). Needs the registry + binding API to enumerate active dirs.
## Independent of
T-478 T-479 can land in parallel; the bound `claude` just won''t have an IDE bridge until both ship.
', NULL, '2026-06-25 10:24:31', '2026-06-25 10:24:31', '2026-06-25 10:24:31', NULL, '2989bdaa9ad40607006e10f99f96f6d6', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W95HJK08BCRQSWFZBF4', 'description', NULL, 'The `clide claude account …` dispatcher and verbs — D-6 parity for the multi-account feature (T-476). UI tickets (T-481 / T-482) call these verbs only; no UI writes the registry directly.
## Verbs
- `clide claude account add <name> [--dir <path>]` — register an account. Default dir `~/.claude-<name>/`. Idempotent (re-add with the same args is a no-op; conflicting args returns a clear userError). Does NOT spawn `claude login` — composability with `login`.
- `clide claude account list` JSON: `{accounts: [{name, dir}], boundAccount: <name|null>, detected: [<dir>...]}`. `boundAccount` is for the current workspace; `detected` is the T-477 probe output (existing `~/.claude-*/` dirs not yet registered).
- `clide claude account login <name>` spawn `CLAUDE_CONFIG_DIR=<dir> claude login` in a managed terminal pane (`lib/builtin/terminal/`). Claude CLI owns the OAuth browser bounce. Verb returns once the pane spawns; OAuth completion is observed by the pane lifecycle, not awaited by the verb.
- `clide claude account set <name>` bind the current workspace AND respawn the active Claude pane(s) on the new account. Respawn flow uses `ClaudeSessionOrchestrator.close` (which already awaits real process death per T-437) followed by a fresh `spawn` with the same id. Honest userError if `<name>` isn''t registered.
- `clide claude account unset` clear the binding for this workspace; respawn pane(s) on the default account. Emits a binding-removed event so T-479''s lock multiplexer can prune the matching `ide/` lock.
- `clide claude account remove <name> [--purge]` — registry-remove; refuses if any open workspace is bound to it (with a clear message). `--purge` additionally `rm -rf`s the `configDir` after registry removal (off by default; require confirmation).
## Where
- Dispatcher: new `lib/src/daemon/claude_account_commands.dart` mirror the shape of `image_commands.dart` (CommandSchema-declared positionals + flags, Flutter-free handler).
- A MessageBus channel e.g. `accountActionChannel = ''claude.account''` for non-CLI consumers (the Claude extension + UI) to observe registry/binding changes.
- Registers in `DaemonDispatcher` alongside the existing `image.show` / `pql.*` verbs.
## Acceptance
1. All six verbs round-trip through `clide capabilities` and execute against the live dispatcher.
2. `clide claude account add work` registers and persists across clide restarts.
3. `clide claude account list` returns the registry + this workspace''s binding + the detected-dirs probe results.
4. `clide claude account login work` opens a managed terminal pane in which `claude login` runs against `~/.claude-work/`; OAuth completion writes credentials into that dir.
5. `clide claude account set work` while a Claude pane is open: pane respawns with `CLAUDE_CONFIG_DIR=~/.claude-work/` (verified end-to-end against the live process).
6. `clide claude account unset` returns the pane to the default config dir; the matching `<dir>/ide/<pid>.lock` (T-479) is removed.
7. `clide claude account remove work` refuses while a workspace is bound; succeeds once unset. `--purge` removes the underlying directory after registry removal.
8. Tests follow the `image_commands` shape: schema declaration, Flutter-free handler tests, dispatcher registration check.
## Depends on
- T-477 (storage / registry).
- T-478 (env injection) so the respawn under `set`/`unset` actually changes which account the new process uses.
## Decision ref
D-6 (CLI parity).
', NULL, '2026-06-25 10:24:31', '2026-06-25 10:24:31', '2026-06-25 10:24:31', NULL, '97b7b3b378c40a416bdae96d6376687b', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49WBTT3ESK6QG0XZ1VN2G', 'description', NULL, 'The conversational/chrome UI surfaces for the multi-account epic (T-476). Settings-UI rows live in T-482; this ticket owns the always-visible affordances.
## Claude pane chrome badge
Small badge in the pane chrome (next to the existing status/banner row see `lib/builtin/claude/src/claude_banner.dart` / status-bar pattern) showing the active account name, or "default" when unbound.
- Tap opens a popover/picker: list of registered accounts (radio-selectable) + "Add account…" entry.
- Selecting an account dispatches `clide claude account set <name>`; "Add account…" runs the add+login flow.
- The badge live-updates from the `claude.account` MessageBus channel published by T-480.
- Colour-coded subtly (one accent per account name? Hash-derived?) so the user can spot at a glance which window is which account.
## Welcome view: Claude accounts section
New section in the welcome view (peer of the existing project / toolchain rows in `lib/builtin/welcome/src/welcome_view.dart`):
- **Registered accounts** one row per registered account: name, configDir path, a per-row affordance to bind this workspace / unbind / re-login.
- **Detected (not yet registered)** rows for the T-477 bootstrap probe results (existing `~/.claude-*/` dirs not in the registry yet). Each has a "Register this" affordance that dispatches `clide claude account add <name> --dir <detected-dir>`.
- **Add new account** button that runs the full add+login flow against a default `~/.claude-<name>/` path.
All actions go through T-480''s CLI verbs; this widget never writes the registry directly.
## Constraints
- Use the existing UI-design vocabulary (theme tokens, control geometry see the `ui-design` skill). No Material/Cupertino.
- The badge is display-with-affordance, not an inline interaction surface in the D-78 sense opening the picker is a navigation gesture, the selection happens in the picker modal.
- The welcome view follows the existing one-screen welcome-layout decisions (don''t add a fold-out / accordion if it conflicts with the current layout).
## Acceptance
1. The Claude pane badge shows the active account name (or "default"), updates live across `set`/`unset`/`add` actions.
2. Tapping the badge opens a picker; selecting a registered account dispatches `clide claude account set` and respawns the pane on the new account.
3. The welcome view''s accounts section lists registered accounts + detected candidates + an add-new affordance.
4. Every action available in the UI is reachable as the corresponding `clide claude account` verb (D-6 parity verified by inspection).
5. Widget tests cover badge rendering for each state (default / bound / unknown) + the picker action dispatch (mock the CLI client).
6. A11y: badge has a semantic label naming the account; picker is keyboard-reachable.
## Depends on
- T-480 (CLI verbs). Wires every action to the dispatcher.
## Out of scope
- Settings-UI rows T-482.
- Project-picker integration (each project shows its bound account) desirable, file as a follow-up if it grows complex.
', NULL, '2026-06-25 10:24:31', '2026-06-25 10:24:31', '2026-06-25 10:24:31', NULL, 'a31ec31dee11c9f441ed920b4c0eb6c4', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49WEE5N4PESF5G1G5JRHR', 'description', NULL, 'Settings UI surface for the multi-account epic (T-476): two schema-driven rows under the "Claude" settings category.
## Rows
### Global: "Claude > Accounts"
Registry CRUD list one entry per registered account. Each entry shows: name, configDir, current sign-in status (a small "signed in" / "not signed in" indicator derived from whether `<dir>/.claude.json` carries an active credential read-only probe, no auth state mutation here). Per-entry affordances: re-login, remove (with `--purge` confirmation). Plus an "Add account…" affordance.
### Per-workspace: "Claude > Account for this workspace"
Dropdown/select listing registered accounts + a "(default)" option. Selecting issues `clide claude account set <name>` (or `unset` for default).
## Where
- Hooks into the schema-driven settings panel coming from T-8 / settings UI. Until that lands, this row set may need to render in a placeholder host.
- All actions go through T-480''s CLI verbs.
- Scope-tag icon for each row per the 2026-06-10 settings convention (folder = project; globe = always/global) both rows are present, distinguishing scope visually.
## Acceptance
1. Global Accounts row renders the registry, supports add / re-login / remove (with purge confirmation).
2. Per-workspace row renders the current binding and lets the user switch / clear it.
3. Both rows reflect live changes from the `claude.account` MessageBus channel (T-480) without a settings reload.
4. All actions issue T-480 verbs no direct settings writes from this UI.
5. Widget tests for the row renderers in each state.
## Depends on
- T-480 (CLI verbs) for the action layer.
- T-477 (storage) for the schema registration.
- Gated on T-8''s settings UI maturity until the schema-driven panel can host these rows, this ticket parks. Track T-8 progress before activating.
## Out of scope
- The Claude pane chrome badge + welcome view T-481.
', NULL, '2026-06-25 10:24:31', '2026-06-25 10:24:31', '2026-06-25 10:24:31', NULL, 'fd3c9504dad8bae438c1df879bd5eb44', 2) ON CONFLICT(hash) DO NOTHING;
+6
View File
@@ -304,3 +304,9 @@ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDXN3ZBRS6JK7Q8G6JVSPXFC', 'T-476', '2026-06-19 07:42:08.735', '2026-06-19 07:42:08.735', NULL, 'f75a0a8c00b0ec40c110c1169e42fe2c', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FEWB9JA5CMTXB5D7G1NSHAV4', 'T-477', '2026-06-22 07:13:19.958', '2026-06-22 07:13:19.958', NULL, '0314d96a690bdadc122ce0a1e9ed0f5c', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFKFYBZFKBX0WV574QJEH2BC', 'T-478', '2026-06-24 13:09:16.923', '2026-06-24 13:09:16.923', NULL, 'f1ea48c2cfa08915841d99eea06a9974', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49VYMPF18PYQXD9PCMHN8', 'T-477', '2026-06-25 09:16:42', '2026-06-25 09:16:42', NULL, '64605163f24e3846463f3afb4ed62fd1', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W3V175EM4F8ZHC6JFM4', 'T-478', '2026-06-25 09:16:42', '2026-06-25 09:16:42', NULL, '1c2e59e6220c0b288103c4a71a8282ca', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W6GP535GR8XD1XEHYWG', 'T-479', '2026-06-25 09:16:42', '2026-06-25 09:16:42', NULL, '2a6162f26c4c0b6e7bceb1f1a2ffe167', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W95HJK08BCRQSWFZBF4', 'T-480', '2026-06-25 09:16:42', '2026-06-25 09:16:42', NULL, '8a42c8c1fc2036d6ea73521266e87235', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49WBTT3ESK6QG0XZ1VN2G', 'T-481', '2026-06-25 09:16:42', '2026-06-25 09:16:42', NULL, 'd1d616d020e6ea8661c1f5553e3001eb', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49WEE5N4PESF5G1G5JRHR', 'T-482', '2026-06-25 09:16:42', '2026-06-25 09:16:42', NULL, '557f92392db9b5c1e6b6840fe1293b7d', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
+473
View File
@@ -7859,3 +7859,476 @@ own budget view rather than send the user to the Activity tab.
Scope is now exactly the "RECOMMENDED SCOPE" above (team_tab.dart MetaSection +
thread _usage from claude_meta_sidebar.dart + team.section.usage i18n key,
en/nl). This resolves Q-34. Ready to pick up; not yet started.', 'done', 'low', NULL, NULL, NULL, '2026-05-23 20:48:38', '2026-06-26 09:31:41.267', NULL, 'a96387776f48125b0def220885e1f27b', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDXN3ZBRS6JK7Q8G6JVSPXFC', 'story', NULL, 'Per-repo Claude account login (not system-wide)', 'Today clide uses Claude Code''s system-wide login (the CLI''s global auth), so every repo/workspace shares one Claude account. Allow selecting/logging into a separate Claude account per repo, so different projects can run under different accounts (e.g. personal vs work). Needs: per-workspace auth storage + a way for the spawned Claude session to use that repo''s account instead of the global one; UI to sign in/switch per repo; fall back to the system login when none is set.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-19 07:42:08.735', '2026-06-25 06:50:37', NULL, '13e578cb9d9e0b863cd4b9a66dd88758', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDXN3ZBRS6JK7Q8G6JVSPXFC', 'story', NULL, 'Per-repo Claude account login (not system-wide)', 'Today clide uses Claude Code''s system-wide login (the CLI''s global auth), so every repo/workspace shares one Claude account. Allow selecting/logging into a separate Claude account per repo so different projects can run under different accounts (e.g. personal vs work). Falls back to the system login when no per-repo binding is set.
## Proven mechanism: `CLAUDE_CONFIG_DIR`
Claude Code reads `CLAUDE_CONFIG_DIR` at process start and uses that directory in place of `~/.claude` credentials, settings, sessions, plugins, projects, the `ide/` discovery dir, everything. Already proven on this machine: `~/.claude-personal/` exists alongside `~/.claude/` with the same layout (`.claude.json`, `sessions/`, `plugins/`, `projects/`, `settings.json`, `ide/`). A child `claude` process spawned with `CLAUDE_CONFIG_DIR=~/.claude-personal` uses that account; clide already has the spawn-time hook to set it.
## Alternatives surveyed (online research, 2026-06)
- **No native Claude Code account switcher** (open issues anthropics/claude-code#44687, #18435, #36151).
- **claude-swap, claude-account-switcher, claude-multiprofile** third-party CLIs; all converge on `CLAUDE_CONFIG_DIR` swapping the active dir or symlinking shared bits (settings/commands/MCP) across per-account dirs.
- **direnv / shell aliases** user-shell automation that exports `CLAUDE_CONFIG_DIR` on cd-in. Not IDE-owned, only works from a terminal launch.
- **AgentsRoom** is the closest peer: "per-project pin, per-agent override, in-app sign-in flow, status badge per account" same mechanism, IDE surface. The shape clide should match.
No cleaner mechanism exists today; everything credible reduces to `CLAUDE_CONFIG_DIR`. The clide value-add is the IDE-native shell around it (per-repo binding + sign-in UX + IDE-bridge plumbing).
## Recommended approach
**Per-account named config dirs**, **per-repo named binding**, **CLI-driven sign-in**. Three concepts:
1. **Accounts** one Claude config dir per account, e.g. `~/.claude-personal/`, `~/.claude-work/`. Selecting an account = exporting `CLAUDE_CONFIG_DIR=<that dir>` for the spawned `claude`. Multiple repos can share one account (no extra sign-in to add a third repo for the same account).
2. **Repo binding** `<workspace>` `<account-name>`, stored in user-scope clide settings (the [T-356](#) relocation lands settings under user scope keyed by workspace-path hash same `fnv1a64Hex` from `lib/src/ipc/paths.dart`). Unset falls back to the global `~/.claude`.
3. **Sign-in** clide invokes `CLAUDE_CONFIG_DIR=<target> claude login` in a managed terminal pane; the CLI owns the OAuth browser bounce; credentials land in the target dir. No new auth code in clide.
Account workspace is the right axis: "work" is one identity used in 5 repos, not 5 sign-ins.
## Implementation
### Storage (where the binding lives)
- `accounts` list (name config-dir path) in **user-scope** clide settings after [T-356](#) relocates `.clide/settings.yaml` to user scope.
- `claude.account.<workspace-hash>` keys map a workspace to an account name. Workspace hash = `fnv1a64Hex(canonicalPath)` (`lib/src/ipc/paths.dart:101`), same key D-70 uses for sockets.
- Bootstrap migration: if `~/.claude-personal/` (or other `~/.claude-*/`) is detected on first run, offer to register it as an account.
### Spawn-time env injection
One resolver pattern matches `lib/src/env/shell_env.dart` (`primeLoginShellPath` / `resolvedToolPath`):
- `String? claudeConfigDirForWorkspace(String cwd)` returns the resolved config-dir path, or null when no binding (use Claude''s default).
- Inject in `lib/builtin/claude/src/agent_bootstrap.dart` (`agentBootstrap`): when the resolver returns a path, add `CLAUDE_CONFIG_DIR=<path>` to the returned `envDelta`. Single hook; every hosted session (primary/secondary/fork/teammate) inherits it because `ClaudeSessionOrchestrator._spawn` already calls `agentBootstrap(spec.cwd, base: spec.env)` at `session_orchestrator.dart:247`.
- `SpawnSpec.env` already exists (line 67) as a per-call override keep it so an explicit caller can win.
### IDE bridge discovery (the trap)
`lib/src/ipc/mcp_server.dart:344` writes the `/ide` lock at `$HOME/.claude/ide/<pid>.lock`. Claude Code with `CLAUDE_CONFIG_DIR=<X>` looks under `<X>/ide/` so today''s lock is invisible to a `claude` running with a custom dir, and clide''s IDE bridge can''t be reached.
Fix: when clide is using one or more non-default config dirs (any bound repo''s account default), write the discovery lock into **every** active config dir''s `ide/` (the default `$HOME/.claude/ide/` plus each `<account-dir>/ide/`). Same lock content; each is `0600`-scoped per [T-362](#). Cleanup tracks all written paths.
### Sign-in flow
One CLI verb (D-6 parity, mirrors [T-249](#) shape):
- `clide claude account add <name> [--dir <path>]` — registers an account. Default dir `~/.claude-<name>/`. Idempotent.
- `clide claude account login <name>` spawns `CLAUDE_CONFIG_DIR=<dir> claude login` in a managed terminal pane (`lib/builtin/terminal/`); the CLI handles OAuth.
- `clide claude account set <name>` bind the current workspace to `<name>`. Active Claude session(s) for this repo respawn under the new account (T-437 close-and-respawn pattern is already correct: `close` awaits real process death, `spawn` is idempotent per id).
- `clide claude account list` JSON: registered accounts + which (if any) is bound to this workspace.
- `clide claude account unset` clear the binding; falls back to default.
- `clide claude account remove <name>` registry remove; offers `--purge` to delete the underlying dir (off by default).
Dispatcher registration lives in `lib/src/daemon/` alongside `image_commands.dart`. Flutter-free handler; the terminal-spawn side publishes on a MessageBus channel the Claude extension subscribes to.
### UI
- **Welcome view**: list registered accounts; tap = bind current workspace; "Add account…" runs the login flow.
- **Claude pane chrome**: small badge showing the active account name. Tap opens the picker (issues `clide claude account set `).
- **Settings**: under "Claude", an Accounts row (manage registry) and a per-repo "Account for this workspace" row.
- **Workspace picker / project list**: each project shows its bound account.
### CLI parity (D-6)
Every UI action above issues a verb; every verb is observable via `clide claude account list`. No UI action lacks a CLI counterpart.
### Fallback / safety
- No binding use `Platform.environment[''CLAUDE_CONFIG_DIR'']` if the parent env already set it; otherwise leave it unset (Claude defaults to `~/.claude`). Same behaviour as today.
- Switching account = respawn the pane''s session, not a hot swap. Existing session closes (T-437 await-death). Transcript is preserved per-account in that account''s `projects/` dir; nothing is merged across accounts.
- Permission rules and `effortLevel` (per `claude_config.dart`) are config-dir-scoped already no new layering needed.
- D-64 (no phone-home): adding an account triggers exactly one CLI-initiated `claude login` browser flow, on explicit user action. No background calls.
## Acceptance
1. `clide claude account add work` registers an account at `~/.claude-work/`; `clide claude account login work` opens a terminal pane that completes OAuth and lands credentials in that dir.
2. `clide claude account set work` in a workspace binds it; respawning the Claude pane spawns `claude` with `CLAUDE_CONFIG_DIR=~/.claude-work/`. Verified by `ps eww` / `lsof` against that process showing the right env.
3. `clide claude account list` returns the registry + the current workspace''s binding (or `null`).
4. `clide`''s `/ide` bridge is reachable from the bound account''s `claude` its `~/.claude-work/ide/<clide-pid>.lock` is present and parseable; an in-session `/ide` resolves.
5. With no binding, behaviour is identical to today (regression test against the global `~/.claude`).
6. A second clide window opened on a differently-bound repo runs under a different account end-to-end; the two transcripts never cross.
7. Sign-out: removing the binding (`unset`) without `--purge` keeps the underlying dir untouched; with `--purge` removes it.
8. UI parity: every account action in the welcome view / settings / Claude pane chrome reaches the same dispatcher verb.
## Out of scope (for this story)
- Per-pane account selection within one workspace (forks/teammates on different accounts). One repo = one account in v1; revisit if needed.
- Cross-account credential sharing / symlinked settings (claude-account-switcher''s pattern). v1 keeps dirs fully isolated; revisit as a follow-up if duplication becomes painful.
- Team-mode account-budget surfacing across multiple accounts (T-158 territory).
- Windows behaviour beyond "if `CLAUDE_CONFIG_DIR` works on the platform, this works on the platform" no Windows-specific UX in v1.
- Auto-binding via `direnv` or `.envrc` integration. The IDE owns the binding; shell-side automation is a follow-up if asked for.
## References / sources
- Code: `lib/builtin/claude/src/agent_bootstrap.dart` (env-delta builder), `lib/builtin/claude/src/session_orchestrator.dart` (spawn site), `lib/src/env/shell_env.dart` (env-resolution pattern), `lib/src/ipc/mcp_server.dart:344` (IDE-lock writer), `lib/src/ipc/paths.dart:101` (`fnv1a64Hex` workspace hash), `lib/builtin/claude/src/claude_config.dart` (per-config-dir settings).
- Tickets: [T-356](#) (workspace settings user scope, dependency), [T-362](#) (`/ide` lock 0600), [T-437](#) (await-death respawn needed for switch-account-and-respawn), [T-249](#) (image-card CLI shape to mirror), D-6 (CLI parity), D-64 (no phone-home), D-70 (workspace hash), D-75 (avoid CC-internals coupling this work uses Claude Code''s *public* CLAUDE_CONFIG_DIR env, not internal state, so it is D-75-safe).
- External: anthropics/claude-code#44687 / #18435 / #36151 (no native switcher); third-party tools claude-swap, claude-account-switcher, claude-multiprofile, AgentsRoom (all use CLAUDE_CONFIG_DIR); CLAUDE_CONFIG_DIR is a documented Claude Code env knob.
', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-19 07:42:08.735', '2026-06-25 07:22:55', NULL, '4540cfc971fd1d78de1af5e02f1d3551', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FDXN3ZBRS6JK7Q8G6JVSPXFC', 'epic', NULL, 'Per-repo Claude account login (not system-wide)', 'Today clide uses Claude Code''s system-wide login (the CLI''s global auth), so every repo/workspace shares one Claude account. Allow selecting/logging into a separate Claude account per repo so different projects can run under different accounts (e.g. personal vs work). Falls back to the system login when no per-repo binding is set.
## Proven mechanism: `CLAUDE_CONFIG_DIR`
Claude Code reads `CLAUDE_CONFIG_DIR` at process start and uses that directory in place of `~/.claude` credentials, settings, sessions, plugins, projects, the `ide/` discovery dir, everything. Already proven on this machine: `~/.claude-personal/` exists alongside `~/.claude/` with the same layout (`.claude.json`, `sessions/`, `plugins/`, `projects/`, `settings.json`, `ide/`). A child `claude` process spawned with `CLAUDE_CONFIG_DIR=~/.claude-personal` uses that account; clide already has the spawn-time hook to set it.
## Alternatives surveyed (online research, 2026-06)
- **No native Claude Code account switcher** (open issues anthropics/claude-code#44687, #18435, #36151).
- **claude-swap, claude-account-switcher, claude-multiprofile** third-party CLIs; all converge on `CLAUDE_CONFIG_DIR` swapping the active dir or symlinking shared bits (settings/commands/MCP) across per-account dirs.
- **direnv / shell aliases** user-shell automation that exports `CLAUDE_CONFIG_DIR` on cd-in. Not IDE-owned, only works from a terminal launch.
- **AgentsRoom** is the closest peer: "per-project pin, per-agent override, in-app sign-in flow, status badge per account" same mechanism, IDE surface. The shape clide should match.
No cleaner mechanism exists today; everything credible reduces to `CLAUDE_CONFIG_DIR`. The clide value-add is the IDE-native shell around it (per-repo binding + sign-in UX + IDE-bridge plumbing).
## Recommended approach
**Per-account named config dirs**, **per-repo named binding**, **CLI-driven sign-in**. Three concepts:
1. **Accounts** one Claude config dir per account, e.g. `~/.claude-personal/`, `~/.claude-work/`. Selecting an account = exporting `CLAUDE_CONFIG_DIR=<that dir>` for the spawned `claude`. Multiple repos can share one account (no extra sign-in to add a third repo for the same account).
2. **Repo binding** `<workspace>` `<account-name>`, stored in user-scope clide settings (the [T-356](#) relocation lands settings under user scope keyed by workspace-path hash same `fnv1a64Hex` from `lib/src/ipc/paths.dart`). Unset falls back to the global `~/.claude`.
3. **Sign-in** clide invokes `CLAUDE_CONFIG_DIR=<target> claude login` in a managed terminal pane; the CLI owns the OAuth browser bounce; credentials land in the target dir. No new auth code in clide.
Account workspace is the right axis: "work" is one identity used in 5 repos, not 5 sign-ins.
## Implementation
### Storage (where the binding lives)
- `accounts` list (name config-dir path) in **user-scope** clide settings after [T-356](#) relocates `.clide/settings.yaml` to user scope.
- `claude.account.<workspace-hash>` keys map a workspace to an account name. Workspace hash = `fnv1a64Hex(canonicalPath)` (`lib/src/ipc/paths.dart:101`), same key D-70 uses for sockets.
- Bootstrap migration: if `~/.claude-personal/` (or other `~/.claude-*/`) is detected on first run, offer to register it as an account.
### Spawn-time env injection
One resolver pattern matches `lib/src/env/shell_env.dart` (`primeLoginShellPath` / `resolvedToolPath`):
- `String? claudeConfigDirForWorkspace(String cwd)` returns the resolved config-dir path, or null when no binding (use Claude''s default).
- Inject in `lib/builtin/claude/src/agent_bootstrap.dart` (`agentBootstrap`): when the resolver returns a path, add `CLAUDE_CONFIG_DIR=<path>` to the returned `envDelta`. Single hook; every hosted session (primary/secondary/fork/teammate) inherits it because `ClaudeSessionOrchestrator._spawn` already calls `agentBootstrap(spec.cwd, base: spec.env)` at `session_orchestrator.dart:247`.
- `SpawnSpec.env` already exists (line 67) as a per-call override keep it so an explicit caller can win.
### IDE bridge discovery (the trap)
`lib/src/ipc/mcp_server.dart:344` writes the `/ide` lock at `$HOME/.claude/ide/<pid>.lock`. Claude Code with `CLAUDE_CONFIG_DIR=<X>` looks under `<X>/ide/` so today''s lock is invisible to a `claude` running with a custom dir, and clide''s IDE bridge can''t be reached.
Fix: when clide is using one or more non-default config dirs (any bound repo''s account default), write the discovery lock into **every** active config dir''s `ide/` (the default `$HOME/.claude/ide/` plus each `<account-dir>/ide/`). Same lock content; each is `0600`-scoped per [T-362](#). Cleanup tracks all written paths.
### Sign-in flow
One CLI verb (D-6 parity, mirrors [T-249](#) shape):
- `clide claude account add <name> [--dir <path>]` — registers an account. Default dir `~/.claude-<name>/`. Idempotent.
- `clide claude account login <name>` spawns `CLAUDE_CONFIG_DIR=<dir> claude login` in a managed terminal pane (`lib/builtin/terminal/`); the CLI handles OAuth.
- `clide claude account set <name>` bind the current workspace to `<name>`. Active Claude session(s) for this repo respawn under the new account (T-437 close-and-respawn pattern is already correct: `close` awaits real process death, `spawn` is idempotent per id).
- `clide claude account list` JSON: registered accounts + which (if any) is bound to this workspace.
- `clide claude account unset` clear the binding; falls back to default.
- `clide claude account remove <name>` registry remove; offers `--purge` to delete the underlying dir (off by default).
Dispatcher registration lives in `lib/src/daemon/` alongside `image_commands.dart`. Flutter-free handler; the terminal-spawn side publishes on a MessageBus channel the Claude extension subscribes to.
### UI
- **Welcome view**: list registered accounts; tap = bind current workspace; "Add account…" runs the login flow.
- **Claude pane chrome**: small badge showing the active account name. Tap opens the picker (issues `clide claude account set `).
- **Settings**: under "Claude", an Accounts row (manage registry) and a per-repo "Account for this workspace" row.
- **Workspace picker / project list**: each project shows its bound account.
### CLI parity (D-6)
Every UI action above issues a verb; every verb is observable via `clide claude account list`. No UI action lacks a CLI counterpart.
### Fallback / safety
- No binding use `Platform.environment[''CLAUDE_CONFIG_DIR'']` if the parent env already set it; otherwise leave it unset (Claude defaults to `~/.claude`). Same behaviour as today.
- Switching account = respawn the pane''s session, not a hot swap. Existing session closes (T-437 await-death). Transcript is preserved per-account in that account''s `projects/` dir; nothing is merged across accounts.
- Permission rules and `effortLevel` (per `claude_config.dart`) are config-dir-scoped already no new layering needed.
- D-64 (no phone-home): adding an account triggers exactly one CLI-initiated `claude login` browser flow, on explicit user action. No background calls.
## Acceptance
1. `clide claude account add work` registers an account at `~/.claude-work/`; `clide claude account login work` opens a terminal pane that completes OAuth and lands credentials in that dir.
2. `clide claude account set work` in a workspace binds it; respawning the Claude pane spawns `claude` with `CLAUDE_CONFIG_DIR=~/.claude-work/`. Verified by `ps eww` / `lsof` against that process showing the right env.
3. `clide claude account list` returns the registry + the current workspace''s binding (or `null`).
4. `clide`''s `/ide` bridge is reachable from the bound account''s `claude` its `~/.claude-work/ide/<clide-pid>.lock` is present and parseable; an in-session `/ide` resolves.
5. With no binding, behaviour is identical to today (regression test against the global `~/.claude`).
6. A second clide window opened on a differently-bound repo runs under a different account end-to-end; the two transcripts never cross.
7. Sign-out: removing the binding (`unset`) without `--purge` keeps the underlying dir untouched; with `--purge` removes it.
8. UI parity: every account action in the welcome view / settings / Claude pane chrome reaches the same dispatcher verb.
## Out of scope (for this story)
- Per-pane account selection within one workspace (forks/teammates on different accounts). One repo = one account in v1; revisit if needed.
- Cross-account credential sharing / symlinked settings (claude-account-switcher''s pattern). v1 keeps dirs fully isolated; revisit as a follow-up if duplication becomes painful.
- Team-mode account-budget surfacing across multiple accounts (T-158 territory).
- Windows behaviour beyond "if `CLAUDE_CONFIG_DIR` works on the platform, this works on the platform" no Windows-specific UX in v1.
- Auto-binding via `direnv` or `.envrc` integration. The IDE owns the binding; shell-side automation is a follow-up if asked for.
## References / sources
- Code: `lib/builtin/claude/src/agent_bootstrap.dart` (env-delta builder), `lib/builtin/claude/src/session_orchestrator.dart` (spawn site), `lib/src/env/shell_env.dart` (env-resolution pattern), `lib/src/ipc/mcp_server.dart:344` (IDE-lock writer), `lib/src/ipc/paths.dart:101` (`fnv1a64Hex` workspace hash), `lib/builtin/claude/src/claude_config.dart` (per-config-dir settings).
- Tickets: [T-356](#) (workspace settings user scope, dependency), [T-362](#) (`/ide` lock 0600), [T-437](#) (await-death respawn needed for switch-account-and-respawn), [T-249](#) (image-card CLI shape to mirror), D-6 (CLI parity), D-64 (no phone-home), D-70 (workspace hash), D-75 (avoid CC-internals coupling this work uses Claude Code''s *public* CLAUDE_CONFIG_DIR env, not internal state, so it is D-75-safe).
- External: anthropics/claude-code#44687 / #18435 / #36151 (no native switcher); third-party tools claude-swap, claude-account-switcher, claude-multiprofile, AgentsRoom (all use CLAUDE_CONFIG_DIR); CLAUDE_CONFIG_DIR is a documented Claude Code env knob.
', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-19 07:42:08.735', '2026-06-25 09:13:35', NULL, 'd011d22e026bdcad85b3d9c24b42a1e9', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49VYMPF18PYQXD9PCMHN8', 'task', '06FDXN3ZBRS6JK7Q8G6JVSPXFC', 'Per-repo Claude account: storage schema + registry + bootstrap migration', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-25 09:16:42', '2026-06-25 09:16:42', NULL, '8fd6413379c7a967f42a235d9fc73fd9', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W3V175EM4F8ZHC6JFM4', 'task', '06FDXN3ZBRS6JK7Q8G6JVSPXFC', 'Per-repo Claude account: spawn-time CLAUDE_CONFIG_DIR injection', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-25 09:16:42', '2026-06-25 09:16:42', NULL, 'fc7a3fe22de73d7156a9130b8d4dbba4', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W6GP535GR8XD1XEHYWG', 'task', '06FDXN3ZBRS6JK7Q8G6JVSPXFC', 'Per-repo Claude account: write /ide discovery lock into every active config dir', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-25 09:16:42', '2026-06-25 09:16:42', NULL, '654dd909e0f13faee777fff5874da73c', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W95HJK08BCRQSWFZBF4', 'story', '06FDXN3ZBRS6JK7Q8G6JVSPXFC', 'Per-repo Claude account: clide CLI verbs (account add/login/set/unset/list/remove)', NULL, 'backlog', 'medium', NULL, NULL, 'D-6', '2026-06-25 09:16:42', '2026-06-25 09:16:42', NULL, 'd086bd860664969736ec6f710b2ce99c', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49WBTT3ESK6QG0XZ1VN2G', 'story', '06FDXN3ZBRS6JK7Q8G6JVSPXFC', 'Per-repo Claude account: UI — Claude pane badge + welcome view accounts section', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-25 09:16:42', '2026-06-25 09:16:42', NULL, '569534589948f6660812b8448d5f48ea', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49WEE5N4PESF5G1G5JRHR', 'task', '06FDXN3ZBRS6JK7Q8G6JVSPXFC', 'Per-repo Claude account: settings UI rows (accounts registry + per-workspace binding)', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-25 09:16:42', '2026-06-25 09:16:42', NULL, '416467120e042783213c7c8b6392a171', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49VYMPF18PYQXD9PCMHN8', 'task', '06FDXN3ZBRS6JK7Q8G6JVSPXFC', 'Per-repo Claude account: storage schema + registry + bootstrap migration', 'Foundation for the multi-account epic (T-476): the user-scope persistence layer that every other child consumes.
## Scope
Two durable concerns, plus a first-run migration:
### Account registry
List of `(name, configDir)` pairs in user-scope clide settings the catalog of Claude accounts this user has set up. Account names are user-chosen (`personal`, `work`, `client-acme`); `configDir` is the `CLAUDE_CONFIG_DIR` path Claude Code will read from. Default dir for a new account is `~/.claude-<name>/`, but an explicit path is allowed (so an existing `~/.claude-personal/` can be adopted as-is).
### Workspace binding
`claude.account.<workspace-hash>` `<account-name>`. Workspace hash is the same FNV-1a 64-bit hex used by D-70 sockets (`fnv1a64Hex` in `lib/src/ipc/paths.dart:101`) single source of truth. Unset binding means "use Claude''s default" (no `CLAUDE_CONFIG_DIR` injection).
### Bootstrap migration
On first run, scan `~` for directories matching `.claude-*` that look like a Claude config dir (have a `.claude.json` or `sessions/` inside). Surface the list actually offering to register them is welcome-view UX (T-481); this ticket just provides the probe.
## Where
- `SettingsStore` schema additions in `lib/kernel/src/settings.dart` (or wherever the schema-driven settings layer lands coordinate with T-356).
- A new `lib/builtin/claude/src/account_registry.dart` (or similar) holding the typed reader/writer + the migration probe.
- Flutter-free; unit-tested directly.
## API shape (sketch refine in implementation)
```
class AccountRegistry {
List<Account> get accounts;
Account? accountForWorkspace(String cwd);
Account? accountByName(String name);
void registerAccount(String name, String dir);
void removeAccount(String name);
void bindWorkspace(String cwd, String name);
void unbindWorkspace(String cwd);
}
List<DetectedAccount> probeExistingAccountDirs(String home);
```
All persistence flows through SettingsStore so the existing `.clide/settings.yaml` (or post-T-356 user-scope equivalent) reload/watch/serialize semantics carry over for free.
## Acceptance
- Schema declares `claude.accounts` (list of `{name, dir}`) and `claude.account.<workspace-hash>` (string).
- `accountForWorkspace(cwd)` resolves an account, or null when no binding.
- Registry CRUD (register/remove/bind/unbind) round-trips through SettingsStore.
- Bootstrap probe returns existing `~/.claude-*` dirs as candidates without mutating anything.
- Unit tests cover registry CRUD, workspace-hash mapping (canonicalised cwd, same hash for trailing-slash variants), probe behaviour (skips non-config dirs).
- No process spawning, no UI, no CLI those are downstream tickets.
## Depends on
- T-356 (settings user scope). The binding keys are PER-USER, not per-repo (you don''t want "which account does this repo use" committed to the repo). Land after T-356 ships its user-scope settings store.
## Consumers
- T-478 (env injection) reads `accountForWorkspace(cwd)` at spawn time.
- T-479 (IDE-lock multiplex) enumerates active accounts to write per-dir locks.
- T-480 (CLI verbs) registry mutations behind every account command.
- T-481 (UI) renders registry + binding state; calls the CRUD via T-480.
- T-482 (settings UI) schema-driven rows hit these keys.
', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-25 09:16:42', '2026-06-25 10:24:31', NULL, 'a4d5805c36720ecdccba19387cc28b5d', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W3V175EM4F8ZHC6JFM4', 'task', '06FDXN3ZBRS6JK7Q8G6JVSPXFC', 'Per-repo Claude account: spawn-time CLAUDE_CONFIG_DIR injection', 'The load-bearing piece of the multi-account epic (T-476): get the right `CLAUDE_CONFIG_DIR` into the hosted Claude process''s environment. Once this is in, a workspace bound to an account spawns its `claude` under that account end-to-end.
## Where
One resolver + one hook pattern follows `lib/src/env/shell_env.dart` (single source of truth for env-related concerns).
### New resolver
`String? claudeConfigDirForWorkspace(String cwd)`:
1. Look up the binding via the T-477 `AccountRegistry`.
2. If bound return the account''s `configDir`.
3. If unbound return `Platform.environment[''CLAUDE_CONFIG_DIR'']` when the parent already set it (respect launcher choice); otherwise `null` (Claude defaults to `~/.claude`).
Flutter-free, pure (registry injected), unit-tested.
### Injection point
`lib/builtin/claude/src/agent_bootstrap.dart`''s `agentBootstrap(workspaceRoot, base:)`: when the resolver returns non-null, merge `''CLAUDE_CONFIG_DIR'': <path>` into the returned `envDelta` BEFORE the `base` spread, so `SpawnSpec.env` keeps its per-call-override precedence (line 67 of `session_orchestrator.dart` already passes the override as `base`).
Every hosted session (primary / secondary / fork / teammate) inherits the binding because `ClaudeSessionOrchestrator._spawn` already routes through `agentBootstrap` at `session_orchestrator.dart:247` no per-call-site work.
## Acceptance
1. A bound workspace spawns `claude` with `CLAUDE_CONFIG_DIR=<account-dir>` verifiable by `ps eww` / `lsof` against the spawned process.
2. An unbound workspace spawns identically to today no `CLAUDE_CONFIG_DIR` injected unless the parent env already had one.
3. `SpawnSpec.env` per-call override still wins (precedence: explicit override > workspace binding > parent env > unset).
4. The resolver returns `null` for unbound workspaces with no parent env; the bootstrap then omits the key (NOT writes an empty value).
5. Unit tests: resolver returns each of the four states; `envDelta` precedence is correct; agent_bootstrap unit tests already cover the merge shape extend them.
## Depends on
- T-477 (storage). The resolver needs `AccountRegistry.accountForWorkspace(cwd)`.
## Out of scope
- Restarting an already-running session on binding change that lives in T-480''s `set` verb (it owns the close-then-spawn flow).
- IDE bridge discovery for the bound account T-479.
', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-25 09:16:42', '2026-06-25 10:24:31', NULL, 'd53dd07f63087be98dd6bb9d2e08ed16', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W6GP535GR8XD1XEHYWG', 'task', '06FDXN3ZBRS6JK7Q8G6JVSPXFC', 'Per-repo Claude account: write /ide discovery lock into every active config dir', 'Make clide''s `/ide` MCP bridge reachable from a `claude` running with a custom `CLAUDE_CONFIG_DIR`. Without this, the per-repo account work (T-476) ships a `claude` that can''t see clide''s IDE bridge.
## The trap
`lib/src/ipc/mcp_server.dart:344` writes the discovery lock at `$HOME/.claude/ide/<pid>.lock`. Claude Code with `CLAUDE_CONFIG_DIR=<X>` looks for `/ide` locks under `<X>/ide/` so a bound Claude session never finds clide today.
## Fix
Extend `_writeDiscoveryFile` (and the cleanup path) to write the lock into **every CURRENTLY-active config dir''s `ide/`**. "Active" means the union of:
- The default `$HOME/.claude/ide/` (always).
- Every account `configDir/ide/` for accounts bound to a workspace that is currently open in this clide process.
Same lock content for each; same `0600` perms per T-362. The MCP server tracks every path it wrote so shutdown cleans them all.
### React to binding changes
The set of active config dirs changes when a workspace is opened/closed in this process, or when a binding is set/unset (T-480 verbs). The MCP server subscribes to these changes and synchronizes the lock set incrementally write a new lock when a dir becomes active, remove it when no remaining workspace references that dir.
## Where
- `lib/src/ipc/mcp_server.dart` extend `_writeDiscoveryFile`, add a tracked-paths set, hook into the shutdown sweep.
- Wire the active-dir source through the existing app boot the MCP server is constructed in `lib/main.dart`; that''s where it can be handed an `AccountRegistry` (T-477) and a workspace-open observer.
## Acceptance
1. With no bindings, behaviour is identical to today (single lock at `$HOME/.claude/ide/<pid>.lock`).
2. With one or more workspaces bound to non-default accounts, a lock exists at `<each-account-dir>/ide/<pid>.lock` with the same content and `0600` perms.
3. Binding a workspace to a new (previously unused) account at runtime causes the corresponding lock to appear; unbinding the last workspace using that account causes it to disappear.
4. clide shutdown removes every lock this process wrote (defaultes plus account dirs); no orphans.
5. A live `claude` running under a bound account finds clide''s `/ide` endpoint end-to-end (manual confirmation in a real session).
6. Tests cover the multi-dir write/cleanup + the dynamic add/remove path (mock the filesystem).
## Depends on
- T-477 (storage). Needs the registry + binding API to enumerate active dirs.
## Independent of
T-478 T-479 can land in parallel; the bound `claude` just won''t have an IDE bridge until both ship.
', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-25 09:16:42', '2026-06-25 10:24:31', NULL, '4c08df52456d6eac49d3490c70c3cd41', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W95HJK08BCRQSWFZBF4', 'story', '06FDXN3ZBRS6JK7Q8G6JVSPXFC', 'Per-repo Claude account: clide CLI verbs (account add/login/set/unset/list/remove)', 'The `clide claude account …` dispatcher and verbs — D-6 parity for the multi-account feature (T-476). UI tickets (T-481 / T-482) call these verbs only; no UI writes the registry directly.
## Verbs
- `clide claude account add <name> [--dir <path>]` — register an account. Default dir `~/.claude-<name>/`. Idempotent (re-add with the same args is a no-op; conflicting args returns a clear userError). Does NOT spawn `claude login` — composability with `login`.
- `clide claude account list` JSON: `{accounts: [{name, dir}], boundAccount: <name|null>, detected: [<dir>...]}`. `boundAccount` is for the current workspace; `detected` is the T-477 probe output (existing `~/.claude-*/` dirs not yet registered).
- `clide claude account login <name>` spawn `CLAUDE_CONFIG_DIR=<dir> claude login` in a managed terminal pane (`lib/builtin/terminal/`). Claude CLI owns the OAuth browser bounce. Verb returns once the pane spawns; OAuth completion is observed by the pane lifecycle, not awaited by the verb.
- `clide claude account set <name>` bind the current workspace AND respawn the active Claude pane(s) on the new account. Respawn flow uses `ClaudeSessionOrchestrator.close` (which already awaits real process death per T-437) followed by a fresh `spawn` with the same id. Honest userError if `<name>` isn''t registered.
- `clide claude account unset` clear the binding for this workspace; respawn pane(s) on the default account. Emits a binding-removed event so T-479''s lock multiplexer can prune the matching `ide/` lock.
- `clide claude account remove <name> [--purge]` — registry-remove; refuses if any open workspace is bound to it (with a clear message). `--purge` additionally `rm -rf`s the `configDir` after registry removal (off by default; require confirmation).
## Where
- Dispatcher: new `lib/src/daemon/claude_account_commands.dart` mirror the shape of `image_commands.dart` (CommandSchema-declared positionals + flags, Flutter-free handler).
- A MessageBus channel e.g. `accountActionChannel = ''claude.account''` for non-CLI consumers (the Claude extension + UI) to observe registry/binding changes.
- Registers in `DaemonDispatcher` alongside the existing `image.show` / `pql.*` verbs.
## Acceptance
1. All six verbs round-trip through `clide capabilities` and execute against the live dispatcher.
2. `clide claude account add work` registers and persists across clide restarts.
3. `clide claude account list` returns the registry + this workspace''s binding + the detected-dirs probe results.
4. `clide claude account login work` opens a managed terminal pane in which `claude login` runs against `~/.claude-work/`; OAuth completion writes credentials into that dir.
5. `clide claude account set work` while a Claude pane is open: pane respawns with `CLAUDE_CONFIG_DIR=~/.claude-work/` (verified end-to-end against the live process).
6. `clide claude account unset` returns the pane to the default config dir; the matching `<dir>/ide/<pid>.lock` (T-479) is removed.
7. `clide claude account remove work` refuses while a workspace is bound; succeeds once unset. `--purge` removes the underlying directory after registry removal.
8. Tests follow the `image_commands` shape: schema declaration, Flutter-free handler tests, dispatcher registration check.
## Depends on
- T-477 (storage / registry).
- T-478 (env injection) so the respawn under `set`/`unset` actually changes which account the new process uses.
## Decision ref
D-6 (CLI parity).
', 'backlog', 'medium', NULL, NULL, 'D-6', '2026-06-25 09:16:42', '2026-06-25 10:24:31', NULL, '325e8454066430d01e7fe256dcbf7785', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49WBTT3ESK6QG0XZ1VN2G', 'story', '06FDXN3ZBRS6JK7Q8G6JVSPXFC', 'Per-repo Claude account: UI — Claude pane badge + welcome view accounts section', 'The conversational/chrome UI surfaces for the multi-account epic (T-476). Settings-UI rows live in T-482; this ticket owns the always-visible affordances.
## Claude pane chrome badge
Small badge in the pane chrome (next to the existing status/banner row see `lib/builtin/claude/src/claude_banner.dart` / status-bar pattern) showing the active account name, or "default" when unbound.
- Tap opens a popover/picker: list of registered accounts (radio-selectable) + "Add account…" entry.
- Selecting an account dispatches `clide claude account set <name>`; "Add account…" runs the add+login flow.
- The badge live-updates from the `claude.account` MessageBus channel published by T-480.
- Colour-coded subtly (one accent per account name? Hash-derived?) so the user can spot at a glance which window is which account.
## Welcome view: Claude accounts section
New section in the welcome view (peer of the existing project / toolchain rows in `lib/builtin/welcome/src/welcome_view.dart`):
- **Registered accounts** one row per registered account: name, configDir path, a per-row affordance to bind this workspace / unbind / re-login.
- **Detected (not yet registered)** rows for the T-477 bootstrap probe results (existing `~/.claude-*/` dirs not in the registry yet). Each has a "Register this" affordance that dispatches `clide claude account add <name> --dir <detected-dir>`.
- **Add new account** button that runs the full add+login flow against a default `~/.claude-<name>/` path.
All actions go through T-480''s CLI verbs; this widget never writes the registry directly.
## Constraints
- Use the existing UI-design vocabulary (theme tokens, control geometry see the `ui-design` skill). No Material/Cupertino.
- The badge is display-with-affordance, not an inline interaction surface in the D-78 sense opening the picker is a navigation gesture, the selection happens in the picker modal.
- The welcome view follows the existing one-screen welcome-layout decisions (don''t add a fold-out / accordion if it conflicts with the current layout).
## Acceptance
1. The Claude pane badge shows the active account name (or "default"), updates live across `set`/`unset`/`add` actions.
2. Tapping the badge opens a picker; selecting a registered account dispatches `clide claude account set` and respawns the pane on the new account.
3. The welcome view''s accounts section lists registered accounts + detected candidates + an add-new affordance.
4. Every action available in the UI is reachable as the corresponding `clide claude account` verb (D-6 parity verified by inspection).
5. Widget tests cover badge rendering for each state (default / bound / unknown) + the picker action dispatch (mock the CLI client).
6. A11y: badge has a semantic label naming the account; picker is keyboard-reachable.
## Depends on
- T-480 (CLI verbs). Wires every action to the dispatcher.
## Out of scope
- Settings-UI rows T-482.
- Project-picker integration (each project shows its bound account) desirable, file as a follow-up if it grows complex.
', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-25 09:16:42', '2026-06-25 10:24:31', NULL, '39b5f602a56a08641b7e247b01de8a6e', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49WEE5N4PESF5G1G5JRHR', 'task', '06FDXN3ZBRS6JK7Q8G6JVSPXFC', 'Per-repo Claude account: settings UI rows (accounts registry + per-workspace binding)', 'Settings UI surface for the multi-account epic (T-476): two schema-driven rows under the "Claude" settings category.
## Rows
### Global: "Claude > Accounts"
Registry CRUD list one entry per registered account. Each entry shows: name, configDir, current sign-in status (a small "signed in" / "not signed in" indicator derived from whether `<dir>/.claude.json` carries an active credential read-only probe, no auth state mutation here). Per-entry affordances: re-login, remove (with `--purge` confirmation). Plus an "Add account…" affordance.
### Per-workspace: "Claude > Account for this workspace"
Dropdown/select listing registered accounts + a "(default)" option. Selecting issues `clide claude account set <name>` (or `unset` for default).
## Where
- Hooks into the schema-driven settings panel coming from T-8 / settings UI. Until that lands, this row set may need to render in a placeholder host.
- All actions go through T-480''s CLI verbs.
- Scope-tag icon for each row per the 2026-06-10 settings convention (folder = project; globe = always/global) both rows are present, distinguishing scope visually.
## Acceptance
1. Global Accounts row renders the registry, supports add / re-login / remove (with purge confirmation).
2. Per-workspace row renders the current binding and lets the user switch / clear it.
3. Both rows reflect live changes from the `claude.account` MessageBus channel (T-480) without a settings reload.
4. All actions issue T-480 verbs no direct settings writes from this UI.
5. Widget tests for the row renderers in each state.
## Depends on
- T-480 (CLI verbs) for the action layer.
- T-477 (storage) for the schema registration.
- Gated on T-8''s settings UI maturity until the schema-driven panel can host these rows, this ticket parks. Track T-8 progress before activating.
## Out of scope
- The Claude pane chrome badge + welcome view T-481.
', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-25 09:16:42', '2026-06-25 10:24:31', NULL, 'a699defe53d009c44e5af7f9f38a0c15', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);