permission-mode control beside the Claude composer (T-275)
Build the T-275 picker on the new popover primitive (D-88): an icon-only, per-mode-coloured button trailing the composer text box opens a ClideMenu of the safe trio (default/acceptEdits/plan, active marked) plus a divided, disabled bypass row (the footgun stays behind the cockpit guard, T-181). The label lives in the tooltip, the menu, and the status bar — the resting button is the glyph alone. Coexists with the composer's Stop row when busy. - new permission_mode_control.dart (PermissionModeControl + per-mode icon/colour helpers); shieldCheck/shieldWarning glyphs added to PhosphorIcons. - claude_composer.dart: permissionMode + onSetPermissionMode props; control trails the text box (bottom-aligned), shown only when wired. - claude_pane.dart: pass the current mode + a setter; demote the status-bar _ModeBadge to a passive, per-mode-coloured text indicator (no click). Ctrl/Cmd+M still cycles (onCycleMode unchanged). Regenerated the phosphor-glyphs reference (47 defined). Tests: menu opens with the trio + disabled bypass, select sets the mode, helpers map colours/icons, control coexists with Stop, hidden when no mode. Note: claude_pane.dart also carries the earlier T-274 resume diagnostic log line (uncommitted in the working tree, reviewed as benign) — it rides along here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
/// Tests for the composer permission-mode control (T-275): opens a menu of the
|
||||
/// safe trio + a disabled bypass row, selecting sets the mode, and it coexists
|
||||
/// with the composer's Stop row while busy.
|
||||
library;
|
||||
|
||||
import 'package:clide/builtin/claude/src/claude_composer.dart';
|
||||
import 'package:clide/builtin/claude/src/permission_mode_control.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../helpers/kernel_fixture.dart';
|
||||
import '../../helpers/widget_harness.dart';
|
||||
|
||||
void main() {
|
||||
group('PermissionModeControl', () {
|
||||
late KernelFixture f;
|
||||
setUp(() async => f = await KernelFixture.create());
|
||||
tearDown(() async => f.dispose());
|
||||
|
||||
Future<void> pump(WidgetTester tester, String mode, ValueChanged<String> onSelect) {
|
||||
return tester.pumpWidget(harness(
|
||||
f,
|
||||
Align(alignment: Alignment.center, child: PermissionModeControl(mode: mode, onSelect: onSelect)),
|
||||
));
|
||||
}
|
||||
|
||||
testWidgets('opens a menu of the safe trio + a bypass row; selecting sets the mode', (tester) async {
|
||||
var picked = '';
|
||||
await pump(tester, 'default', (m) => picked = m);
|
||||
await tester.pump();
|
||||
expect(find.text('accept-edits'), findsNothing, reason: 'menu starts closed');
|
||||
|
||||
await tester.tap(find.byType(PermissionModeControl));
|
||||
await tester.pump();
|
||||
expect(find.text('default'), findsOneWidget);
|
||||
expect(find.text('accept-edits'), findsOneWidget);
|
||||
expect(find.text('plan'), findsOneWidget);
|
||||
expect(find.text('bypass'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('plan'));
|
||||
await tester.pump();
|
||||
expect(picked, 'plan');
|
||||
expect(find.text('accept-edits'), findsNothing, reason: 'menu closes on select');
|
||||
});
|
||||
|
||||
testWidgets('the bypass row is disabled — selecting it does nothing', (tester) async {
|
||||
var picked = '';
|
||||
await pump(tester, 'default', (m) => picked = m);
|
||||
await tester.pump();
|
||||
await tester.tap(find.byType(PermissionModeControl));
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('bypass'));
|
||||
await tester.pump();
|
||||
expect(picked, '', reason: 'bypass stays behind the cockpit guard (T-181)');
|
||||
});
|
||||
|
||||
testWidgets('per-mode helpers map labels/colours/icons', (tester) async {
|
||||
final tokens = f.services.theme.current.surface;
|
||||
expect(permissionModeColor('acceptEdits', tokens), tokens.statusWarning);
|
||||
expect(permissionModeColor('plan', tokens), tokens.globalFocus);
|
||||
expect(permissionModeColor('bypassPermissions', tokens), tokens.statusError);
|
||||
expect(permissionModeColor('default', tokens), tokens.globalTextMuted);
|
||||
expect(permissionModeIcon('default'), isNot(equals(permissionModeIcon('plan'))));
|
||||
});
|
||||
});
|
||||
|
||||
group('ClaudeComposer + mode control', () {
|
||||
late KernelFixture f;
|
||||
setUp(() async => f = await KernelFixture.create());
|
||||
tearDown(() async => f.dispose());
|
||||
|
||||
testWidgets('the mode control and the Stop row coexist while busy', (tester) async {
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
ClaudeComposer(
|
||||
onSubmit: (_) {},
|
||||
busy: true,
|
||||
onInterrupt: () {},
|
||||
permissionMode: 'default',
|
||||
onSetPermissionMode: (_) {},
|
||||
),
|
||||
));
|
||||
await tester.pump();
|
||||
// Stop affordance (busy row) and the trailing mode control are both present.
|
||||
expect(find.textContaining('Stop'), findsOneWidget);
|
||||
expect(find.byType(PermissionModeControl), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('no mode control when permissionMode is null', (tester) async {
|
||||
await tester.pumpWidget(harness(f, ClaudeComposer(onSubmit: (_) {})));
|
||||
await tester.pump();
|
||||
expect(find.byType(PermissionModeControl), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user