T-310: numpad digits select prompt-card options
The prompt card's number-key shortcuts matched only digit1..digit9 (the number row); numpad 1-9 fell through to ignored. Add a parallel _numpadKeys list and check it in _onKey so the keypad maps to the same 1-9 selection for Allow/Deny and question options. numpadEnter was already handled. The hasPrimaryFocus guard still lets digits type into a focused note field. Adds four widget tests (numpad Allow/Deny, question option, focused-note swallow). Closes T-310 (under UI epic T-276). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2496,3 +2496,5 @@ ACCEPTANCE
|
||||
- Widget test covers the new button resolving to a DenyTool with the expected note.
|
||||
|
||||
CLARIFICATION (the TOOLTIP line above lost a word to shell escaping): ClideButton already exposes a tooltip parameter (clide_button.dart:17,26,42) — pass tooltip on the new button for the mouseover; no widget change needed.', NULL, '2026-06-10 09:56:24', '2026-06-10 09:56:24', '2026-06-10 09:56:24', NULL, 'f4fa8bc5799ef68d14512e128c03d1ba', 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 ('06FB1SJKA7KRCQMZE41SPQG780', 'status', 'backlog', 'ready', NULL, '2026-06-10 10:01:09', '2026-06-10 10:01:09', '2026-06-10 10:01:09', NULL, '7ed94c6ae98bb9f7a2e3a843512c47bd', 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 ('06FB1SJKA7KRCQMZE41SPQG780', 'status', 'ready', 'done', NULL, '2026-06-10 10:07:19', '2026-06-10 10:07:19', '2026-06-10 10:07:19', NULL, '15b320f72bf2a7bec603482d4b980bf6', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
|
||||
@@ -136,3 +136,4 @@ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1S7613SYF0M9XQT5JNWM40', 'T-309', '2026-06-10 09:37:31', '2026-06-10 09:37:31', NULL, 'b4f48310ea6b0f49f83c46e3bc5b7ad8', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1SJKA7KRCQMZE41SPQG780', 'T-310', '2026-06-10 09:39:05', '2026-06-10 09:39:05', NULL, '9088122bf9d5e04ca8a7fee3bf031a53', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1XDWKQ594ET4GDYEFK5ZJ4', 'T-311', '2026-06-10 09:55:55', '2026-06-10 09:55:55', NULL, 'ed03464125d09ff5f23913e6247064e3', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1ZFJK6J2GSV4SA69QF730C', 'T-312', '2026-06-10 10:04:53', '2026-06-10 10:04:53', NULL, 'dd22af12b333c13376b23aa5381ea2d7', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
|
||||
@@ -1421,3 +1421,28 @@ ACCEPTANCE
|
||||
- Widget test covers the new button resolving to a DenyTool with the expected note.
|
||||
|
||||
CLARIFICATION (the TOOLTIP line above lost a word to shell escaping): ClideButton already exposes a tooltip parameter (clide_button.dart:17,26,42) — pass tooltip on the new button for the mouseover; no widget change needed.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-10 09:55:55', '2026-06-10 09:56:24', NULL, '43ec3a8962c96601978154295a38cebb', 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 ('06FB1SJKA7KRCQMZE41SPQG780', 'bug', '06FB0TNQM5TWC00GW0P3X02HZW', 'Prompt card: numpad digits should select options, not just the number row', 'The Claude prompt card (permission / AskUserQuestion) number-key shortcuts only respond to the top number-row digits, not the numeric keypad. Users on a full keyboard expect numpad 1-9 to pick a button/option too.
|
||||
|
||||
Root cause: lib/builtin/claude/src/prompt_card.dart:125-135 defines _digitKeys as LogicalKeyboardKey.digit1..digit9 only. _onKey (line 146) matches against that list, so numpad1..numpad9 fall through to ignored. Note that numpadEnter is already handled alongside enter at line 143 — numpad parity is partial.
|
||||
|
||||
Fix: extend _digitKeys (or the match in _onKey) to also map LogicalKeyboardKey.numpad1..numpad9 to the same 1-9 selection. Keep the hasPrimaryFocus / modifier guards as-is so digits still type into note fields.
|
||||
|
||||
Acceptance: with the prompt card focused, numpad 1-9 selects the corresponding option/button exactly like the number-row digits; numpad keys still type normally when a note field has focus.', 'ready', 'low', NULL, NULL, NULL, '2026-06-10 09:39:05', '2026-06-10 10:01:09', NULL, '5e2f8dfdf163e310d8bc479165ce5130', 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 ('06FB1ZFJK6J2GSV4SA69QF730C', 'bug', '06FB0TNQM5TWC00GW0P3X02HZW', 'Image card caches by path — overwritten file shows stale render', 'clide image show (and the conversation image viewer card) appears to cache by file path: re-showing a file after its bytes change at the SAME path renders the previous image, not the new one. Hit live while iterating on a Frame0 wireframe (re-exported settings-modal.png in place; the pane kept showing the prior version until exported under a new filename).
|
||||
|
||||
Expected: showing a path always reflects the current file bytes.
|
||||
|
||||
Explore / options:
|
||||
- Cache-key on mtime + size (or a content hash) instead of path alone; invalidate when they change.
|
||||
- A force-refresh path for clide image show (re-decode, bypass cache).
|
||||
- Check where the cache lives: the image card widget (Image.file / ImageProvider cache — Flutter''s imageCache keys by path+scale and won''t refresh on overwrite), the IPC image.show handler, or the pane render layer.
|
||||
- Likely Flutter imageCache: evict the key (or use a FileImage with a key that includes mtime) before showing.
|
||||
|
||||
Note: surfaced via clide image show (T-249 / T-252 lightbox share the same provider). Repro: export an image, show it, overwrite the same path with different content, show again -> stale.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-10 10:04:53', '2026-06-10 10:04:53', NULL, '9179616adf1829faafb62ff32b7ead0a', 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 ('06FB1SJKA7KRCQMZE41SPQG780', 'bug', '06FB0TNQM5TWC00GW0P3X02HZW', 'Prompt card: numpad digits should select options, not just the number row', 'The Claude prompt card (permission / AskUserQuestion) number-key shortcuts only respond to the top number-row digits, not the numeric keypad. Users on a full keyboard expect numpad 1-9 to pick a button/option too.
|
||||
|
||||
Root cause: lib/builtin/claude/src/prompt_card.dart:125-135 defines _digitKeys as LogicalKeyboardKey.digit1..digit9 only. _onKey (line 146) matches against that list, so numpad1..numpad9 fall through to ignored. Note that numpadEnter is already handled alongside enter at line 143 — numpad parity is partial.
|
||||
|
||||
Fix: extend _digitKeys (or the match in _onKey) to also map LogicalKeyboardKey.numpad1..numpad9 to the same 1-9 selection. Keep the hasPrimaryFocus / modifier guards as-is so digits still type into note fields.
|
||||
|
||||
Acceptance: with the prompt card focused, numpad 1-9 selects the corresponding option/button exactly like the number-row digits; numpad keys still type normally when a note field has focus.', 'done', 'low', NULL, NULL, NULL, '2026-06-10 09:39:05', '2026-06-10 10:07:19', NULL, '229d4e82ae8b66e249475f49d69bacdb', 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);
|
||||
|
||||
@@ -256,6 +256,11 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Numpad digits now pick permission/question options too.** The prompt card's
|
||||
number-key shortcuts only matched the top number row; numpad `1`-`9` now map to
|
||||
the same 1-9 selection, so the keypad works for Allow/Deny and question options.
|
||||
The note-field guard still lets digits type normally when a note is focused.
|
||||
(T-310)
|
||||
- **The composer no longer jams against the window bottom when the status bar
|
||||
is hidden.** With the bar gone, the bottom-most pane content used to run flush
|
||||
into the window's resize-drag edge; the layout now reserves that edge so the
|
||||
|
||||
@@ -122,6 +122,8 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
|
||||
|
||||
// -- number-key + Enter shortcuts (T-240, CLI muscle memory) ---------------
|
||||
|
||||
// Number-row digits and their numpad twins map to the same 1-9 selection,
|
||||
// so the shortcut works regardless of which key the user reaches for (T-310).
|
||||
static const _digitKeys = [
|
||||
LogicalKeyboardKey.digit1,
|
||||
LogicalKeyboardKey.digit2,
|
||||
@@ -134,6 +136,18 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
|
||||
LogicalKeyboardKey.digit9,
|
||||
];
|
||||
|
||||
static const _numpadKeys = [
|
||||
LogicalKeyboardKey.numpad1,
|
||||
LogicalKeyboardKey.numpad2,
|
||||
LogicalKeyboardKey.numpad3,
|
||||
LogicalKeyboardKey.numpad4,
|
||||
LogicalKeyboardKey.numpad5,
|
||||
LogicalKeyboardKey.numpad6,
|
||||
LogicalKeyboardKey.numpad7,
|
||||
LogicalKeyboardKey.numpad8,
|
||||
LogicalKeyboardKey.numpad9,
|
||||
];
|
||||
|
||||
KeyEventResult _onKey(FocusNode node, KeyEvent e) {
|
||||
if (e is! KeyDownEvent) return KeyEventResult.ignored;
|
||||
// A note field has focus → let the digits type; only act for the card.
|
||||
@@ -143,7 +157,8 @@ class _ToolPromptCardState extends State<ToolPromptCard> {
|
||||
if (e.logicalKey == LogicalKeyboardKey.enter || e.logicalKey == LogicalKeyboardKey.numpadEnter) {
|
||||
return _activatePrimary() ? KeyEventResult.handled : KeyEventResult.ignored;
|
||||
}
|
||||
final i = _digitKeys.indexOf(e.logicalKey);
|
||||
var i = _digitKeys.indexOf(e.logicalKey);
|
||||
if (i < 0) i = _numpadKeys.indexOf(e.logicalKey);
|
||||
if (i < 0) return KeyEventResult.ignored;
|
||||
return _activateNumber(i + 1) ? KeyEventResult.handled : KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
@@ -547,5 +547,42 @@ void main() {
|
||||
await tester.pump();
|
||||
expect(d, isNull, reason: 'typing in the note must not trigger Allow');
|
||||
});
|
||||
|
||||
// Numpad twins map to the same selection as the number row (T-310).
|
||||
testWidgets('numpad 1 = Allow', (tester) async {
|
||||
ToolDecision? d;
|
||||
await pumpCard(tester, permissionPrompt(), (x) => d = x);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.numpad1);
|
||||
await tester.pump();
|
||||
expect(d, isA<AllowTool>());
|
||||
});
|
||||
|
||||
testWidgets('numpad 2 = Deny when there is no remember button', (tester) async {
|
||||
ToolDecision? d;
|
||||
await pumpCard(tester, permissionPrompt(), (x) => d = x);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.numpad2);
|
||||
await tester.pump();
|
||||
expect(d, isA<DenyTool>());
|
||||
});
|
||||
|
||||
testWidgets('numpad selects a question option just like the number row', (tester) async {
|
||||
ToolDecision? d;
|
||||
await pumpCard(tester, questionPrompt(), (x) => d = x);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.numpad2); // 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('a focused note field swallows numpad digits too', (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.numpad1);
|
||||
await tester.pump();
|
||||
expect(d, isNull, reason: 'typing in the note must not trigger Allow');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user