@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.
- package/CHANGELOG.md +131 -0
- package/dist/core/agent-session-runtime.d.ts +28 -0
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +61 -2
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/keybindings.d.ts +102 -30
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +123 -29
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/settings-defaults.d.ts +1 -1
- package/dist/core/settings-defaults.d.ts.map +1 -1
- package/dist/core/settings-defaults.js +1 -1
- package/dist/core/settings-defaults.js.map +1 -1
- package/dist/core/settings-manager.d.ts +10 -3
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +19 -8
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/settings-types.d.ts +1 -1
- package/dist/core/settings-types.d.ts.map +1 -1
- package/dist/core/settings-types.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +5 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/tool-output-view.d.ts +33 -0
- package/dist/core/tool-output-view.d.ts.map +1 -0
- package/dist/core/tool-output-view.js +31 -0
- package/dist/core/tool-output-view.js.map +1 -0
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +4 -0
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/modes/interactive/command-executor.d.ts +20 -0
- package/dist/modes/interactive/command-executor.d.ts.map +1 -1
- package/dist/modes/interactive/command-executor.js +138 -9
- package/dist/modes/interactive/command-executor.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +4 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +31 -2
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.d.ts +2 -2
- package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js +2 -2
- package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +2 -2
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +3 -2
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +13 -12
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-chain-summary.d.ts +65 -0
- package/dist/modes/interactive/components/tool-chain-summary.d.ts.map +1 -0
- package/dist/modes/interactive/components/tool-chain-summary.js +216 -0
- package/dist/modes/interactive/components/tool-chain-summary.js.map +1 -0
- package/dist/modes/interactive/components/tool-chain.d.ts +61 -0
- package/dist/modes/interactive/components/tool-chain.d.ts.map +1 -0
- package/dist/modes/interactive/components/tool-chain.js +156 -0
- package/dist/modes/interactive/components/tool-chain.js.map +1 -0
- package/dist/modes/interactive/components/tool-execution.d.ts +55 -9
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +173 -33
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/tool-signal.d.ts +50 -0
- package/dist/modes/interactive/components/tool-signal.d.ts.map +1 -0
- package/dist/modes/interactive/components/tool-signal.js +151 -0
- package/dist/modes/interactive/components/tool-signal.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +50 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +239 -29
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/resource-display.d.ts +9 -0
- package/dist/modes/interactive/resource-display.d.ts.map +1 -1
- package/dist/modes/interactive/resource-display.js +13 -0
- package/dist/modes/interactive/resource-display.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- 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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
361
|
+
readonly defaultKeys: "alt+p";
|
|
294
362
|
readonly description: "Toggle session path display";
|
|
295
363
|
};
|
|
296
364
|
readonly "app.session.toggleSort": {
|
|
297
|
-
readonly defaultKeys: "
|
|
298
|
-
readonly description: "Toggle session sort
|
|
365
|
+
readonly defaultKeys: "alt+o";
|
|
366
|
+
readonly description: "Toggle session sort order";
|
|
299
367
|
};
|
|
300
368
|
readonly "app.session.rename": {
|
|
301
|
-
readonly defaultKeys: "
|
|
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: "
|
|
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: "
|
|
385
|
+
readonly defaultKeys: "alt+s";
|
|
314
386
|
readonly description: "Save model selection";
|
|
315
387
|
};
|
|
316
388
|
readonly "app.models.enableAll": {
|
|
317
|
-
readonly defaultKeys: "
|
|
389
|
+
readonly defaultKeys: "alt+a";
|
|
318
390
|
readonly description: "Enable all models";
|
|
319
391
|
};
|
|
320
392
|
readonly "app.models.clearAll": {
|
|
321
|
-
readonly defaultKeys: "
|
|
393
|
+
readonly defaultKeys: "alt+x";
|
|
322
394
|
readonly description: "Clear all models";
|
|
323
395
|
};
|
|
324
396
|
readonly "app.models.toggleProvider": {
|
|
325
|
-
readonly defaultKeys: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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+
|
|
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"]}
|
package/dist/core/keybindings.js
CHANGED
|
@@ -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
|
-
|
|
27
|
-
|
|
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: "
|
|
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: "
|
|
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
|
-
|
|
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: "
|
|
179
|
+
defaultKeys: "alt+p",
|
|
103
180
|
description: "Toggle session path display",
|
|
104
181
|
},
|
|
105
182
|
"app.session.toggleSort": {
|
|
106
|
-
defaultKeys: "
|
|
107
|
-
description: "Toggle session sort
|
|
183
|
+
defaultKeys: "alt+o",
|
|
184
|
+
description: "Toggle session sort order",
|
|
108
185
|
},
|
|
109
186
|
"app.session.rename": {
|
|
110
|
-
defaultKeys: "
|
|
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: "
|
|
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: "
|
|
205
|
+
defaultKeys: "alt+s",
|
|
123
206
|
description: "Save model selection",
|
|
124
207
|
},
|
|
125
208
|
"app.models.enableAll": {
|
|
126
|
-
defaultKeys: "
|
|
209
|
+
defaultKeys: "alt+a",
|
|
127
210
|
description: "Enable all models",
|
|
128
211
|
},
|
|
129
212
|
"app.models.clearAll": {
|
|
130
|
-
defaultKeys: "
|
|
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: "
|
|
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: "
|
|
238
|
+
defaultKeys: "alt+1",
|
|
149
239
|
description: "Tree filter: default view",
|
|
150
240
|
},
|
|
151
241
|
"app.tree.filter.noTools": {
|
|
152
|
-
defaultKeys: "
|
|
242
|
+
defaultKeys: "alt+2",
|
|
153
243
|
description: "Tree filter: hide tool results",
|
|
154
244
|
},
|
|
155
245
|
"app.tree.filter.userOnly": {
|
|
156
|
-
defaultKeys: "
|
|
246
|
+
defaultKeys: "alt+3",
|
|
157
247
|
description: "Tree filter: user messages only",
|
|
158
248
|
},
|
|
159
249
|
"app.tree.filter.labeledOnly": {
|
|
160
|
-
defaultKeys: "
|
|
250
|
+
defaultKeys: "alt+4",
|
|
161
251
|
description: "Tree filter: labeled entries only",
|
|
162
252
|
},
|
|
163
253
|
"app.tree.filter.all": {
|
|
164
|
-
defaultKeys: "
|
|
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: "
|
|
262
|
+
defaultKeys: "alt+c",
|
|
169
263
|
description: "Tree filter: cycle forward",
|
|
170
264
|
},
|
|
171
265
|
"app.tree.filter.cycleBackward": {
|
|
172
|
-
defaultKeys: "shift+
|
|
266
|
+
defaultKeys: "shift+alt+c",
|
|
173
267
|
description: "Tree filter: cycle backward",
|
|
174
268
|
},
|
|
175
269
|
};
|