feat(claude): reach bypass via Ctrl/Cmd+Shift+M + shift-click (T-510)

Restores the original intent that T-226's refinement wording lost: the
shift modifier is the explicit opt-in for bypassPermissions, not a
separate confirm flow. Plain Ctrl/Cmd+M keeps cycling the safe trio;
Ctrl/Cmd+Shift+M cycles the full list. The composer menu's bypass row
was permanently disabled, deferring to "the cockpit's confirmed path" —
but that roster is ghost-fed (T-396), so bypass was unreachable from
the primary session's UI entirely. The row now no-ops on a plain click
(menu stays open) and selects on shift-click, with a hint naming the
gesture. Roster badge and /permissions paths unchanged.

lib/test_app.dart is a format-only follow-up to the previous commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-05 12:54:11 +02:00
co-authored by Claude Fable 5
parent 0f1d15efdd
commit a15b3fd098
14 changed files with 175 additions and 29 deletions
@@ -373,6 +373,39 @@ void main() {
await tester.pump();
expect(cycles, 0);
});
testWidgets('Ctrl+Shift+M fires onCycleModeFull, not onCycleMode (T-510)', (tester) async {
var safe = 0, full = 0;
await tester.pumpWidget(harness(f, ClaudeComposer(onSubmit: (_) {}, onCycleMode: () => safe++, onCycleModeFull: () => full++)));
await tester.tap(find.byType(EditableText));
await tester.pump();
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.keyM);
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.pump();
expect(full, 1);
expect(safe, 0);
expect(tester.widget<EditableText>(find.byType(EditableText)).controller.text, isEmpty);
});
testWidgets('plain Ctrl+M still cycles the safe trio only', (tester) async {
var safe = 0, full = 0;
await tester.pumpWidget(harness(f, ClaudeComposer(onSubmit: (_) {}, onCycleMode: () => safe++, onCycleModeFull: () => full++)));
await tester.tap(find.byType(EditableText));
await tester.pump();
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.keyM);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.pump();
expect(safe, 1);
expect(full, 0);
});
});
group('ClaudeComposer external focus node (T-227)', () {
@@ -90,6 +90,20 @@ void main() {
});
});
group('nextPermissionMode (T-510)', () {
test('cycles the full list including bypass and wraps', () {
expect(nextPermissionMode('default'), 'acceptEdits');
expect(nextPermissionMode('acceptEdits'), 'plan');
expect(nextPermissionMode('plan'), 'bypassPermissions');
expect(nextPermissionMode('bypassPermissions'), 'default');
});
test('unknown restarts at default', () {
expect(nextPermissionMode('whatever'), 'default');
expect(kFullPermissionCycle, contains('bypassPermissions'));
});
});
group('statusSegmentsAroundMode (T-226)', () {
test('splits model (leading) from ctx/cost/rate (trailing), mode excluded', () {
const s = SessionStatus(model: 'claude-opus-4-7', permissionMode: 'plan', contextTokens: 21000, cost: 0.05);
@@ -1,10 +1,11 @@
/// 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.
/// safe trio + a shift-click-gated bypass row (T-510), 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/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -48,7 +49,7 @@ void main() {
expect(find.text('accept-edits'), findsNothing, reason: 'menu closes on select');
});
testWidgets('the bypass row is disabled — selecting it does nothing', (tester) async {
testWidgets('a plain click on the bypass row does nothing and keeps the menu open', (tester) async {
var picked = '';
await pump(tester, 'default', (m) => picked = m);
await tester.pump();
@@ -56,7 +57,24 @@ void main() {
await tester.pump();
await tester.tap(find.text('bypass'));
await tester.pump();
expect(picked, '', reason: 'bypass stays behind the cockpit guard (T-181)');
expect(picked, '', reason: 'bypass is never a plain click away (T-510)');
expect(find.text('bypass'), findsOneWidget, reason: 'menu stays open for a retry with shift');
});
testWidgets('shift-click on the bypass row selects it and closes the menu (T-510)', (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.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
await tester.tap(find.text('bypass'));
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
await tester.pump();
expect(picked, 'bypassPermissions');
expect(find.text('bypass'), findsNothing, reason: 'menu closes after the shift-select');
});
testWidgets('per-mode helpers map labels/colours/icons', (tester) async {