diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index 55cf6a97..493eb6c4 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -5907,3 +5907,5 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang 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 ('06FD09XWEXRJ4JS8MFPQGJEY5M', 'status', 'in_progress', 'done', NULL, '2026-06-17 10:22:42', '2026-06-17 10:22:42.544', '2026-06-17 10:22:42.544', NULL, 'b150c458bb2754f0369000e65406dec2', 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 ('06FD0A6K9B60ADTFBZA6NBVJFM', 'status', 'backlog', 'in_progress', NULL, '2026-06-17 10:29:07', '2026-06-17 10:29:07.736', '2026-06-17 10:29:07.736', NULL, '431cacc8adfb10c478617292fac8776f', 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 ('06FD0A6K9B60ADTFBZA6NBVJFM', 'status', 'in_progress', 'done', NULL, '2026-06-17 10:31:23', '2026-06-17 10:31:23.935', '2026-06-17 10:31:23.935', NULL, '9228abe9a3eb9b340c91717b4dade863', 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 ('06FD0A0ZTS9JZCSQ4W8ZVEH2VR', 'status', 'backlog', 'in_progress', NULL, '2026-06-17 10:44:25', '2026-06-17 10:44:25.018', '2026-06-17 10:44:25.018', NULL, '4626e79f524f63e003c0f93511e8cdce', 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 ('06FD0A0ZTS9JZCSQ4W8ZVEH2VR', 'status', 'in_progress', 'done', NULL, '2026-06-17 10:48:30', '2026-06-17 10:48:30.719', '2026-06-17 10:48:30.719', NULL, '3758ec372c9635bfd7756a8939f4b329', 2) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index 3c7bc2a7..aed3793c 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -7175,3 +7175,5 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat 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 ('06FD09XWEXRJ4JS8MFPQGJEY5M', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Cross-category settings search', 'The search box filters fields across ALL categories; results render as carded groups under category subheaders, editable inline, with per-category match counts shown in the rail. Zero-match categories dim. Wireframe: settings-search.png.', 'done', 'high', NULL, NULL, NULL, '2026-06-16 11:18:45', '2026-06-17 10:22:42.544', NULL, 'a10a68df4d4cf7ed0f639d74dda41d37', 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 ('06FD0A6K9B60ADTFBZA6NBVJFM', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Activity settings category (fold level + display)', 'Register the Claude-activity settings schema: activity fold level (T-183) and related conversation display options, via the standard field renderer (T-448).', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-16 11:19:57', '2026-06-17 10:29:07.736', NULL, 'a9ef8003e03def2e2ba4c1f59fbba07f', 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 ('06FD0A6K9B60ADTFBZA6NBVJFM', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Activity settings category (fold level + display)', 'Register the Claude-activity settings schema: activity fold level (T-183) and related conversation display options, via the standard field renderer (T-448).', 'done', 'medium', NULL, NULL, NULL, '2026-06-16 11:19:57', '2026-06-17 10:31:23.935', NULL, 'a7ddd9362097e6a2f7df764faf7cbf79', 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 ('06FD0A0ZTS9JZCSQ4W8ZVEH2VR', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Keymap settings category (preset select + options)', 'Register the keymap settings schema: preset enum select (default / vscode / jetbrains / vim — T-115, T-64, T-65, T-66) wired to KeymapService.setPreset, plus related options (e.g. sequence-timeout). Reuses the schema field renderer (T-448).', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-16 11:19:11', '2026-06-17 10:44:25.018', NULL, 'c5d65c432709c0ffad00237e82b7a23c', 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 ('06FD0A0ZTS9JZCSQ4W8ZVEH2VR', 'task', '06FD07ECS3GK2V7Z2WYYPJHJYC', 'Keymap settings category (preset select + options)', 'Register the keymap settings schema: preset enum select (default / vscode / jetbrains / vim — T-115, T-64, T-65, T-66) wired to KeymapService.setPreset, plus related options (e.g. sequence-timeout). Reuses the schema field renderer (T-448).', 'done', 'medium', NULL, NULL, NULL, '2026-06-16 11:19:11', '2026-06-17 10:48:30.719', NULL, '4678d116aadfaea65526200caf8e0ece', 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 5228317c..37141f51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. conversation fold level (none / tools / thinking / everything) as a schema field; picking a level applies live to the activity stream. (T-453) +- **Settings → Keymap category.** A preset select (Default / Vim / VS Code / + JetBrains); picking one switches the active keymap live via the preset + command. (T-451) + ### Changed - **Theme picker relabelled "Theme…".** The ⌘K theme picker's command title diff --git a/lib/builtin/keybindings_ui/src/extension.dart b/lib/builtin/keybindings_ui/src/extension.dart index 617a5957..1e3bc71a 100644 --- a/lib/builtin/keybindings_ui/src/extension.dart +++ b/lib/builtin/keybindings_ui/src/extension.dart @@ -42,5 +42,39 @@ class KeybindingsUiExtension extends ClideExtension { return IpcResponse.ok(id: '', data: {'preset': entry.key}); }, ), + // Keymap settings category (T-451). The select reads the active preset + // from kKeymapPresetSetting; picking one runs `keymap.preset.` + // (applyCommandPrefix), which calls KeymapService.setPreset — persisting + // and reloading the layered keymap live. + const SettingsCategoryContribution( + id: 'keymap', + category: SettingsCategory( + id: 'keymap', + title: 'Keymap', + iconName: 'keyboard', + priority: 20, + sections: [ + SettingsSection( + label: 'Preset', + fields: [ + SettingsField( + key: kKeymapPresetSetting, + kind: SettingsFieldKind.select, + label: 'Active preset', + help: 'Keyboard layout for the whole app.', + defaultValue: 'default', + applyCommandPrefix: 'keymap.preset.', + options: [ + SettingsOption(value: 'default', label: 'Default'), + SettingsOption(value: 'vim', label: 'Vim'), + SettingsOption(value: 'vscode', label: 'VS Code'), + SettingsOption(value: 'jetbrains', label: 'JetBrains'), + ], + ), + ], + ), + ], + ), + ), ]; } diff --git a/lib/builtin/settings_ui/src/settings_category_view.dart b/lib/builtin/settings_ui/src/settings_category_view.dart index b4ef27fb..685ae11d 100644 --- a/lib/builtin/settings_ui/src/settings_category_view.dart +++ b/lib/builtin/settings_ui/src/settings_category_view.dart @@ -206,7 +206,19 @@ class _Control extends StatelessWidget { case SettingsFieldKind.toggle: return _ToggleControl(checked: value == true, onChanged: _set); case SettingsFieldKind.select: - return _SelectControl(field: field, value: value?.toString(), onPick: _set); + return _SelectControl( + field: field, + value: value?.toString(), + onPick: (v) { + final prefix = field.applyCommandPrefix; + if (prefix != null) { + // Value selects a command (the subsystem applies + persists). + ClideKernel.of(context).commands.execute('$prefix$v'); + } else { + _set(v); + } + }, + ); case SettingsFieldKind.text: return _EditControl(field: field, value: value?.toString() ?? '', numeric: false, onCommit: _set); case SettingsFieldKind.number: diff --git a/lib/kernel/src/settings_schema.dart b/lib/kernel/src/settings_schema.dart index c016a337..05b23e1c 100644 --- a/lib/kernel/src/settings_schema.dart +++ b/lib/kernel/src/settings_schema.dart @@ -52,6 +52,7 @@ class SettingsField { this.min, this.max, this.fileCommand, + this.applyCommandPrefix, }); final String key; @@ -73,6 +74,13 @@ class SettingsField { /// For [SettingsFieldKind.file]: the command id the row's button invokes. final String? fileCommand; + + /// For [SettingsFieldKind.select]: when set, picking option `` runs + /// the command `` instead of writing [key] + /// directly — for settings a subsystem applies via a command (and only then + /// persists). The current value is still read from [key], so the scope tag + /// and selection still work. Example: `'keymap.preset.'` → `keymap.preset.vim`. + final String? applyCommandPrefix; } /// A carded group of fields (surface.md "sectioned cards"). [label] is the diff --git a/test/builtin/keybindings_ui/preset_commands_test.dart b/test/builtin/keybindings_ui/preset_commands_test.dart index 7aea256d..9d7f718c 100644 --- a/test/builtin/keybindings_ui/preset_commands_test.dart +++ b/test/builtin/keybindings_ui/preset_commands_test.dart @@ -3,6 +3,7 @@ library; import 'package:clide/builtin/keybindings_ui/keybindings_ui.dart'; +import 'package:clide/extension/extension.dart'; import 'package:clide/kernel/kernel.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -46,4 +47,16 @@ void main() { await f.services.commands.execute('keymap.preset.default'); expect(f.services.settings.get(kKeymapPresetSetting), 'default'); }); + + test('contributes a Keymap settings category routed through the preset commands (T-451)', () { + final category = KeybindingsUiExtension().contributions + .whereType() + .firstWhere((c) => c.id == 'keymap') + .category; + expect(category.title, 'Keymap'); + final field = category.sections.expand((s) => s.fields).firstWhere((f) => f.key == kKeymapPresetSetting); + expect(field.kind, SettingsFieldKind.select); + expect(field.applyCommandPrefix, 'keymap.preset.'); + expect(field.options.map((o) => o.value), containsAll(['default', 'vim', 'vscode', 'jetbrains'])); + }); } diff --git a/test/builtin/settings_ui/category_view_test.dart b/test/builtin/settings_ui/category_view_test.dart index f23ea2c5..9e67ec03 100644 --- a/test/builtin/settings_ui/category_view_test.dart +++ b/test/builtin/settings_ui/category_view_test.dart @@ -1,4 +1,5 @@ import 'package:clide/builtin/settings_ui/settings_ui.dart'; +import 'package:clide/clide.dart' show IpcResponse; import 'package:clide/extension/extension.dart'; import 'package:clide/kernel/kernel.dart'; import 'package:clide/widgets/widgets.dart'; @@ -105,6 +106,47 @@ void main() { await tester.pump(); expect(f.services.settings.get('app.demo.level'), 'debug'); }); + + testWidgets('a select with applyCommandPrefix runs the command, not a key write', (tester) async { + var ran = ''; + f.services.commands.register( + CommandContribution( + id: 'test.apply.vim', + command: 'test.apply.vim', + run: (_) async { + ran = 'vim'; + return IpcResponse.ok(id: '', data: const {}); + }, + ), + ); + const cat = SettingsCategory( + id: 'k', + title: 'K', + sections: [ + SettingsSection( + label: 'P', + fields: [ + SettingsField( + key: 'app.k.preset', + kind: SettingsFieldKind.select, + label: 'Preset', + defaultValue: 'default', + applyCommandPrefix: 'test.apply.', + options: [SettingsOption(value: 'default', label: 'Default'), SettingsOption(value: 'vim', label: 'Vim')], + ), + ], + ), + ], + ); + await tester.pumpWidget(harness(f, _bounded(const SettingsCategoryView(category: cat)))); + await tester.tap(find.bySemanticsLabel(RegExp(r'^Preset:'))); + await tester.pump(); + await tester.tap(find.text('Vim')); + await tester.pump(); + expect(ran, 'vim'); + // The key is applied by the command, not written directly by the engine. + expect(f.services.settings.get('app.k.preset'), isNull); + }); }); group('scope tag (T-449)', () {