diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index ceb3eeba..84d4bbe0 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -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; diff --git a/.pql/changelog/ticket_idmap/2026-06.sql b/.pql/changelog/ticket_idmap/2026-06.sql index 1f253d57..04ac8b0c 100644 --- a/.pql/changelog/ticket_idmap/2026-06.sql +++ b/.pql/changelog/ticket_idmap/2026-06.sql @@ -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); diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index af1d00b8..baa13862 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -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); diff --git a/CHANGELOG.md b/CHANGELOG.md index fdbd5bac..d7bde4e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/builtin/claude/src/prompt_card.dart b/lib/builtin/claude/src/prompt_card.dart index 34d5e2be..44f44083 100644 --- a/lib/builtin/claude/src/prompt_card.dart +++ b/lib/builtin/claude/src/prompt_card.dart @@ -122,6 +122,8 @@ class _ToolPromptCardState extends State { // -- 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 { 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 { 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; } diff --git a/test/builtin/claude/prompt_card_test.dart b/test/builtin/claude/prompt_card_test.dart index 4d97731f..bbb55955 100644 --- a/test/builtin/claude/prompt_card_test.dart +++ b/test/builtin/claude/prompt_card_test.dart @@ -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()); + }); + + 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()); + }); + + 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'); + }); }); }