diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql
index 6ce13626..d72a3a58 100644
--- a/.pql/changelog/ticket_history/2026-06.sql
+++ b/.pql/changelog/ticket_history/2026-06.sql
@@ -6951,3 +6951,77 @@ T-478 — T-479 can land in parallel; the bound `claude` just won''t have an IDE
Done: McpServer writes the /ide discovery lock into every active config dir (default ~/.claude/ide + the bound account''s
/ide), reconciled by syncDiscoveryLocks() on start + on every accountActionChannel event, all cleaned on stop. boundConfigDir injected from main.dart via AccountRegistry. Acceptance 1-4,6 covered by test/ipc/mcp_server_test.dart; #5 (live claude finds the bridge end-to-end) is manual.', NULL, '2026-06-27 19:24:52', '2026-06-27 19:24:52.424', '2026-06-27 19:24:52.424', NULL, 'fc350455bedf3c3c4e8c61a5599cef33', 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', 'status', 'in_progress', 'done', NULL, '2026-06-27 19:24:52', '2026-06-27 19:24:52.456', '2026-06-27 19:24:52.456', NULL, '9a1ec766e6abf5712e32dc32ace8c02d', 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', 'status', 'backlog', 'in_progress', NULL, '2026-06-27 21:32:32', '2026-06-27 21:32:32.255', '2026-06-27 21:32:32.255', NULL, 'de26a987bbc6b273147ea85568816ef6', 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', '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 `/.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 ` (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.
+', '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 `/.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 ` (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.
+
+
+Slice 1 done: the per-workspace account picker (acceptance #2) — a custom SettingsControlContribution ''claude.workspace-account'' in the Claude settings category. Dropdown of registered accounts + Default; selecting binds/unbinds via AccountRegistry and publishes set/unset on accountActionChannel (respawn + lock sync follow). Live via the settings notifier. Empty/no-workspace states covered. Remaining (Slice 2): the global Accounts registry CRUD list with sign-in probe + add/re-login/remove(--purge) (acceptance #1).', NULL, '2026-06-27 21:46:33', '2026-06-27 21:46:33.584', '2026-06-27 21:46:33.584', NULL, '4f4f8d74d775750d79f054ba5821ce5a', 2) ON CONFLICT(hash) DO NOTHING;
diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql
index 3ea9ae00..44735351 100644
--- a/.pql/changelog/tickets/2026-06.sql
+++ b/.pql/changelog/tickets/2026-06.sql
@@ -8862,3 +8862,77 @@ T-478 — T-479 can land in parallel; the bound `claude` just won''t have an IDE
Done: McpServer writes the /ide discovery lock into every active config dir (default ~/.claude/ide + the bound account''s /ide), reconciled by syncDiscoveryLocks() on start + on every accountActionChannel event, all cleaned on stop. boundConfigDir injected from main.dart via AccountRegistry. Acceptance 1-4,6 covered by test/ipc/mcp_server_test.dart; #5 (live claude finds the bridge end-to-end) is manual.', 'done', 'medium', NULL, NULL, NULL, '2026-06-25 09:16:42', '2026-06-27 19:24:52.456', NULL, '8798b0b97587cdbac22b75004cad8f07', 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 `/.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 ` (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.
+', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-25 09:16:42', '2026-06-27 21:32:32.254', NULL, 'ccd495a63a508e1c6f2aa1e7585efee6', 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 `/.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 ` (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.
+
+
+Slice 1 done: the per-workspace account picker (acceptance #2) — a custom SettingsControlContribution ''claude.workspace-account'' in the Claude settings category. Dropdown of registered accounts + Default; selecting binds/unbinds via AccountRegistry and publishes set/unset on accountActionChannel (respawn + lock sync follow). Live via the settings notifier. Empty/no-workspace states covered. Remaining (Slice 2): the global Accounts registry CRUD list with sign-in probe + add/re-login/remove(--purge) (acceptance #1).', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-25 09:16:42', '2026-06-27 21:46:33.584', NULL, '31cf98ecc2a8e496cd9cb324fa75a404', 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);
diff --git a/CHANGELOG.md b/CHANGELOG.md
index adf5df3f..ed9a7342 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,9 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
### Added
+- **Settings → Claude → Account picker.** A per-workspace dropdown binds this
+ repo to a registered Claude account (or Default); switching respawns the pane
+ onto it. (T-482, epic T-476)
- **Per-repo Claude accounts — `clide claude account` verbs.** Manage named
Claude config dirs and bind one per workspace: `add`/`list`/`set`/`unset`/
`remove [--purge]`. `set`/`unset` respawn the workspace's Claude pane(s) onto
diff --git a/assets/i18n/en_us/builtin.claude.json b/assets/i18n/en_us/builtin.claude.json
index a71e4458..f9d2a10d 100644
--- a/assets/i18n/en_us/builtin.claude.json
+++ b/assets/i18n/en_us/builtin.claude.json
@@ -237,5 +237,8 @@
"settings.claude.effort.label": { "translation": "Effort" },
"settings.claude.effort.help": { "translation": "Reasoning effort for new sessions (applied via --effort at spawn)." },
"settings.claude.permissionMode.label": { "translation": "Permission mode" },
- "settings.claude.permissionMode.help": { "translation": "Starting permission mode for new sessions." }
+ "settings.claude.permissionMode.help": { "translation": "Starting permission mode for new sessions." },
+ "settings.claude.account.label": { "translation": "Account" },
+ "settings.claude.account.workspace.label": { "translation": "Account for this workspace" },
+ "settings.claude.account.workspace.help": { "translation": "Which Claude account this repo runs under; Default uses the system login." }
}
diff --git a/assets/i18n/nl_nl/builtin.claude.json b/assets/i18n/nl_nl/builtin.claude.json
index 861b7c73..6f75cef6 100644
--- a/assets/i18n/nl_nl/builtin.claude.json
+++ b/assets/i18n/nl_nl/builtin.claude.json
@@ -237,5 +237,8 @@
"settings.claude.effort.label": { "translation": "Effort" },
"settings.claude.effort.help": { "translation": "Reasoning effort voor nieuwe sessies (toegepast via --effort bij het starten)." },
"settings.claude.permissionMode.label": { "translation": "Permissiemodus" },
- "settings.claude.permissionMode.help": { "translation": "Begin-permissiemodus voor nieuwe sessies." }
+ "settings.claude.permissionMode.help": { "translation": "Begin-permissiemodus voor nieuwe sessies." },
+ "settings.claude.account.label": { "translation": "Account" },
+ "settings.claude.account.workspace.label": { "translation": "Account voor deze werkmap" },
+ "settings.claude.account.workspace.help": { "translation": "Welk Claude-account deze repo gebruikt; Standaard gebruikt de systeemlogin." }
}
diff --git a/lib/builtin/claude/src/account_settings_control.dart b/lib/builtin/claude/src/account_settings_control.dart
new file mode 100644
index 00000000..c6c1328d
--- /dev/null
+++ b/lib/builtin/claude/src/account_settings_control.dart
@@ -0,0 +1,134 @@
+import 'package:clide/builtin/claude/src/account_registry.dart';
+import 'package:clide/kernel/kernel.dart';
+import 'package:clide/src/daemon/claude_account_commands.dart' show accountActionChannel;
+import 'package:clide/widgets/widgets.dart';
+import 'package:flutter/widgets.dart';
+
+/// Settings control for "Account for this workspace" (T-482, epic T-476). A
+/// dropdown of the registered Claude accounts plus a Default option; picking one
+/// binds (or unbinds) the current workspace and publishes on
+/// [accountActionChannel] — the same channel the CLI `set`/`unset` verbs use, so
+/// the session respawns onto the account (T-480) and the IDE lock re-syncs
+/// (T-479). Registry writes flow through the shared [SettingsStore], whose
+/// notifier this control listens to, so it stays live for both UI and CLI edits.
+class ClaudeWorkspaceAccountControl extends StatefulWidget {
+ const ClaudeWorkspaceAccountControl({super.key});
+
+ @override
+ State createState() => _ClaudeWorkspaceAccountControlState();
+}
+
+class _ClaudeWorkspaceAccountControlState extends State {
+ final ClideOverlayController _overlay = ClideOverlayController();
+ SettingsStore? _settings;
+
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+ final settings = ClideKernel.maybeOf(context)?.settings;
+ if (identical(settings, _settings)) return;
+ _settings?.removeListener(_onChange);
+ _settings = settings;
+ _settings?.addListener(_onChange);
+ }
+
+ void _onChange() {
+ if (mounted) setState(() {});
+ }
+
+ @override
+ void dispose() {
+ _settings?.removeListener(_onChange);
+ _overlay.dispose();
+ super.dispose();
+ }
+
+ static const _defaultLabel = 'Default';
+
+ Future _bind(KernelServices services, String cwd, String? name) async {
+ _overlay.close();
+ final reg = AccountRegistry(services.settings);
+ // The registry write sets the in-memory binding synchronously, then flushes
+ // to disk; publish once the binding is live (before the flush completes) so
+ // the respawn (T-480) + lock-sync (T-479) consumers see the new state. The
+ // settings notifier rebuilds us once the write lands.
+ if (name == null) {
+ final previous = reg.boundName(cwd);
+ final write = reg.unbindWorkspace(cwd);
+ services.messages.publish('ui', accountActionChannel, {'action': 'unset', 'cwd': cwd, 'previous': previous});
+ await write;
+ } else {
+ final write = reg.bindWorkspace(cwd, name);
+ services.messages.publish('ui', accountActionChannel, {'action': 'set', 'name': name, 'cwd': cwd});
+ await write;
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final tokens = ClideSettings.theme.of(context).surface;
+ final services = ClideKernel.maybeOf(context);
+ final cwd = services?.settings.projectDir?.path;
+ if (services == null || cwd == null) {
+ return ClideText('Open a workspace to bind a Claude account.', fontSize: clideFontCaption, color: tokens.globalTextMuted);
+ }
+
+ final reg = AccountRegistry(services.settings);
+ final accounts = reg.accounts;
+ if (accounts.isEmpty) {
+ return ClideText('No accounts yet — add one with `clide claude account add `.', fontSize: clideFontCaption, color: tokens.globalTextMuted);
+ }
+
+ final boundName = reg.boundName(cwd);
+ final selectedLabel = boundName ?? _defaultLabel;
+ return ClideAnchoredOverlay(
+ controller: _overlay,
+ align: ClideAnchorAlign.start,
+ overlayBuilder: (ctx, c) => ClideMenu(
+ onClose: c.close,
+ entries: [
+ ClideMenuItem(
+ label: _defaultLabel,
+ active: boundName == null,
+ semanticLabel: 'Account for this workspace: $_defaultLabel',
+ onSelect: () => _bind(services, cwd, null),
+ ),
+ const ClideMenuSeparator(),
+ for (final a in accounts)
+ ClideMenuItem(
+ label: a.name,
+ active: a.name == boundName,
+ semanticLabel: 'Account for this workspace: ${a.name}',
+ onSelect: () => _bind(services, cwd, a.name),
+ ),
+ ],
+ ),
+ anchor: Semantics(
+ button: true,
+ label: 'Account for this workspace: $selectedLabel. Click to change.',
+ excludeSemantics: true,
+ onTap: _overlay.toggle,
+ child: ClideTappable(
+ cursor: SystemMouseCursors.click,
+ onTap: _overlay.toggle,
+ builder: (ctx, hovered, _) => Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
+ decoration: BoxDecoration(
+ color: tokens.panelBackground,
+ border: Border.all(color: hovered ? tokens.panelActiveBorder : tokens.dividerColor),
+ borderRadius: BorderRadius.circular(4),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ ClideText(selectedLabel, color: tokens.globalForeground),
+ const SizedBox(width: 6),
+ ClideIcon(PhosphorIcons.byName('caret-down'), size: 10, color: tokens.globalTextMuted),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/builtin/claude/src/extension.dart b/lib/builtin/claude/src/extension.dart
index 19b86cb8..061c81a2 100644
--- a/lib/builtin/claude/src/extension.dart
+++ b/lib/builtin/claude/src/extension.dart
@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:clide/clide.dart';
import 'package:clide/builtin/claude/src/account_registry.dart';
+import 'package:clide/builtin/claude/src/account_settings_control.dart';
import 'package:clide/builtin/claude/src/activity_cluster.dart' show foldLevelFromName, kActivityFoldLevelKey, nextFoldLevel;
import 'package:clide/builtin/claude/src/claude_config.dart';
import 'package:clide/builtin/claude/src/claude_status.dart' show nextSafePermissionMode;
@@ -198,9 +199,31 @@ class ClaudeExtension extends ClideExtension {
),
],
),
+ // Per-repo Claude account (T-482, epic T-476). The dropdown binds this
+ // workspace to a registered account; manage the registry itself with
+ // the `clide claude account` verbs (T-480).
+ SettingsSection(
+ label: 'Account',
+ labelKey: 'settings.claude.account.label',
+ fields: [
+ SettingsField(
+ // Placeholder key — a custom field is rendered by its control,
+ // never stored here; kept clear of the app.claude.account.
+ // binding namespace the registry scans (T-480).
+ key: 'app.claude.workspaceAccount',
+ kind: SettingsFieldKind.custom,
+ label: 'Account for this workspace',
+ labelKey: 'settings.claude.account.workspace.label',
+ help: 'Which Claude account this repo runs under; Default uses the system login.',
+ helpKey: 'settings.claude.account.workspace.help',
+ customId: 'claude.workspace-account',
+ ),
+ ],
+ ),
],
),
),
+ SettingsControlContribution(id: 'claude.workspace-account', customId: 'claude.workspace-account', builder: (_) => const ClaudeWorkspaceAccountControl()),
// T-171: agent roster controls (D-6 CLI/UI parity).
// Usage: clide claude.agent.show
CommandContribution(
diff --git a/test/builtin/claude/account_settings_control_test.dart b/test/builtin/claude/account_settings_control_test.dart
new file mode 100644
index 00000000..ebbeab75
--- /dev/null
+++ b/test/builtin/claude/account_settings_control_test.dart
@@ -0,0 +1,98 @@
+/// T-482: the per-workspace Claude account picker settings control. Covers the
+/// no-workspace and no-accounts empty states, and the bind flow — the dropdown
+/// lists Default + registered accounts, and picking one binds the workspace and
+/// publishes the `set` action on accountActionChannel (driving respawn + lock
+/// sync, T-480/T-479).
+///
+/// Registry/settings writes are real file I/O, so seeding goes through
+/// [WidgetTester.runAsync] — awaiting it inside the fake-async body would hang.
+library;
+
+import 'package:clide/builtin/claude/src/account_registry.dart';
+import 'package:clide/builtin/claude/src/account_settings_control.dart';
+import 'package:clide/src/daemon/claude_account_commands.dart' show accountActionChannel;
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import '../../helpers/kernel_fixture.dart';
+import '../../helpers/widget_harness.dart';
+
+void main() {
+ late KernelFixture f;
+ setUp(() async => f = await KernelFixture.create());
+ tearDown(() async => f.dispose());
+
+ Future pump(WidgetTester tester) => tester.pumpWidget(
+ harness(
+ f,
+ const Align(
+ alignment: Alignment.center,
+ child: SizedBox(width: 320, child: ClaudeWorkspaceAccountControl()),
+ ),
+ ),
+ );
+
+ testWidgets('no workspace open → prompts to open one', (tester) async {
+ await pump(tester);
+ await tester.pump();
+ expect(find.textContaining('Open a workspace'), findsOneWidget);
+ });
+
+ testWidgets('workspace open but no accounts → hints at the CLI verb', (tester) async {
+ await tester.runAsync(() => f.services.settings.setProjectDir(f.tempDir));
+ await pump(tester);
+ await tester.pump();
+ expect(find.textContaining('No accounts yet'), findsOneWidget);
+ });
+
+ testWidgets('lists Default + accounts; picking one binds the workspace and publishes set', (tester) async {
+ final reg = AccountRegistry(f.services.settings);
+ await tester.runAsync(() async {
+ await f.services.settings.setProjectDir(f.tempDir);
+ await reg.registerAccount('work', '/home/u/.claude-work');
+ await reg.registerAccount('personal', '/home/u/.claude-personal');
+ });
+
+ final published =