number-key + Enter shortcuts for interactive prompt buttons (T-240)
In a ToolPromptCard, number keys pick the matching button/option (labels are prefixed 1./2./3.…) and Enter confirms the primary action — matching the Claude CLI. Permission: 1=Allow, 2=Allow&remember (when offered) else Deny, 3=Deny. AskUserQuestion: 1..N select/toggle the current question's options + Other. The card autofocuses and the key handler self-guards on hasPrimaryFocus, so once the user clicks into a note field the digits type normally and never fire a button. Shared the permission/option actions between the buttons and the keys. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import 'package:clide/builtin/claude/src/prompt_card.dart';
|
||||
import 'package:clide/builtin/claude/src/stream_json_session.dart';
|
||||
import 'package:clide/widgets/widgets.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@@ -84,10 +85,10 @@ void main() {
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('permission · Write'), findsOneWidget);
|
||||
expect(find.text('Allow'), findsOneWidget);
|
||||
expect(find.text('Deny'), findsOneWidget);
|
||||
expect(find.text('1. Allow'), findsOneWidget);
|
||||
expect(find.text('2. Deny'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Allow'));
|
||||
await tester.tap(find.text('1. Allow'));
|
||||
await tester.pump();
|
||||
|
||||
expect(id, 'req-1');
|
||||
@@ -188,7 +189,7 @@ void main() {
|
||||
));
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.text('Deny'));
|
||||
await tester.tap(find.text('2. Deny'));
|
||||
await tester.pump();
|
||||
|
||||
expect(decision, isA<DenyTool>());
|
||||
@@ -198,7 +199,7 @@ void main() {
|
||||
testWidgets('permission: no "don\'t ask again" button without a suggestion', (tester) async {
|
||||
await tester.pumpWidget(harness(f, ToolPromptCard(prompt: permissionPrompt(), onResolve: (_, __) {})));
|
||||
await tester.pump();
|
||||
expect(find.text("Allow & don't ask again"), findsNothing);
|
||||
expect(find.text("2. Allow & don't ask again"), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('permission: "don\'t ask again" shows with a suggestion and returns updatedPermissions', (tester) async {
|
||||
@@ -212,8 +213,8 @@ void main() {
|
||||
));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text("Allow & don't ask again"), findsOneWidget);
|
||||
await tester.tap(find.text("Allow & don't ask again"));
|
||||
expect(find.text("2. Allow & don't ask again"), findsOneWidget);
|
||||
await tester.tap(find.text("2. Allow & don't ask again"));
|
||||
await tester.pump();
|
||||
expect((decision as AllowTool).updatedPermissions, hasLength(1));
|
||||
});
|
||||
@@ -224,7 +225,7 @@ void main() {
|
||||
await tester.pump();
|
||||
await tester.enterText(find.byType(EditableText), 'write it under docs/ instead');
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Deny'));
|
||||
await tester.tap(find.text('2. Deny'));
|
||||
await tester.pump();
|
||||
expect((decision as DenyTool).message, 'write it under docs/ instead');
|
||||
});
|
||||
@@ -235,7 +236,7 @@ void main() {
|
||||
await tester.pump();
|
||||
await tester.enterText(find.byType(EditableText), 'fyi: sandbox only');
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Allow'));
|
||||
await tester.tap(find.text('1. Allow'));
|
||||
await tester.pump();
|
||||
expect((decision as AllowTool).followUpNote, 'fyi: sandbox only');
|
||||
});
|
||||
@@ -289,7 +290,7 @@ void main() {
|
||||
await tester.pumpWidget(harness(f, ToolPromptCard(prompt: questionPrompt(), onResolve: (_, d) => decision = d)));
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.text('○ Other…'));
|
||||
await tester.tap(find.textContaining('Other…'));
|
||||
await tester.pump();
|
||||
// Two fields now: [0] = the Other free-text, [1] = the per-choice note.
|
||||
await tester.enterText(find.byType(EditableText).first, 'Kiwi');
|
||||
@@ -472,4 +473,79 @@ void main() {
|
||||
expect((decision as AllowTool).updatedInput['answers']['New question?'], 'Alpha');
|
||||
});
|
||||
});
|
||||
|
||||
group('number-key + Enter shortcuts (T-240)', () {
|
||||
late KernelFixture f;
|
||||
setUp(() async => f = await KernelFixture.create());
|
||||
tearDown(() => f.dispose());
|
||||
|
||||
Future<void> pumpCard(WidgetTester tester, ToolPrompt p, void Function(ToolDecision) onDecide) async {
|
||||
await tester.pumpWidget(harness(f, ToolPromptCard(prompt: p, onResolve: (_, d) => onDecide(d))));
|
||||
await tester.pump(); // let the card autofocus
|
||||
}
|
||||
|
||||
testWidgets('1 = Allow, 2 = Deny when there is no remember button', (tester) async {
|
||||
ToolDecision? d;
|
||||
await pumpCard(tester, permissionPrompt(), (x) => d = x);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.digit1);
|
||||
await tester.pump();
|
||||
expect(d, isA<AllowTool>());
|
||||
});
|
||||
|
||||
testWidgets('2 = Deny with no remember button', (tester) async {
|
||||
ToolDecision? d;
|
||||
await pumpCard(tester, permissionPrompt(), (x) => d = x);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.digit2);
|
||||
await tester.pump();
|
||||
expect(d, isA<DenyTool>());
|
||||
});
|
||||
|
||||
const sugg = [
|
||||
{'type': 'setMode', 'mode': 'acceptEdits', 'destination': 'session'}
|
||||
];
|
||||
|
||||
testWidgets('with a remember suggestion: 2 = Allow & remember', (tester) async {
|
||||
ToolDecision? d;
|
||||
await pumpCard(tester, permissionPrompt(suggestions: sugg), (x) => d = x);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.digit2);
|
||||
await tester.pump();
|
||||
expect((d as AllowTool).updatedPermissions, hasLength(1));
|
||||
});
|
||||
|
||||
testWidgets('with a remember suggestion: 3 = Deny', (tester) async {
|
||||
ToolDecision? d;
|
||||
await pumpCard(tester, permissionPrompt(suggestions: sugg), (x) => d = x);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.digit3);
|
||||
await tester.pump();
|
||||
expect(d, isA<DenyTool>());
|
||||
});
|
||||
|
||||
testWidgets('a number key selects a question option', (tester) async {
|
||||
ToolDecision? d;
|
||||
await pumpCard(tester, questionPrompt(), (x) => d = x);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.digit2); // Dogs (option 2)
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Submit'));
|
||||
await tester.pump();
|
||||
expect((d as AllowTool).updatedInput['answers']['Do you prefer cats or dogs?'], 'Dogs');
|
||||
});
|
||||
|
||||
testWidgets('Enter confirms the primary action (Allow)', (tester) async {
|
||||
ToolDecision? d;
|
||||
await pumpCard(tester, permissionPrompt(), (x) => d = x);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.pump();
|
||||
expect(d, isA<AllowTool>());
|
||||
});
|
||||
|
||||
testWidgets('a focused note field swallows digits (no button fires)', (tester) async {
|
||||
ToolDecision? d;
|
||||
await pumpCard(tester, permissionPrompt(), (x) => d = x);
|
||||
await tester.tap(find.byType(EditableText)); // focus the note field
|
||||
await tester.pump();
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.digit1);
|
||||
await tester.pump();
|
||||
expect(d, isNull, reason: 'typing in the note must not trigger Allow');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user