feat(claude): Settings → Claude per-workspace account picker (T-482 part 1)
A custom settings control under the Claude category: a dropdown of the registered accounts plus Default. Picking one binds (or unbinds) this workspace via the AccountRegistry and publishes set/unset on accountActionChannel — the same channel the CLI verbs use, so the session respawns onto the account (T-480) and the IDE lock re-syncs (T-479). It reads live off the shared settings notifier, so a CLI `account set` updates the dropdown too. Empty states cover no-workspace and no-accounts, each pointing at the CLI. Registry writes set the in-memory binding synchronously then flush, so the control publishes the bus event before the disk write completes. Part 2 (the global Accounts registry CRUD list + sign-in probe) is still open on T-482. en/nl strings added; widget test covers the states + bind/unbind. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<void> 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 = <Map<String, Object?>>[];
|
||||
final sub = f.services.messages.subscribe(channel: accountActionChannel).listen((m) => published.add(m.data));
|
||||
addTearDown(sub.cancel);
|
||||
|
||||
await pump(tester);
|
||||
await tester.pump();
|
||||
expect(find.textContaining('Default'), findsOneWidget, reason: 'anchor shows Default while unbound');
|
||||
|
||||
await tester.tap(find.byType(ClaudeWorkspaceAccountControl));
|
||||
await tester.pump();
|
||||
expect(find.text('work'), findsOneWidget);
|
||||
expect(find.text('personal'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('work'));
|
||||
await tester.pump();
|
||||
expect(reg.boundName(f.tempDir.path), 'work');
|
||||
expect(published.single['action'], 'set');
|
||||
expect(published.single['name'], 'work');
|
||||
});
|
||||
|
||||
testWidgets('picking Default while bound unbinds the workspace and publishes unset', (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.bindWorkspace(f.tempDir.path, 'work');
|
||||
});
|
||||
|
||||
final published = <Map<String, Object?>>[];
|
||||
final sub = f.services.messages.subscribe(channel: accountActionChannel).listen((m) => published.add(m.data));
|
||||
addTearDown(sub.cancel);
|
||||
|
||||
await pump(tester);
|
||||
await tester.pump();
|
||||
await tester.tap(find.byType(ClaudeWorkspaceAccountControl));
|
||||
await tester.pump();
|
||||
await tester.tap(find.textContaining('Default'));
|
||||
await tester.pump();
|
||||
expect(reg.boundName(f.tempDir.path), isNull);
|
||||
expect(published.single['action'], 'unset');
|
||||
expect(published.single['previous'], 'work');
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user