@kolisachint/hoocode-agent 0.5.31 → 0.5.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +131 -0
  2. package/dist/core/agent-session-runtime.d.ts +28 -0
  3. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  4. package/dist/core/agent-session-runtime.js +61 -2
  5. package/dist/core/agent-session-runtime.js.map +1 -1
  6. package/dist/core/keybindings.d.ts +102 -30
  7. package/dist/core/keybindings.d.ts.map +1 -1
  8. package/dist/core/keybindings.js +123 -29
  9. package/dist/core/keybindings.js.map +1 -1
  10. package/dist/core/settings-defaults.d.ts +1 -1
  11. package/dist/core/settings-defaults.d.ts.map +1 -1
  12. package/dist/core/settings-defaults.js +1 -1
  13. package/dist/core/settings-defaults.js.map +1 -1
  14. package/dist/core/settings-manager.d.ts +10 -3
  15. package/dist/core/settings-manager.d.ts.map +1 -1
  16. package/dist/core/settings-manager.js +19 -8
  17. package/dist/core/settings-manager.js.map +1 -1
  18. package/dist/core/settings-types.d.ts +1 -1
  19. package/dist/core/settings-types.d.ts.map +1 -1
  20. package/dist/core/settings-types.js.map +1 -1
  21. package/dist/core/slash-commands.d.ts.map +1 -1
  22. package/dist/core/slash-commands.js +5 -1
  23. package/dist/core/slash-commands.js.map +1 -1
  24. package/dist/core/tool-output-view.d.ts +33 -0
  25. package/dist/core/tool-output-view.d.ts.map +1 -0
  26. package/dist/core/tool-output-view.js +31 -0
  27. package/dist/core/tool-output-view.js.map +1 -0
  28. package/dist/core/tools/edit.d.ts.map +1 -1
  29. package/dist/core/tools/edit.js +4 -0
  30. package/dist/core/tools/edit.js.map +1 -1
  31. package/dist/modes/interactive/command-executor.d.ts +20 -0
  32. package/dist/modes/interactive/command-executor.d.ts.map +1 -1
  33. package/dist/modes/interactive/command-executor.js +138 -9
  34. package/dist/modes/interactive/command-executor.js.map +1 -1
  35. package/dist/modes/interactive/components/footer.d.ts +4 -0
  36. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  37. package/dist/modes/interactive/components/footer.js +31 -2
  38. package/dist/modes/interactive/components/footer.js.map +1 -1
  39. package/dist/modes/interactive/components/scoped-models-selector.d.ts +2 -2
  40. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  41. package/dist/modes/interactive/components/scoped-models-selector.js +2 -2
  42. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  43. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  44. package/dist/modes/interactive/components/session-selector.js +2 -2
  45. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  46. package/dist/modes/interactive/components/settings-selector.d.ts +3 -2
  47. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  48. package/dist/modes/interactive/components/settings-selector.js +13 -12
  49. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  50. package/dist/modes/interactive/components/tool-chain-summary.d.ts +65 -0
  51. package/dist/modes/interactive/components/tool-chain-summary.d.ts.map +1 -0
  52. package/dist/modes/interactive/components/tool-chain-summary.js +216 -0
  53. package/dist/modes/interactive/components/tool-chain-summary.js.map +1 -0
  54. package/dist/modes/interactive/components/tool-chain.d.ts +61 -0
  55. package/dist/modes/interactive/components/tool-chain.d.ts.map +1 -0
  56. package/dist/modes/interactive/components/tool-chain.js +156 -0
  57. package/dist/modes/interactive/components/tool-chain.js.map +1 -0
  58. package/dist/modes/interactive/components/tool-execution.d.ts +55 -9
  59. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  60. package/dist/modes/interactive/components/tool-execution.js +173 -33
  61. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  62. package/dist/modes/interactive/components/tool-signal.d.ts +50 -0
  63. package/dist/modes/interactive/components/tool-signal.d.ts.map +1 -0
  64. package/dist/modes/interactive/components/tool-signal.js +151 -0
  65. package/dist/modes/interactive/components/tool-signal.js.map +1 -0
  66. package/dist/modes/interactive/interactive-mode.d.ts +50 -1
  67. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  68. package/dist/modes/interactive/interactive-mode.js +239 -29
  69. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  70. package/dist/modes/interactive/resource-display.d.ts +9 -0
  71. package/dist/modes/interactive/resource-display.d.ts.map +1 -1
  72. package/dist/modes/interactive/resource-display.js +13 -0
  73. package/dist/modes/interactive/resource-display.js.map +1 -1
  74. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  75. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  76. package/examples/extensions/sandbox/package.json +1 -1
  77. package/examples/extensions/with-deps/package.json +1 -1
  78. package/package.json +4 -4
@@ -9,6 +9,10 @@ interface AppKeybindings {
9
9
  "app.model.cycleBackward": true;
10
10
  "app.model.select": true;
11
11
  "app.tools.expand": true;
12
+ "app.view.cycleForward": true;
13
+ "app.view.cycleBackward": true;
14
+ "app.tools.unfoldOne": true;
15
+ "app.tools.foldOne": true;
12
16
  "app.thinking.toggle": true;
13
17
  "app.tasks.cycleView": true;
14
18
  "app.team.focus": true;
@@ -24,6 +28,10 @@ interface AppKeybindings {
24
28
  "app.session.tree": true;
25
29
  "app.session.fork": true;
26
30
  "app.session.resume": true;
31
+ "app.session.changeDirectory": true;
32
+ "app.settings.open": true;
33
+ "app.hotkeys.open": true;
34
+ "app.mode.cycle": true;
27
35
  "app.tree.foldOrUp": true;
28
36
  "app.tree.unfoldOrDown": true;
29
37
  "app.tree.editLabel": true;
@@ -52,6 +60,38 @@ declare module "@kolisachint/hoocode-tui" {
52
60
  interface Keybindings extends AppKeybindings {
53
61
  }
54
62
  }
63
+ /**
64
+ * The cockpit layout.
65
+ *
66
+ * Three rings, and which ring a key belongs to is decided by its modifier:
67
+ *
68
+ * - **ctrl — the view.** What is on screen right now: expand, thinking blocks,
69
+ * task panel, model cycling. Pressed many times a minute, so they sit under
70
+ * the hand and never require a second modifier.
71
+ * - **alt — the cockpit.** What the agent *is* and where it works: model,
72
+ * mode, working directory, settings, sessions. Pressed a few times a session.
73
+ * - **overlays.** Inside a picker, the query line is a text field, so every
74
+ * `ctrl+<letter>` there belongs to the *text* (ctrl+a start of line, ctrl+u
75
+ * kill line, ctrl+w kill word). A picker's own verbs are therefore all on
76
+ * `alt+<letter>`, mnemonic to that picker; the picker captures keys while it
77
+ * is open, so reusing a global letter there is unambiguous.
78
+ *
79
+ * Two rules keep the set learnable: shift reverses whatever the unshifted key
80
+ * does, and no binding takes a key the editor or the terminal already owns
81
+ * (ctrl+a/e/b/f/k/u/w/y/d/l/r/g, ctrl+s XOFF, ctrl+m == enter, ctrl+i == tab).
82
+ *
83
+ * Two constraints from the key parser shape which alt keys are usable at all,
84
+ * and `test/keybinding-layout.test.ts` holds both:
85
+ *
86
+ * - Only `alt+<letter>` and `alt+<digit>` survive a terminal without the Kitty
87
+ * keyboard protocol. `alt+[` and `alt+]` arrive as the CSI and OSC
88
+ * introducers and are not parsed as keys; nor is any shift-modified key, so a
89
+ * `shift+…` default is a Kitty-only convenience on top of an unshifted key
90
+ * that works everywhere (as `shift+ctrl+p` has always been).
91
+ * - Legacy `alt+p` and `alt+n` are also accepted as `alt+up` and `alt+down`
92
+ * (the emacs previous/next aliases), so no scope may bind both halves of
93
+ * either pair.
94
+ */
55
95
  export declare const KEYBINDINGS: {
56
96
  readonly "tui.editor.cursorUp": {
57
97
  readonly defaultKeys: "up";
@@ -206,12 +246,28 @@ export declare const KEYBINDINGS: {
206
246
  readonly description: "Cycle to previous model";
207
247
  };
208
248
  readonly "app.model.select": {
209
- readonly defaultKeys: "ctrl+l";
249
+ readonly defaultKeys: "alt+m";
210
250
  readonly description: "Open model selector";
211
251
  };
212
252
  readonly "app.tools.expand": {
213
253
  readonly defaultKeys: "ctrl+o";
214
- readonly description: "Toggle tool output";
254
+ readonly description: "Expand or collapse what is in front of you (tool bodies, header, summaries)";
255
+ };
256
+ readonly "app.view.cycleForward": {
257
+ readonly defaultKeys: "alt+o";
258
+ readonly description: "Cycle tool output view (radar → glance → full)";
259
+ };
260
+ readonly "app.view.cycleBackward": {
261
+ readonly defaultKeys: "shift+alt+o";
262
+ readonly description: "Cycle tool output view backward";
263
+ };
264
+ readonly "app.tools.unfoldOne": {
265
+ readonly defaultKeys: "alt+u";
266
+ readonly description: "Open the newest folded thing — a chain in radar, a tool body otherwise";
267
+ };
268
+ readonly "app.tools.foldOne": {
269
+ readonly defaultKeys: "shift+alt+u";
270
+ readonly description: "Re-fold the most recently opened chain or tool block";
215
271
  };
216
272
  readonly "app.thinking.toggle": {
217
273
  readonly defaultKeys: "ctrl+t";
@@ -221,10 +277,6 @@ export declare const KEYBINDINGS: {
221
277
  readonly defaultKeys: "ctrl+n";
222
278
  readonly description: "Cycle task panel view (tasks → subagents → teams, skips empty lenses)";
223
279
  };
224
- readonly "app.session.toggleNamedFilter": {
225
- readonly defaultKeys: "ctrl+n";
226
- readonly description: "Toggle named session filter";
227
- };
228
280
  readonly "app.team.focus": {
229
281
  readonly defaultKeys: "alt+n";
230
282
  readonly description: "Focus the team roster (navigate roles, n nudge, a attach)";
@@ -238,7 +290,7 @@ export declare const KEYBINDINGS: {
238
290
  readonly description: "Attach to the selected team role (team focus mode)";
239
291
  };
240
292
  readonly "app.editor.external": {
241
- readonly defaultKeys: "ctrl+g";
293
+ readonly defaultKeys: "alt+e";
242
294
  readonly description: "Open external editor";
243
295
  };
244
296
  readonly "app.message.followUp": {
@@ -254,24 +306,40 @@ export declare const KEYBINDINGS: {
254
306
  readonly description: "Paste image from clipboard";
255
307
  };
256
308
  readonly "app.input.voiceTranscribe": {
257
- readonly defaultKeys: "ctrl+r";
309
+ readonly defaultKeys: "alt+r";
258
310
  readonly description: "Record voice and transcribe into the editor";
259
311
  };
260
312
  readonly "app.session.new": {
261
313
  readonly defaultKeys: [];
262
314
  readonly description: "Start a new session";
263
315
  };
264
- readonly "app.session.tree": {
265
- readonly defaultKeys: [];
266
- readonly description: "Open session tree";
267
- };
268
316
  readonly "app.session.fork": {
269
317
  readonly defaultKeys: [];
270
318
  readonly description: "Fork current session";
271
319
  };
320
+ readonly "app.session.tree": {
321
+ readonly defaultKeys: "alt+t";
322
+ readonly description: "Open session tree";
323
+ };
272
324
  readonly "app.session.resume": {
273
- readonly defaultKeys: [];
274
- readonly description: "Resume a session";
325
+ readonly defaultKeys: "alt+h";
326
+ readonly description: "Resume a session from history";
327
+ };
328
+ readonly "app.session.changeDirectory": {
329
+ readonly defaultKeys: "alt+w";
330
+ readonly description: "Change working directory (move to another repo without quitting)";
331
+ };
332
+ readonly "app.settings.open": {
333
+ readonly defaultKeys: "alt+s";
334
+ readonly description: "Open settings";
335
+ };
336
+ readonly "app.hotkeys.open": {
337
+ readonly defaultKeys: "alt+k";
338
+ readonly description: "Show keyboard shortcuts";
339
+ };
340
+ readonly "app.mode.cycle": {
341
+ readonly defaultKeys: "alt+g";
342
+ readonly description: "Cycle agent mode (ask → plan → build → debug)";
275
343
  };
276
344
  readonly "app.tree.foldOrUp": {
277
345
  readonly defaultKeys: ["ctrl+left", "alt+left"];
@@ -290,19 +358,23 @@ export declare const KEYBINDINGS: {
290
358
  readonly description: "Toggle tree label timestamps";
291
359
  };
292
360
  readonly "app.session.togglePath": {
293
- readonly defaultKeys: "ctrl+p";
361
+ readonly defaultKeys: "alt+p";
294
362
  readonly description: "Toggle session path display";
295
363
  };
296
364
  readonly "app.session.toggleSort": {
297
- readonly defaultKeys: "ctrl+s";
298
- readonly description: "Toggle session sort mode";
365
+ readonly defaultKeys: "alt+o";
366
+ readonly description: "Toggle session sort order";
299
367
  };
300
368
  readonly "app.session.rename": {
301
- readonly defaultKeys: "ctrl+r";
369
+ readonly defaultKeys: "alt+r";
302
370
  readonly description: "Rename session";
303
371
  };
372
+ readonly "app.session.toggleNamedFilter": {
373
+ readonly defaultKeys: "alt+n";
374
+ readonly description: "Toggle named session filter";
375
+ };
304
376
  readonly "app.session.delete": {
305
- readonly defaultKeys: "ctrl+d";
377
+ readonly defaultKeys: "alt+x";
306
378
  readonly description: "Delete session";
307
379
  };
308
380
  readonly "app.session.deleteNoninvasive": {
@@ -310,19 +382,19 @@ export declare const KEYBINDINGS: {
310
382
  readonly description: "Delete session when query is empty";
311
383
  };
312
384
  readonly "app.models.save": {
313
- readonly defaultKeys: "ctrl+s";
385
+ readonly defaultKeys: "alt+s";
314
386
  readonly description: "Save model selection";
315
387
  };
316
388
  readonly "app.models.enableAll": {
317
- readonly defaultKeys: "ctrl+a";
389
+ readonly defaultKeys: "alt+a";
318
390
  readonly description: "Enable all models";
319
391
  };
320
392
  readonly "app.models.clearAll": {
321
- readonly defaultKeys: "ctrl+x";
393
+ readonly defaultKeys: "alt+x";
322
394
  readonly description: "Clear all models";
323
395
  };
324
396
  readonly "app.models.toggleProvider": {
325
- readonly defaultKeys: "ctrl+p";
397
+ readonly defaultKeys: "alt+g";
326
398
  readonly description: "Toggle all models for provider";
327
399
  };
328
400
  readonly "app.models.reorderUp": {
@@ -342,31 +414,31 @@ export declare const KEYBINDINGS: {
342
414
  readonly description: "Go back to the previous question";
343
415
  };
344
416
  readonly "app.tree.filter.default": {
345
- readonly defaultKeys: "ctrl+d";
417
+ readonly defaultKeys: "alt+1";
346
418
  readonly description: "Tree filter: default view";
347
419
  };
348
420
  readonly "app.tree.filter.noTools": {
349
- readonly defaultKeys: "ctrl+t";
421
+ readonly defaultKeys: "alt+2";
350
422
  readonly description: "Tree filter: hide tool results";
351
423
  };
352
424
  readonly "app.tree.filter.userOnly": {
353
- readonly defaultKeys: "ctrl+u";
425
+ readonly defaultKeys: "alt+3";
354
426
  readonly description: "Tree filter: user messages only";
355
427
  };
356
428
  readonly "app.tree.filter.labeledOnly": {
357
- readonly defaultKeys: "ctrl+l";
429
+ readonly defaultKeys: "alt+4";
358
430
  readonly description: "Tree filter: labeled entries only";
359
431
  };
360
432
  readonly "app.tree.filter.all": {
361
- readonly defaultKeys: "ctrl+a";
433
+ readonly defaultKeys: "alt+5";
362
434
  readonly description: "Tree filter: show all entries";
363
435
  };
364
436
  readonly "app.tree.filter.cycleForward": {
365
- readonly defaultKeys: "ctrl+o";
437
+ readonly defaultKeys: "alt+c";
366
438
  readonly description: "Tree filter: cycle forward";
367
439
  };
368
440
  readonly "app.tree.filter.cycleBackward": {
369
- readonly defaultKeys: "shift+ctrl+o";
441
+ readonly defaultKeys: "shift+alt+c";
370
442
  readonly description: "Tree filter: cycle backward";
371
443
  };
372
444
  };
@@ -1 +1 @@
1
- {"version":3,"file":"keybindings.d.ts","sourceRoot":"","sources":["../../src/core/keybindings.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\ttype Keybinding,\n\ttype KeybindingDefinitions,\n\ttype KeybindingsConfig,\n\ttype KeyId,\n\tTUI_KEYBINDINGS,\n\tKeybindingsManager as TuiKeybindingsManager,\n} from \"@kolisachint/hoocode-tui\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../config.js\";\n\ninterface AppKeybindings {\n\t\"app.interrupt\": true;\n\t\"app.clear\": true;\n\t\"app.exit\": true;\n\t\"app.suspend\": true;\n\t\"app.thinking.cycle\": true;\n\t\"app.model.cycleForward\": true;\n\t\"app.model.cycleBackward\": true;\n\t\"app.model.select\": true;\n\t\"app.tools.expand\": true;\n\t\"app.thinking.toggle\": true;\n\t\"app.tasks.cycleView\": true;\n\t\"app.team.focus\": true;\n\t\"app.team.nudge\": true;\n\t\"app.team.attach\": true;\n\t\"app.session.toggleNamedFilter\": true;\n\t\"app.editor.external\": true;\n\t\"app.message.followUp\": true;\n\t\"app.message.dequeue\": true;\n\t\"app.clipboard.pasteImage\": true;\n\t\"app.input.voiceTranscribe\": true;\n\t\"app.session.new\": true;\n\t\"app.session.tree\": true;\n\t\"app.session.fork\": true;\n\t\"app.session.resume\": true;\n\t\"app.tree.foldOrUp\": true;\n\t\"app.tree.unfoldOrDown\": true;\n\t\"app.tree.editLabel\": true;\n\t\"app.tree.toggleLabelTimestamp\": true;\n\t\"app.session.togglePath\": true;\n\t\"app.session.toggleSort\": true;\n\t\"app.session.rename\": true;\n\t\"app.session.delete\": true;\n\t\"app.session.deleteNoninvasive\": true;\n\t\"app.models.save\": true;\n\t\"app.models.enableAll\": true;\n\t\"app.models.clearAll\": true;\n\t\"app.models.toggleProvider\": true;\n\t\"app.models.reorderUp\": true;\n\t\"app.models.reorderDown\": true;\n\t\"app.tree.filter.default\": true;\n\t\"app.tree.filter.noTools\": true;\n\t\"app.tree.filter.userOnly\": true;\n\t\"app.tree.filter.labeledOnly\": true;\n\t\"app.tree.filter.all\": true;\n\t\"app.tree.filter.cycleForward\": true;\n\t\"app.tree.filter.cycleBackward\": true;\n}\n\nexport type AppKeybinding = keyof AppKeybindings;\n\ndeclare module \"@kolisachint/hoocode-tui\" {\n\tinterface Keybindings extends AppKeybindings {}\n}\n\nexport const KEYBINDINGS = {\n\t...TUI_KEYBINDINGS,\n\t\"app.interrupt\": { defaultKeys: \"escape\", description: \"Cancel or abort\" },\n\t\"app.clear\": { defaultKeys: \"ctrl+c\", description: \"Clear editor\" },\n\t\"app.exit\": { defaultKeys: \"ctrl+d\", description: \"Exit when editor is empty\" },\n\t\"app.suspend\": {\n\t\tdefaultKeys: process.platform === \"win32\" ? [] : \"ctrl+z\",\n\t\tdescription: \"Suspend to background\",\n\t},\n\t\"app.thinking.cycle\": {\n\t\tdefaultKeys: \"shift+tab\",\n\t\tdescription: \"Cycle thinking level\",\n\t},\n\t\"app.model.cycleForward\": {\n\t\tdefaultKeys: \"ctrl+p\",\n\t\tdescription: \"Cycle to next model\",\n\t},\n\t\"app.model.cycleBackward\": {\n\t\tdefaultKeys: \"shift+ctrl+p\",\n\t\tdescription: \"Cycle to previous model\",\n\t},\n\t\"app.model.select\": { defaultKeys: \"ctrl+l\", description: \"Open model selector\" },\n\t\"app.tools.expand\": { defaultKeys: \"ctrl+o\", description: \"Toggle tool output\" },\n\t\"app.thinking.toggle\": {\n\t\tdefaultKeys: \"ctrl+t\",\n\t\tdescription: \"Toggle thinking blocks\",\n\t},\n\t\"app.tasks.cycleView\": {\n\t\t// ctrl+n is free in the main editor (no emacs next-line binding here);\n\t\t// app.session.toggleNamedFilter also uses ctrl+n but only inside the\n\t\t// session-selector overlay, which captures keys while open.\n\t\tdefaultKeys: \"ctrl+n\",\n\t\tdescription: \"Cycle task panel view (tasks → subagents → teams, skips empty lenses)\",\n\t},\n\t\"app.session.toggleNamedFilter\": {\n\t\tdefaultKeys: \"ctrl+n\",\n\t\tdescription: \"Toggle named session filter\",\n\t},\n\t\"app.team.focus\": {\n\t\t// Pairs with ctrl+n (cycle task panel view): alt+n steps INTO the teams\n\t\t// lens and focuses the role roster (--team only). Not shift+ctrl+n —\n\t\t// Windows Terminal intercepts that as its \"new window\" shortcut, the\n\t\t// same trap that moved app.tasks.cycleView off ctrl+shift+t.\n\t\tdefaultKeys: \"alt+n\",\n\t\tdescription: \"Focus the team roster (navigate roles, n nudge, a attach)\",\n\t},\n\t\"app.team.nudge\": {\n\t\t// Plain letters are safe here: these fire only while the task panel holds\n\t\t// focus (team-focus mode), never while typing in the editor.\n\t\tdefaultKeys: \"n\",\n\t\tdescription: \"Nudge the selected team role (team focus mode)\",\n\t},\n\t\"app.team.attach\": {\n\t\tdefaultKeys: \"a\",\n\t\tdescription: \"Attach to the selected team role (team focus mode)\",\n\t},\n\t\"app.editor.external\": {\n\t\tdefaultKeys: \"ctrl+g\",\n\t\tdescription: \"Open external editor\",\n\t},\n\t\"app.message.followUp\": {\n\t\tdefaultKeys: \"alt+enter\",\n\t\tdescription: \"Queue follow-up message\",\n\t},\n\t\"app.message.dequeue\": {\n\t\tdefaultKeys: \"alt+up\",\n\t\tdescription: \"Restore queued messages\",\n\t},\n\t\"app.clipboard.pasteImage\": {\n\t\tdefaultKeys: process.platform === \"win32\" ? \"alt+v\" : \"ctrl+v\",\n\t\tdescription: \"Paste image from clipboard\",\n\t},\n\t\"app.input.voiceTranscribe\": {\n\t\tdefaultKeys: \"ctrl+r\",\n\t\tdescription: \"Record voice and transcribe into the editor\",\n\t},\n\t\"app.session.new\": { defaultKeys: [], description: \"Start a new session\" },\n\t\"app.session.tree\": { defaultKeys: [], description: \"Open session tree\" },\n\t\"app.session.fork\": { defaultKeys: [], description: \"Fork current session\" },\n\t\"app.session.resume\": { defaultKeys: [], description: \"Resume a session\" },\n\t\"app.tree.foldOrUp\": {\n\t\tdefaultKeys: [\"ctrl+left\", \"alt+left\"],\n\t\tdescription: \"Fold tree branch or move up\",\n\t},\n\t\"app.tree.unfoldOrDown\": {\n\t\tdefaultKeys: [\"ctrl+right\", \"alt+right\"],\n\t\tdescription: \"Unfold tree branch or move down\",\n\t},\n\t\"app.tree.editLabel\": {\n\t\tdefaultKeys: \"shift+l\",\n\t\tdescription: \"Edit tree label\",\n\t},\n\t\"app.tree.toggleLabelTimestamp\": {\n\t\tdefaultKeys: \"shift+t\",\n\t\tdescription: \"Toggle tree label timestamps\",\n\t},\n\t\"app.session.togglePath\": {\n\t\tdefaultKeys: \"ctrl+p\",\n\t\tdescription: \"Toggle session path display\",\n\t},\n\t\"app.session.toggleSort\": {\n\t\tdefaultKeys: \"ctrl+s\",\n\t\tdescription: \"Toggle session sort mode\",\n\t},\n\t\"app.session.rename\": {\n\t\tdefaultKeys: \"ctrl+r\",\n\t\tdescription: \"Rename session\",\n\t},\n\t\"app.session.delete\": {\n\t\tdefaultKeys: \"ctrl+d\",\n\t\tdescription: \"Delete session\",\n\t},\n\t\"app.session.deleteNoninvasive\": {\n\t\tdefaultKeys: \"ctrl+backspace\",\n\t\tdescription: \"Delete session when query is empty\",\n\t},\n\t\"app.models.save\": {\n\t\tdefaultKeys: \"ctrl+s\",\n\t\tdescription: \"Save model selection\",\n\t},\n\t\"app.models.enableAll\": {\n\t\tdefaultKeys: \"ctrl+a\",\n\t\tdescription: \"Enable all models\",\n\t},\n\t\"app.models.clearAll\": {\n\t\tdefaultKeys: \"ctrl+x\",\n\t\tdescription: \"Clear all models\",\n\t},\n\t\"app.models.toggleProvider\": {\n\t\tdefaultKeys: \"ctrl+p\",\n\t\tdescription: \"Toggle all models for provider\",\n\t},\n\t\"app.models.reorderUp\": {\n\t\tdefaultKeys: \"alt+up\",\n\t\tdescription: \"Move model up in order\",\n\t},\n\t\"app.models.reorderDown\": {\n\t\tdefaultKeys: \"alt+down\",\n\t\tdescription: \"Move model down in order\",\n\t},\n\t\"app.options.next\": { defaultKeys: \"right\", description: \"Confirm and advance to the next question\" },\n\t\"app.options.back\": { defaultKeys: \"left\", description: \"Go back to the previous question\" },\n\t\"app.tree.filter.default\": {\n\t\tdefaultKeys: \"ctrl+d\",\n\t\tdescription: \"Tree filter: default view\",\n\t},\n\t\"app.tree.filter.noTools\": {\n\t\tdefaultKeys: \"ctrl+t\",\n\t\tdescription: \"Tree filter: hide tool results\",\n\t},\n\t\"app.tree.filter.userOnly\": {\n\t\tdefaultKeys: \"ctrl+u\",\n\t\tdescription: \"Tree filter: user messages only\",\n\t},\n\t\"app.tree.filter.labeledOnly\": {\n\t\tdefaultKeys: \"ctrl+l\",\n\t\tdescription: \"Tree filter: labeled entries only\",\n\t},\n\t\"app.tree.filter.all\": {\n\t\tdefaultKeys: \"ctrl+a\",\n\t\tdescription: \"Tree filter: show all entries\",\n\t},\n\t\"app.tree.filter.cycleForward\": {\n\t\tdefaultKeys: \"ctrl+o\",\n\t\tdescription: \"Tree filter: cycle forward\",\n\t},\n\t\"app.tree.filter.cycleBackward\": {\n\t\tdefaultKeys: \"shift+ctrl+o\",\n\t\tdescription: \"Tree filter: cycle backward\",\n\t},\n} as const satisfies KeybindingDefinitions;\n\nconst KEYBINDING_NAME_MIGRATIONS = {\n\tcursorUp: \"tui.editor.cursorUp\",\n\tcursorDown: \"tui.editor.cursorDown\",\n\tcursorLeft: \"tui.editor.cursorLeft\",\n\tcursorRight: \"tui.editor.cursorRight\",\n\tcursorWordLeft: \"tui.editor.cursorWordLeft\",\n\tcursorWordRight: \"tui.editor.cursorWordRight\",\n\tcursorLineStart: \"tui.editor.cursorLineStart\",\n\tcursorLineEnd: \"tui.editor.cursorLineEnd\",\n\tjumpForward: \"tui.editor.jumpForward\",\n\tjumpBackward: \"tui.editor.jumpBackward\",\n\tpageUp: \"tui.editor.pageUp\",\n\tpageDown: \"tui.editor.pageDown\",\n\tdeleteCharBackward: \"tui.editor.deleteCharBackward\",\n\tdeleteCharForward: \"tui.editor.deleteCharForward\",\n\tdeleteWordBackward: \"tui.editor.deleteWordBackward\",\n\tdeleteWordForward: \"tui.editor.deleteWordForward\",\n\tdeleteToLineStart: \"tui.editor.deleteToLineStart\",\n\tdeleteToLineEnd: \"tui.editor.deleteToLineEnd\",\n\tyank: \"tui.editor.yank\",\n\tyankPop: \"tui.editor.yankPop\",\n\tundo: \"tui.editor.undo\",\n\tnewLine: \"tui.input.newLine\",\n\tsubmit: \"tui.input.submit\",\n\ttab: \"tui.input.tab\",\n\tcopy: \"tui.input.copy\",\n\tselectUp: \"tui.select.up\",\n\tselectDown: \"tui.select.down\",\n\tselectPageUp: \"tui.select.pageUp\",\n\tselectPageDown: \"tui.select.pageDown\",\n\tselectConfirm: \"tui.select.confirm\",\n\tselectCancel: \"tui.select.cancel\",\n\tinterrupt: \"app.interrupt\",\n\tclear: \"app.clear\",\n\texit: \"app.exit\",\n\tsuspend: \"app.suspend\",\n\tcycleThinkingLevel: \"app.thinking.cycle\",\n\tcycleModelForward: \"app.model.cycleForward\",\n\tcycleModelBackward: \"app.model.cycleBackward\",\n\tselectModel: \"app.model.select\",\n\texpandTools: \"app.tools.expand\",\n\ttoggleThinking: \"app.thinking.toggle\",\n\ttoggleSessionNamedFilter: \"app.session.toggleNamedFilter\",\n\texternalEditor: \"app.editor.external\",\n\tfollowUp: \"app.message.followUp\",\n\tdequeue: \"app.message.dequeue\",\n\tpasteImage: \"app.clipboard.pasteImage\",\n\tnewSession: \"app.session.new\",\n\ttree: \"app.session.tree\",\n\tfork: \"app.session.fork\",\n\tresume: \"app.session.resume\",\n\ttreeFoldOrUp: \"app.tree.foldOrUp\",\n\ttreeUnfoldOrDown: \"app.tree.unfoldOrDown\",\n\ttreeEditLabel: \"app.tree.editLabel\",\n\ttreeToggleLabelTimestamp: \"app.tree.toggleLabelTimestamp\",\n\ttoggleSessionPath: \"app.session.togglePath\",\n\ttoggleSessionSort: \"app.session.toggleSort\",\n\trenameSession: \"app.session.rename\",\n\tdeleteSession: \"app.session.delete\",\n\tdeleteSessionNoninvasive: \"app.session.deleteNoninvasive\",\n} as const satisfies Record<string, Keybinding>;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isLegacyKeybindingName(key: string): key is keyof typeof KEYBINDING_NAME_MIGRATIONS {\n\treturn key in KEYBINDING_NAME_MIGRATIONS;\n}\n\nfunction toKeybindingsConfig(value: unknown): KeybindingsConfig {\n\tif (!isRecord(value)) return {};\n\n\tconst config: KeybindingsConfig = {};\n\tfor (const [key, binding] of Object.entries(value)) {\n\t\tif (typeof binding === \"string\") {\n\t\t\tconfig[key] = binding as KeyId;\n\t\t\tcontinue;\n\t\t}\n\t\tif (Array.isArray(binding) && binding.every((entry) => typeof entry === \"string\")) {\n\t\t\tconfig[key] = binding as KeyId[];\n\t\t}\n\t}\n\treturn config;\n}\n\nexport function migrateKeybindingsConfig(rawConfig: Record<string, unknown>): {\n\tconfig: Record<string, unknown>;\n\tmigrated: boolean;\n} {\n\tconst config: Record<string, unknown> = {};\n\tlet migrated = false;\n\n\tfor (const [key, value] of Object.entries(rawConfig)) {\n\t\tconst nextKey = isLegacyKeybindingName(key) ? KEYBINDING_NAME_MIGRATIONS[key] : key;\n\t\tif (nextKey !== key) {\n\t\t\tmigrated = true;\n\t\t}\n\t\tif (key !== nextKey && Object.hasOwn(rawConfig, nextKey)) {\n\t\t\tmigrated = true;\n\t\t\tcontinue;\n\t\t}\n\t\tconfig[nextKey] = value;\n\t}\n\n\treturn { config: orderKeybindingsConfig(config), migrated };\n}\n\nfunction orderKeybindingsConfig(config: Record<string, unknown>): Record<string, unknown> {\n\tconst ordered: Record<string, unknown> = {};\n\tfor (const keybinding of Object.keys(KEYBINDINGS)) {\n\t\tif (Object.hasOwn(config, keybinding)) {\n\t\t\tordered[keybinding] = config[keybinding];\n\t\t}\n\t}\n\n\tconst extras = Object.keys(config)\n\t\t.filter((key) => !Object.hasOwn(ordered, key))\n\t\t.sort();\n\tfor (const key of extras) {\n\t\tordered[key] = config[key];\n\t}\n\n\treturn ordered;\n}\n\nfunction loadRawConfig(path: string): Record<string, unknown> | undefined {\n\tif (!existsSync(path)) return undefined;\n\ttry {\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf-8\")) as unknown;\n\t\treturn isRecord(parsed) ? parsed : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport class KeybindingsManager extends TuiKeybindingsManager {\n\tprivate configPath: string | undefined;\n\n\tconstructor(userBindings: KeybindingsConfig = {}, configPath?: string) {\n\t\tsuper(KEYBINDINGS, userBindings);\n\t\tthis.configPath = configPath;\n\t}\n\n\tstatic create(agentDir: string = getAgentDir()): KeybindingsManager {\n\t\tconst configPath = join(agentDir, \"keybindings.json\");\n\t\tconst userBindings = KeybindingsManager.loadFromFile(configPath);\n\t\treturn new KeybindingsManager(userBindings, configPath);\n\t}\n\n\treload(): void {\n\t\tif (!this.configPath) return;\n\t\tthis.setUserBindings(KeybindingsManager.loadFromFile(this.configPath));\n\t}\n\n\tgetEffectiveConfig(): KeybindingsConfig {\n\t\treturn this.getResolvedBindings();\n\t}\n\n\tprivate static loadFromFile(path: string): KeybindingsConfig {\n\t\tconst rawConfig = loadRawConfig(path);\n\t\tif (!rawConfig) return {};\n\t\treturn toKeybindingsConfig(migrateKeybindingsConfig(rawConfig).config);\n\t}\n}\n\nexport type { KeyId, KeybindingsConfig };\n"]}
1
+ {"version":3,"file":"keybindings.d.ts","sourceRoot":"","sources":["../../src/core/keybindings.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\ttype Keybinding,\n\ttype KeybindingDefinitions,\n\ttype KeybindingsConfig,\n\ttype KeyId,\n\tTUI_KEYBINDINGS,\n\tKeybindingsManager as TuiKeybindingsManager,\n} from \"@kolisachint/hoocode-tui\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../config.js\";\n\ninterface AppKeybindings {\n\t\"app.interrupt\": true;\n\t\"app.clear\": true;\n\t\"app.exit\": true;\n\t\"app.suspend\": true;\n\t\"app.thinking.cycle\": true;\n\t\"app.model.cycleForward\": true;\n\t\"app.model.cycleBackward\": true;\n\t\"app.model.select\": true;\n\t\"app.tools.expand\": true;\n\t\"app.view.cycleForward\": true;\n\t\"app.view.cycleBackward\": true;\n\t\"app.tools.unfoldOne\": true;\n\t\"app.tools.foldOne\": true;\n\t\"app.thinking.toggle\": true;\n\t\"app.tasks.cycleView\": true;\n\t\"app.team.focus\": true;\n\t\"app.team.nudge\": true;\n\t\"app.team.attach\": true;\n\t\"app.session.toggleNamedFilter\": true;\n\t\"app.editor.external\": true;\n\t\"app.message.followUp\": true;\n\t\"app.message.dequeue\": true;\n\t\"app.clipboard.pasteImage\": true;\n\t\"app.input.voiceTranscribe\": true;\n\t\"app.session.new\": true;\n\t\"app.session.tree\": true;\n\t\"app.session.fork\": true;\n\t\"app.session.resume\": true;\n\t\"app.session.changeDirectory\": true;\n\t\"app.settings.open\": true;\n\t\"app.hotkeys.open\": true;\n\t\"app.mode.cycle\": true;\n\t\"app.tree.foldOrUp\": true;\n\t\"app.tree.unfoldOrDown\": true;\n\t\"app.tree.editLabel\": true;\n\t\"app.tree.toggleLabelTimestamp\": true;\n\t\"app.session.togglePath\": true;\n\t\"app.session.toggleSort\": true;\n\t\"app.session.rename\": true;\n\t\"app.session.delete\": true;\n\t\"app.session.deleteNoninvasive\": true;\n\t\"app.models.save\": true;\n\t\"app.models.enableAll\": true;\n\t\"app.models.clearAll\": true;\n\t\"app.models.toggleProvider\": true;\n\t\"app.models.reorderUp\": true;\n\t\"app.models.reorderDown\": true;\n\t\"app.tree.filter.default\": true;\n\t\"app.tree.filter.noTools\": true;\n\t\"app.tree.filter.userOnly\": true;\n\t\"app.tree.filter.labeledOnly\": true;\n\t\"app.tree.filter.all\": true;\n\t\"app.tree.filter.cycleForward\": true;\n\t\"app.tree.filter.cycleBackward\": true;\n}\n\nexport type AppKeybinding = keyof AppKeybindings;\n\ndeclare module \"@kolisachint/hoocode-tui\" {\n\tinterface Keybindings extends AppKeybindings {}\n}\n\n/**\n * The cockpit layout.\n *\n * Three rings, and which ring a key belongs to is decided by its modifier:\n *\n * - **ctrl — the view.** What is on screen right now: expand, thinking blocks,\n * task panel, model cycling. Pressed many times a minute, so they sit under\n * the hand and never require a second modifier.\n * - **alt — the cockpit.** What the agent *is* and where it works: model,\n * mode, working directory, settings, sessions. Pressed a few times a session.\n * - **overlays.** Inside a picker, the query line is a text field, so every\n * `ctrl+<letter>` there belongs to the *text* (ctrl+a start of line, ctrl+u\n * kill line, ctrl+w kill word). A picker's own verbs are therefore all on\n * `alt+<letter>`, mnemonic to that picker; the picker captures keys while it\n * is open, so reusing a global letter there is unambiguous.\n *\n * Two rules keep the set learnable: shift reverses whatever the unshifted key\n * does, and no binding takes a key the editor or the terminal already owns\n * (ctrl+a/e/b/f/k/u/w/y/d/l/r/g, ctrl+s XOFF, ctrl+m == enter, ctrl+i == tab).\n *\n * Two constraints from the key parser shape which alt keys are usable at all,\n * and `test/keybinding-layout.test.ts` holds both:\n *\n * - Only `alt+<letter>` and `alt+<digit>` survive a terminal without the Kitty\n * keyboard protocol. `alt+[` and `alt+]` arrive as the CSI and OSC\n * introducers and are not parsed as keys; nor is any shift-modified key, so a\n * `shift+…` default is a Kitty-only convenience on top of an unshifted key\n * that works everywhere (as `shift+ctrl+p` has always been).\n * - Legacy `alt+p` and `alt+n` are also accepted as `alt+up` and `alt+down`\n * (the emacs previous/next aliases), so no scope may bind both halves of\n * either pair.\n */\nexport const KEYBINDINGS = {\n\t...TUI_KEYBINDINGS,\n\t\"app.interrupt\": { defaultKeys: \"escape\", description: \"Cancel or abort\" },\n\t\"app.clear\": { defaultKeys: \"ctrl+c\", description: \"Clear editor\" },\n\t\"app.exit\": { defaultKeys: \"ctrl+d\", description: \"Exit when editor is empty\" },\n\t\"app.suspend\": {\n\t\tdefaultKeys: process.platform === \"win32\" ? [] : \"ctrl+z\",\n\t\tdescription: \"Suspend to background\",\n\t},\n\t\"app.thinking.cycle\": {\n\t\tdefaultKeys: \"shift+tab\",\n\t\tdescription: \"Cycle thinking level\",\n\t},\n\t\"app.model.cycleForward\": {\n\t\tdefaultKeys: \"ctrl+p\",\n\t\tdescription: \"Cycle to next model\",\n\t},\n\t\"app.model.cycleBackward\": {\n\t\tdefaultKeys: \"shift+ctrl+p\",\n\t\tdescription: \"Cycle to previous model\",\n\t},\n\t// alt+m, not ctrl+l: ctrl+l is \"clear the screen\" in every shell and every\n\t// full-screen terminal app, and users press it reflexively to tidy up. It\n\t// opening a model dialog is the single most surprising key in the old set.\n\t\"app.model.select\": { defaultKeys: \"alt+m\", description: \"Open model selector\" },\n\t\"app.tools.expand\": {\n\t\tdefaultKeys: \"ctrl+o\",\n\t\tdescription: \"Expand or collapse what is in front of you (tool bodies, header, summaries)\",\n\t},\n\t// The view dial pairs with ctrl+o on purpose: same letter, different ring.\n\t// ctrl+o opens what is already there; alt+o decides how much is ever there.\n\t\"app.view.cycleForward\": {\n\t\tdefaultKeys: \"alt+o\",\n\t\tdescription: \"Cycle tool output view (radar → glance → full)\",\n\t},\n\t\"app.view.cycleBackward\": {\n\t\tdefaultKeys: \"shift+alt+o\",\n\t\tdescription: \"Cycle tool output view backward\",\n\t},\n\t// The one-at-a-time counterpart to ctrl+o's all-or-nothing. The transcript is\n\t// bottom-anchored with no app-level scrolling — anything far enough up is in\n\t// the terminal's own scrollback, which this process cannot address — so \"open\n\t// one\" can only ever mean \"open one at the tail\". Repeating the key peels\n\t// backwards from there.\n\t\"app.tools.unfoldOne\": {\n\t\tdefaultKeys: \"alt+u\",\n\t\tdescription: \"Open the newest folded thing — a chain in radar, a tool body otherwise\",\n\t},\n\t\"app.tools.foldOne\": {\n\t\tdefaultKeys: \"shift+alt+u\",\n\t\tdescription: \"Re-fold the most recently opened chain or tool block\",\n\t},\n\t\"app.thinking.toggle\": {\n\t\tdefaultKeys: \"ctrl+t\",\n\t\tdescription: \"Toggle thinking blocks\",\n\t},\n\t\"app.tasks.cycleView\": {\n\t\t// ctrl+n is free in the main editor (no emacs next-line binding here).\n\t\tdefaultKeys: \"ctrl+n\",\n\t\tdescription: \"Cycle task panel view (tasks → subagents → teams, skips empty lenses)\",\n\t},\n\t\"app.team.focus\": {\n\t\t// Pairs with ctrl+n (cycle task panel view): alt+n steps INTO the teams\n\t\t// lens and focuses the role roster (--team only). Not shift+ctrl+n —\n\t\t// Windows Terminal intercepts that as its \"new window\" shortcut, the\n\t\t// same trap that moved app.tasks.cycleView off ctrl+shift+t.\n\t\tdefaultKeys: \"alt+n\",\n\t\tdescription: \"Focus the team roster (navigate roles, n nudge, a attach)\",\n\t},\n\t\"app.team.nudge\": {\n\t\t// Plain letters are safe here: these fire only while the task panel holds\n\t\t// focus (team-focus mode), never while typing in the editor.\n\t\tdefaultKeys: \"n\",\n\t\tdescription: \"Nudge the selected team role (team focus mode)\",\n\t},\n\t\"app.team.attach\": {\n\t\tdefaultKeys: \"a\",\n\t\tdescription: \"Attach to the selected team role (team focus mode)\",\n\t},\n\t// alt+e, not ctrl+g: ctrl+g is emacs/readline \"abort\" — the key you hit to\n\t// get out of something. Having it launch $EDITOR inverts that reflex.\n\t\"app.editor.external\": {\n\t\tdefaultKeys: \"alt+e\",\n\t\tdescription: \"Open external editor\",\n\t},\n\t\"app.message.followUp\": {\n\t\tdefaultKeys: \"alt+enter\",\n\t\tdescription: \"Queue follow-up message\",\n\t},\n\t\"app.message.dequeue\": {\n\t\tdefaultKeys: \"alt+up\",\n\t\tdescription: \"Restore queued messages\",\n\t},\n\t\"app.clipboard.pasteImage\": {\n\t\tdefaultKeys: process.platform === \"win32\" ? \"alt+v\" : \"ctrl+v\",\n\t\tdescription: \"Paste image from clipboard\",\n\t},\n\t// alt+r, not ctrl+r: ctrl+r is reverse history search in every shell, and it\n\t// was also the session picker's rename key — one chord, two meanings.\n\t\"app.input.voiceTranscribe\": {\n\t\tdefaultKeys: \"alt+r\",\n\t\tdescription: \"Record voice and transcribe into the editor\",\n\t},\n\t// The two destructive-ish session moves stay unbound by default. /new\n\t// replaces the transcript and /fork needs a message picked out of it, so\n\t// neither wants to be one stray chord away; both remain bindable by hand.\n\t\"app.session.new\": { defaultKeys: [], description: \"Start a new session\" },\n\t\"app.session.fork\": { defaultKeys: [], description: \"Fork current session\" },\n\t// Navigation is non-destructive, so it gets keys: t for tree, h for history.\n\t\"app.session.tree\": { defaultKeys: \"alt+t\", description: \"Open session tree\" },\n\t\"app.session.resume\": { defaultKeys: \"alt+h\", description: \"Resume a session from history\" },\n\t\"app.session.changeDirectory\": {\n\t\tdefaultKeys: \"alt+w\",\n\t\tdescription: \"Change working directory (move to another repo without quitting)\",\n\t},\n\t\"app.settings.open\": { defaultKeys: \"alt+s\", description: \"Open settings\" },\n\t\"app.hotkeys.open\": { defaultKeys: \"alt+k\", description: \"Show keyboard shortcuts\" },\n\t\"app.mode.cycle\": {\n\t\tdefaultKeys: \"alt+g\",\n\t\tdescription: \"Cycle agent mode (ask → plan → build → debug)\",\n\t},\n\t\"app.tree.foldOrUp\": {\n\t\tdefaultKeys: [\"ctrl+left\", \"alt+left\"],\n\t\tdescription: \"Fold tree branch or move up\",\n\t},\n\t\"app.tree.unfoldOrDown\": {\n\t\tdefaultKeys: [\"ctrl+right\", \"alt+right\"],\n\t\tdescription: \"Unfold tree branch or move down\",\n\t},\n\t\"app.tree.editLabel\": {\n\t\tdefaultKeys: \"shift+l\",\n\t\tdescription: \"Edit tree label\",\n\t},\n\t\"app.tree.toggleLabelTimestamp\": {\n\t\tdefaultKeys: \"shift+t\",\n\t\tdescription: \"Toggle tree label timestamps\",\n\t},\n\t// Session picker verbs. All on alt so the picker's query line keeps the\n\t// emacs editing keys it used to lose: ctrl+a jumped to the start of the\n\t// query *and* enabled every model, ctrl+d deleted a character *and* deleted\n\t// the highlighted session.\n\t\"app.session.togglePath\": {\n\t\tdefaultKeys: \"alt+p\",\n\t\tdescription: \"Toggle session path display\",\n\t},\n\t\"app.session.toggleSort\": {\n\t\tdefaultKeys: \"alt+o\",\n\t\tdescription: \"Toggle session sort order\",\n\t},\n\t\"app.session.rename\": {\n\t\tdefaultKeys: \"alt+r\",\n\t\tdescription: \"Rename session\",\n\t},\n\t\"app.session.toggleNamedFilter\": {\n\t\tdefaultKeys: \"alt+n\",\n\t\tdescription: \"Toggle named session filter\",\n\t},\n\t\"app.session.delete\": {\n\t\tdefaultKeys: \"alt+x\",\n\t\tdescription: \"Delete session\",\n\t},\n\t\"app.session.deleteNoninvasive\": {\n\t\tdefaultKeys: \"ctrl+backspace\",\n\t\tdescription: \"Delete session when query is empty\",\n\t},\n\t// Model picker verbs, same rule. ctrl+s in particular was XOFF: on a terminal\n\t// with flow control still on, \"save\" froze the session instead.\n\t\"app.models.save\": {\n\t\tdefaultKeys: \"alt+s\",\n\t\tdescription: \"Save model selection\",\n\t},\n\t\"app.models.enableAll\": {\n\t\tdefaultKeys: \"alt+a\",\n\t\tdescription: \"Enable all models\",\n\t},\n\t\"app.models.clearAll\": {\n\t\tdefaultKeys: \"alt+x\",\n\t\tdescription: \"Clear all models\",\n\t},\n\t// alt+g (\"group\"), not alt+p: on a terminal without the Kitty protocol, alt+p\n\t// arrives as ESC-p, which the key parser also accepts as alt+up — and alt+up\n\t// is this same picker's reorder key.\n\t\"app.models.toggleProvider\": {\n\t\tdefaultKeys: \"alt+g\",\n\t\tdescription: \"Toggle all models for provider\",\n\t},\n\t\"app.models.reorderUp\": {\n\t\tdefaultKeys: \"alt+up\",\n\t\tdescription: \"Move model up in order\",\n\t},\n\t\"app.models.reorderDown\": {\n\t\tdefaultKeys: \"alt+down\",\n\t\tdescription: \"Move model down in order\",\n\t},\n\t\"app.options.next\": { defaultKeys: \"right\", description: \"Confirm and advance to the next question\" },\n\t\"app.options.back\": { defaultKeys: \"left\", description: \"Go back to the previous question\" },\n\t// Five lenses in a fixed order, so they are numbered rather than lettered:\n\t// alt+1..alt+5 needs no mnemonic and collides with nothing. The old set\n\t// (ctrl+d/t/u/l/a) collided with delete-char, thinking, kill-to-start,\n\t// clear-screen and start-of-line respectively.\n\t\"app.tree.filter.default\": {\n\t\tdefaultKeys: \"alt+1\",\n\t\tdescription: \"Tree filter: default view\",\n\t},\n\t\"app.tree.filter.noTools\": {\n\t\tdefaultKeys: \"alt+2\",\n\t\tdescription: \"Tree filter: hide tool results\",\n\t},\n\t\"app.tree.filter.userOnly\": {\n\t\tdefaultKeys: \"alt+3\",\n\t\tdescription: \"Tree filter: user messages only\",\n\t},\n\t\"app.tree.filter.labeledOnly\": {\n\t\tdefaultKeys: \"alt+4\",\n\t\tdescription: \"Tree filter: labeled entries only\",\n\t},\n\t\"app.tree.filter.all\": {\n\t\tdefaultKeys: \"alt+5\",\n\t\tdescription: \"Tree filter: show all entries\",\n\t},\n\t// alt+c for \"cycle\". Not a bracket pair, tempting as `alt+[` / `alt+]` are:\n\t// without the Kitty protocol those arrive as ESC-[ and ESC-], which are the\n\t// CSI and OSC introducers and so are not parsed as keys at all. The backward\n\t// half needs Kitty, like every other shift-modified default here.\n\t\"app.tree.filter.cycleForward\": {\n\t\tdefaultKeys: \"alt+c\",\n\t\tdescription: \"Tree filter: cycle forward\",\n\t},\n\t\"app.tree.filter.cycleBackward\": {\n\t\tdefaultKeys: \"shift+alt+c\",\n\t\tdescription: \"Tree filter: cycle backward\",\n\t},\n} as const satisfies KeybindingDefinitions;\n\nconst KEYBINDING_NAME_MIGRATIONS = {\n\tcursorUp: \"tui.editor.cursorUp\",\n\tcursorDown: \"tui.editor.cursorDown\",\n\tcursorLeft: \"tui.editor.cursorLeft\",\n\tcursorRight: \"tui.editor.cursorRight\",\n\tcursorWordLeft: \"tui.editor.cursorWordLeft\",\n\tcursorWordRight: \"tui.editor.cursorWordRight\",\n\tcursorLineStart: \"tui.editor.cursorLineStart\",\n\tcursorLineEnd: \"tui.editor.cursorLineEnd\",\n\tjumpForward: \"tui.editor.jumpForward\",\n\tjumpBackward: \"tui.editor.jumpBackward\",\n\tpageUp: \"tui.editor.pageUp\",\n\tpageDown: \"tui.editor.pageDown\",\n\tdeleteCharBackward: \"tui.editor.deleteCharBackward\",\n\tdeleteCharForward: \"tui.editor.deleteCharForward\",\n\tdeleteWordBackward: \"tui.editor.deleteWordBackward\",\n\tdeleteWordForward: \"tui.editor.deleteWordForward\",\n\tdeleteToLineStart: \"tui.editor.deleteToLineStart\",\n\tdeleteToLineEnd: \"tui.editor.deleteToLineEnd\",\n\tyank: \"tui.editor.yank\",\n\tyankPop: \"tui.editor.yankPop\",\n\tundo: \"tui.editor.undo\",\n\tnewLine: \"tui.input.newLine\",\n\tsubmit: \"tui.input.submit\",\n\ttab: \"tui.input.tab\",\n\tcopy: \"tui.input.copy\",\n\tselectUp: \"tui.select.up\",\n\tselectDown: \"tui.select.down\",\n\tselectPageUp: \"tui.select.pageUp\",\n\tselectPageDown: \"tui.select.pageDown\",\n\tselectConfirm: \"tui.select.confirm\",\n\tselectCancel: \"tui.select.cancel\",\n\tinterrupt: \"app.interrupt\",\n\tclear: \"app.clear\",\n\texit: \"app.exit\",\n\tsuspend: \"app.suspend\",\n\tcycleThinkingLevel: \"app.thinking.cycle\",\n\tcycleModelForward: \"app.model.cycleForward\",\n\tcycleModelBackward: \"app.model.cycleBackward\",\n\tselectModel: \"app.model.select\",\n\texpandTools: \"app.tools.expand\",\n\ttoggleThinking: \"app.thinking.toggle\",\n\ttoggleSessionNamedFilter: \"app.session.toggleNamedFilter\",\n\texternalEditor: \"app.editor.external\",\n\tfollowUp: \"app.message.followUp\",\n\tdequeue: \"app.message.dequeue\",\n\tpasteImage: \"app.clipboard.pasteImage\",\n\tnewSession: \"app.session.new\",\n\ttree: \"app.session.tree\",\n\tfork: \"app.session.fork\",\n\tresume: \"app.session.resume\",\n\ttreeFoldOrUp: \"app.tree.foldOrUp\",\n\ttreeUnfoldOrDown: \"app.tree.unfoldOrDown\",\n\ttreeEditLabel: \"app.tree.editLabel\",\n\ttreeToggleLabelTimestamp: \"app.tree.toggleLabelTimestamp\",\n\ttoggleSessionPath: \"app.session.togglePath\",\n\ttoggleSessionSort: \"app.session.toggleSort\",\n\trenameSession: \"app.session.rename\",\n\tdeleteSession: \"app.session.delete\",\n\tdeleteSessionNoninvasive: \"app.session.deleteNoninvasive\",\n} as const satisfies Record<string, Keybinding>;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isLegacyKeybindingName(key: string): key is keyof typeof KEYBINDING_NAME_MIGRATIONS {\n\treturn key in KEYBINDING_NAME_MIGRATIONS;\n}\n\nfunction toKeybindingsConfig(value: unknown): KeybindingsConfig {\n\tif (!isRecord(value)) return {};\n\n\tconst config: KeybindingsConfig = {};\n\tfor (const [key, binding] of Object.entries(value)) {\n\t\tif (typeof binding === \"string\") {\n\t\t\tconfig[key] = binding as KeyId;\n\t\t\tcontinue;\n\t\t}\n\t\tif (Array.isArray(binding) && binding.every((entry) => typeof entry === \"string\")) {\n\t\t\tconfig[key] = binding as KeyId[];\n\t\t}\n\t}\n\treturn config;\n}\n\nexport function migrateKeybindingsConfig(rawConfig: Record<string, unknown>): {\n\tconfig: Record<string, unknown>;\n\tmigrated: boolean;\n} {\n\tconst config: Record<string, unknown> = {};\n\tlet migrated = false;\n\n\tfor (const [key, value] of Object.entries(rawConfig)) {\n\t\tconst nextKey = isLegacyKeybindingName(key) ? KEYBINDING_NAME_MIGRATIONS[key] : key;\n\t\tif (nextKey !== key) {\n\t\t\tmigrated = true;\n\t\t}\n\t\tif (key !== nextKey && Object.hasOwn(rawConfig, nextKey)) {\n\t\t\tmigrated = true;\n\t\t\tcontinue;\n\t\t}\n\t\tconfig[nextKey] = value;\n\t}\n\n\treturn { config: orderKeybindingsConfig(config), migrated };\n}\n\nfunction orderKeybindingsConfig(config: Record<string, unknown>): Record<string, unknown> {\n\tconst ordered: Record<string, unknown> = {};\n\tfor (const keybinding of Object.keys(KEYBINDINGS)) {\n\t\tif (Object.hasOwn(config, keybinding)) {\n\t\t\tordered[keybinding] = config[keybinding];\n\t\t}\n\t}\n\n\tconst extras = Object.keys(config)\n\t\t.filter((key) => !Object.hasOwn(ordered, key))\n\t\t.sort();\n\tfor (const key of extras) {\n\t\tordered[key] = config[key];\n\t}\n\n\treturn ordered;\n}\n\nfunction loadRawConfig(path: string): Record<string, unknown> | undefined {\n\tif (!existsSync(path)) return undefined;\n\ttry {\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf-8\")) as unknown;\n\t\treturn isRecord(parsed) ? parsed : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport class KeybindingsManager extends TuiKeybindingsManager {\n\tprivate configPath: string | undefined;\n\n\tconstructor(userBindings: KeybindingsConfig = {}, configPath?: string) {\n\t\tsuper(KEYBINDINGS, userBindings);\n\t\tthis.configPath = configPath;\n\t}\n\n\tstatic create(agentDir: string = getAgentDir()): KeybindingsManager {\n\t\tconst configPath = join(agentDir, \"keybindings.json\");\n\t\tconst userBindings = KeybindingsManager.loadFromFile(configPath);\n\t\treturn new KeybindingsManager(userBindings, configPath);\n\t}\n\n\treload(): void {\n\t\tif (!this.configPath) return;\n\t\tthis.setUserBindings(KeybindingsManager.loadFromFile(this.configPath));\n\t}\n\n\tgetEffectiveConfig(): KeybindingsConfig {\n\t\treturn this.getResolvedBindings();\n\t}\n\n\tprivate static loadFromFile(path: string): KeybindingsConfig {\n\t\tconst rawConfig = loadRawConfig(path);\n\t\tif (!rawConfig) return {};\n\t\treturn toKeybindingsConfig(migrateKeybindingsConfig(rawConfig).config);\n\t}\n}\n\nexport type { KeyId, KeybindingsConfig };\n"]}
@@ -2,6 +2,38 @@ import { TUI_KEYBINDINGS, KeybindingsManager as TuiKeybindingsManager, } from "@
2
2
  import { existsSync, readFileSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { getAgentDir } from "../config.js";
5
+ /**
6
+ * The cockpit layout.
7
+ *
8
+ * Three rings, and which ring a key belongs to is decided by its modifier:
9
+ *
10
+ * - **ctrl — the view.** What is on screen right now: expand, thinking blocks,
11
+ * task panel, model cycling. Pressed many times a minute, so they sit under
12
+ * the hand and never require a second modifier.
13
+ * - **alt — the cockpit.** What the agent *is* and where it works: model,
14
+ * mode, working directory, settings, sessions. Pressed a few times a session.
15
+ * - **overlays.** Inside a picker, the query line is a text field, so every
16
+ * `ctrl+<letter>` there belongs to the *text* (ctrl+a start of line, ctrl+u
17
+ * kill line, ctrl+w kill word). A picker's own verbs are therefore all on
18
+ * `alt+<letter>`, mnemonic to that picker; the picker captures keys while it
19
+ * is open, so reusing a global letter there is unambiguous.
20
+ *
21
+ * Two rules keep the set learnable: shift reverses whatever the unshifted key
22
+ * does, and no binding takes a key the editor or the terminal already owns
23
+ * (ctrl+a/e/b/f/k/u/w/y/d/l/r/g, ctrl+s XOFF, ctrl+m == enter, ctrl+i == tab).
24
+ *
25
+ * Two constraints from the key parser shape which alt keys are usable at all,
26
+ * and `test/keybinding-layout.test.ts` holds both:
27
+ *
28
+ * - Only `alt+<letter>` and `alt+<digit>` survive a terminal without the Kitty
29
+ * keyboard protocol. `alt+[` and `alt+]` arrive as the CSI and OSC
30
+ * introducers and are not parsed as keys; nor is any shift-modified key, so a
31
+ * `shift+…` default is a Kitty-only convenience on top of an unshifted key
32
+ * that works everywhere (as `shift+ctrl+p` has always been).
33
+ * - Legacy `alt+p` and `alt+n` are also accepted as `alt+up` and `alt+down`
34
+ * (the emacs previous/next aliases), so no scope may bind both halves of
35
+ * either pair.
36
+ */
5
37
  export const KEYBINDINGS = {
6
38
  ...TUI_KEYBINDINGS,
7
39
  "app.interrupt": { defaultKeys: "escape", description: "Cancel or abort" },
@@ -23,23 +55,46 @@ export const KEYBINDINGS = {
23
55
  defaultKeys: "shift+ctrl+p",
24
56
  description: "Cycle to previous model",
25
57
  },
26
- "app.model.select": { defaultKeys: "ctrl+l", description: "Open model selector" },
27
- "app.tools.expand": { defaultKeys: "ctrl+o", description: "Toggle tool output" },
58
+ // alt+m, not ctrl+l: ctrl+l is "clear the screen" in every shell and every
59
+ // full-screen terminal app, and users press it reflexively to tidy up. It
60
+ // opening a model dialog is the single most surprising key in the old set.
61
+ "app.model.select": { defaultKeys: "alt+m", description: "Open model selector" },
62
+ "app.tools.expand": {
63
+ defaultKeys: "ctrl+o",
64
+ description: "Expand or collapse what is in front of you (tool bodies, header, summaries)",
65
+ },
66
+ // The view dial pairs with ctrl+o on purpose: same letter, different ring.
67
+ // ctrl+o opens what is already there; alt+o decides how much is ever there.
68
+ "app.view.cycleForward": {
69
+ defaultKeys: "alt+o",
70
+ description: "Cycle tool output view (radar → glance → full)",
71
+ },
72
+ "app.view.cycleBackward": {
73
+ defaultKeys: "shift+alt+o",
74
+ description: "Cycle tool output view backward",
75
+ },
76
+ // The one-at-a-time counterpart to ctrl+o's all-or-nothing. The transcript is
77
+ // bottom-anchored with no app-level scrolling — anything far enough up is in
78
+ // the terminal's own scrollback, which this process cannot address — so "open
79
+ // one" can only ever mean "open one at the tail". Repeating the key peels
80
+ // backwards from there.
81
+ "app.tools.unfoldOne": {
82
+ defaultKeys: "alt+u",
83
+ description: "Open the newest folded thing — a chain in radar, a tool body otherwise",
84
+ },
85
+ "app.tools.foldOne": {
86
+ defaultKeys: "shift+alt+u",
87
+ description: "Re-fold the most recently opened chain or tool block",
88
+ },
28
89
  "app.thinking.toggle": {
29
90
  defaultKeys: "ctrl+t",
30
91
  description: "Toggle thinking blocks",
31
92
  },
32
93
  "app.tasks.cycleView": {
33
- // ctrl+n is free in the main editor (no emacs next-line binding here);
34
- // app.session.toggleNamedFilter also uses ctrl+n but only inside the
35
- // session-selector overlay, which captures keys while open.
94
+ // ctrl+n is free in the main editor (no emacs next-line binding here).
36
95
  defaultKeys: "ctrl+n",
37
96
  description: "Cycle task panel view (tasks → subagents → teams, skips empty lenses)",
38
97
  },
39
- "app.session.toggleNamedFilter": {
40
- defaultKeys: "ctrl+n",
41
- description: "Toggle named session filter",
42
- },
43
98
  "app.team.focus": {
44
99
  // Pairs with ctrl+n (cycle task panel view): alt+n steps INTO the teams
45
100
  // lens and focuses the role roster (--team only). Not shift+ctrl+n —
@@ -58,8 +113,10 @@ export const KEYBINDINGS = {
58
113
  defaultKeys: "a",
59
114
  description: "Attach to the selected team role (team focus mode)",
60
115
  },
116
+ // alt+e, not ctrl+g: ctrl+g is emacs/readline "abort" — the key you hit to
117
+ // get out of something. Having it launch $EDITOR inverts that reflex.
61
118
  "app.editor.external": {
62
- defaultKeys: "ctrl+g",
119
+ defaultKeys: "alt+e",
63
120
  description: "Open external editor",
64
121
  },
65
122
  "app.message.followUp": {
@@ -74,14 +131,30 @@ export const KEYBINDINGS = {
74
131
  defaultKeys: process.platform === "win32" ? "alt+v" : "ctrl+v",
75
132
  description: "Paste image from clipboard",
76
133
  },
134
+ // alt+r, not ctrl+r: ctrl+r is reverse history search in every shell, and it
135
+ // was also the session picker's rename key — one chord, two meanings.
77
136
  "app.input.voiceTranscribe": {
78
- defaultKeys: "ctrl+r",
137
+ defaultKeys: "alt+r",
79
138
  description: "Record voice and transcribe into the editor",
80
139
  },
140
+ // The two destructive-ish session moves stay unbound by default. /new
141
+ // replaces the transcript and /fork needs a message picked out of it, so
142
+ // neither wants to be one stray chord away; both remain bindable by hand.
81
143
  "app.session.new": { defaultKeys: [], description: "Start a new session" },
82
- "app.session.tree": { defaultKeys: [], description: "Open session tree" },
83
144
  "app.session.fork": { defaultKeys: [], description: "Fork current session" },
84
- "app.session.resume": { defaultKeys: [], description: "Resume a session" },
145
+ // Navigation is non-destructive, so it gets keys: t for tree, h for history.
146
+ "app.session.tree": { defaultKeys: "alt+t", description: "Open session tree" },
147
+ "app.session.resume": { defaultKeys: "alt+h", description: "Resume a session from history" },
148
+ "app.session.changeDirectory": {
149
+ defaultKeys: "alt+w",
150
+ description: "Change working directory (move to another repo without quitting)",
151
+ },
152
+ "app.settings.open": { defaultKeys: "alt+s", description: "Open settings" },
153
+ "app.hotkeys.open": { defaultKeys: "alt+k", description: "Show keyboard shortcuts" },
154
+ "app.mode.cycle": {
155
+ defaultKeys: "alt+g",
156
+ description: "Cycle agent mode (ask → plan → build → debug)",
157
+ },
85
158
  "app.tree.foldOrUp": {
86
159
  defaultKeys: ["ctrl+left", "alt+left"],
87
160
  description: "Fold tree branch or move up",
@@ -98,40 +171,53 @@ export const KEYBINDINGS = {
98
171
  defaultKeys: "shift+t",
99
172
  description: "Toggle tree label timestamps",
100
173
  },
174
+ // Session picker verbs. All on alt so the picker's query line keeps the
175
+ // emacs editing keys it used to lose: ctrl+a jumped to the start of the
176
+ // query *and* enabled every model, ctrl+d deleted a character *and* deleted
177
+ // the highlighted session.
101
178
  "app.session.togglePath": {
102
- defaultKeys: "ctrl+p",
179
+ defaultKeys: "alt+p",
103
180
  description: "Toggle session path display",
104
181
  },
105
182
  "app.session.toggleSort": {
106
- defaultKeys: "ctrl+s",
107
- description: "Toggle session sort mode",
183
+ defaultKeys: "alt+o",
184
+ description: "Toggle session sort order",
108
185
  },
109
186
  "app.session.rename": {
110
- defaultKeys: "ctrl+r",
187
+ defaultKeys: "alt+r",
111
188
  description: "Rename session",
112
189
  },
190
+ "app.session.toggleNamedFilter": {
191
+ defaultKeys: "alt+n",
192
+ description: "Toggle named session filter",
193
+ },
113
194
  "app.session.delete": {
114
- defaultKeys: "ctrl+d",
195
+ defaultKeys: "alt+x",
115
196
  description: "Delete session",
116
197
  },
117
198
  "app.session.deleteNoninvasive": {
118
199
  defaultKeys: "ctrl+backspace",
119
200
  description: "Delete session when query is empty",
120
201
  },
202
+ // Model picker verbs, same rule. ctrl+s in particular was XOFF: on a terminal
203
+ // with flow control still on, "save" froze the session instead.
121
204
  "app.models.save": {
122
- defaultKeys: "ctrl+s",
205
+ defaultKeys: "alt+s",
123
206
  description: "Save model selection",
124
207
  },
125
208
  "app.models.enableAll": {
126
- defaultKeys: "ctrl+a",
209
+ defaultKeys: "alt+a",
127
210
  description: "Enable all models",
128
211
  },
129
212
  "app.models.clearAll": {
130
- defaultKeys: "ctrl+x",
213
+ defaultKeys: "alt+x",
131
214
  description: "Clear all models",
132
215
  },
216
+ // alt+g ("group"), not alt+p: on a terminal without the Kitty protocol, alt+p
217
+ // arrives as ESC-p, which the key parser also accepts as alt+up — and alt+up
218
+ // is this same picker's reorder key.
133
219
  "app.models.toggleProvider": {
134
- defaultKeys: "ctrl+p",
220
+ defaultKeys: "alt+g",
135
221
  description: "Toggle all models for provider",
136
222
  },
137
223
  "app.models.reorderUp": {
@@ -144,32 +230,40 @@ export const KEYBINDINGS = {
144
230
  },
145
231
  "app.options.next": { defaultKeys: "right", description: "Confirm and advance to the next question" },
146
232
  "app.options.back": { defaultKeys: "left", description: "Go back to the previous question" },
233
+ // Five lenses in a fixed order, so they are numbered rather than lettered:
234
+ // alt+1..alt+5 needs no mnemonic and collides with nothing. The old set
235
+ // (ctrl+d/t/u/l/a) collided with delete-char, thinking, kill-to-start,
236
+ // clear-screen and start-of-line respectively.
147
237
  "app.tree.filter.default": {
148
- defaultKeys: "ctrl+d",
238
+ defaultKeys: "alt+1",
149
239
  description: "Tree filter: default view",
150
240
  },
151
241
  "app.tree.filter.noTools": {
152
- defaultKeys: "ctrl+t",
242
+ defaultKeys: "alt+2",
153
243
  description: "Tree filter: hide tool results",
154
244
  },
155
245
  "app.tree.filter.userOnly": {
156
- defaultKeys: "ctrl+u",
246
+ defaultKeys: "alt+3",
157
247
  description: "Tree filter: user messages only",
158
248
  },
159
249
  "app.tree.filter.labeledOnly": {
160
- defaultKeys: "ctrl+l",
250
+ defaultKeys: "alt+4",
161
251
  description: "Tree filter: labeled entries only",
162
252
  },
163
253
  "app.tree.filter.all": {
164
- defaultKeys: "ctrl+a",
254
+ defaultKeys: "alt+5",
165
255
  description: "Tree filter: show all entries",
166
256
  },
257
+ // alt+c for "cycle". Not a bracket pair, tempting as `alt+[` / `alt+]` are:
258
+ // without the Kitty protocol those arrive as ESC-[ and ESC-], which are the
259
+ // CSI and OSC introducers and so are not parsed as keys at all. The backward
260
+ // half needs Kitty, like every other shift-modified default here.
167
261
  "app.tree.filter.cycleForward": {
168
- defaultKeys: "ctrl+o",
262
+ defaultKeys: "alt+c",
169
263
  description: "Tree filter: cycle forward",
170
264
  },
171
265
  "app.tree.filter.cycleBackward": {
172
- defaultKeys: "shift+ctrl+o",
266
+ defaultKeys: "shift+alt+c",
173
267
  description: "Tree filter: cycle backward",
174
268
  },
175
269
  };