Skip to content

Commit c3c6df1

Browse files
mswiszczclaude
andcommitted
feat: add block focus cycling (CW/CCW) and lowercase all keybinding IDs
- Add block:focusnext (Ctrl+Shift+]) and block:focusprev (Ctrl+Shift+[) to cycle focus through blocks in leaf order - Lowercase all keybinding command IDs to match settings.json conventions (e.g. block:splitRight → block:splitright, app:toggleAIPanel → app:toggleaipanel) - Update schema and docs to reflect both changes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e2c04f3 commit c3c6df1

3 files changed

Lines changed: 91 additions & 65 deletions

File tree

docs/docs/keybindings.mdx

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ Chords are shown with a + between the keys. You have 2 seconds to hit the 2nd ch
4343
| <Kbd k="Ctrl:Shift:0"/> | Focus WaveAI input |
4444
| <Kbd k="Ctrl:Shift:1-9"/> | Switch to block number |
4545
| <Kbd k="Ctrl:Shift:Arrows"/> / <Kbd k="Ctrl:Shift:h/j/k/l"/> | Move left, right, up, down between blocks |
46+
| <Kbd k="Ctrl:Shift:]"/> | Cycle block focus forward (CW) |
47+
| <Kbd k="Ctrl:Shift:["/> | Cycle block focus backward (CCW) |
4648
| <Kbd k="Ctrl:Shift:x"/> | Replace the current block with a launcher block |
4749
| <Kbd k="Cmd:1-9"/> | Switch to tab number |
4850
| <Kbd k="Cmd:["/> / <Kbd k="Shift:Cmd:["/> | Switch tab left |
@@ -131,15 +133,15 @@ Key combinations use colon-separated format:
131133
**Disable a keybinding:** Remove <Kbd k="Cmd:w"/> close block:
132134
```json
133135
[
134-
{ "key": null, "command": "-block:close" }
136+
{ "key": null, "command": "block:close" }
135137
]
136138
```
137139

138140
**Swap two keys:**
139141
```json
140142
[
141-
{ "key": "Cmd:d", "command": "block:splitDown" },
142-
{ "key": "Cmd:Shift:d", "command": "block:splitRight" }
143+
{ "key": "Cmd:d", "command": "block:splitdown" },
144+
{ "key": "Cmd:Shift:d", "command": "block:splitright" }
143145
]
144146
```
145147

@@ -151,24 +153,26 @@ Key combinations use colon-separated format:
151153
| `tab:close` | <Kbd k="Cmd:Shift:w"/> | Close the current tab |
152154
| `tab:prev` | <Kbd k="Cmd:["/> | Switch to previous tab |
153155
| `tab:next` | <Kbd k="Cmd:]"/> | Switch to next tab |
154-
| `tab:switchTo1``tab:switchTo9` | <Kbd k="Cmd:1"/>–<Kbd k="Cmd:9"/> | Switch to tab N |
156+
| `tab:switchto1``tab:switchto9` | <Kbd k="Cmd:1"/>–<Kbd k="Cmd:9"/> | Switch to tab N |
155157
| `block:new` | <Kbd k="Cmd:n"/> | Open a new block |
156158
| `block:close` | <Kbd k="Cmd:w"/> | Close the current block |
157-
| `block:splitRight` | <Kbd k="Cmd:d"/> | Split right |
158-
| `block:splitDown` | <Kbd k="Cmd:Shift:d"/> | Split down |
159+
| `block:splitright` | <Kbd k="Cmd:d"/> | Split right |
160+
| `block:splitdown` | <Kbd k="Cmd:Shift:d"/> | Split down |
159161
| `block:magnify` | <Kbd k="Cmd:m"/> | Magnify/unmagnify block |
160162
| `block:refocus` | <Kbd k="Cmd:i"/> | Refocus the current block |
161-
| `block:navUp/Down/Left/Right` | <Kbd k="Ctrl:Shift:Arrows"/> | Navigate between blocks |
162-
| `block:switchTo1``block:switchTo9` | <Kbd k="Ctrl:Shift:1"/>–<Kbd k="Ctrl:Shift:9"/> | Switch to block N |
163-
| `block:switchToAI` | <Kbd k="Ctrl:Shift:0"/> | Focus WaveAI input |
164-
| `block:replaceWithLauncher` | <Kbd k="Ctrl:Shift:x"/> | Replace block with launcher |
163+
| `block:navup/navdown/navleft/navright` | <Kbd k="Ctrl:Shift:Arrows"/> | Navigate between blocks |
164+
| `block:focusnext` | <Kbd k="Ctrl:Shift:]"/> | Cycle block focus forward (CW) |
165+
| `block:focusprev` | <Kbd k="Ctrl:Shift:["/> | Cycle block focus backward (CCW) |
166+
| `block:switchto1``block:switchto9` | <Kbd k="Ctrl:Shift:1"/>–<Kbd k="Ctrl:Shift:9"/> | Switch to block N |
167+
| `block:switchtoai` | <Kbd k="Ctrl:Shift:0"/> | Focus WaveAI input |
168+
| `block:replacewithlauncher` | <Kbd k="Ctrl:Shift:x"/> | Replace block with launcher |
165169
| `app:search` | <Kbd k="Cmd:f"/> | Find/search |
166-
| `app:openConnection` | <Kbd k="Cmd:g"/> | Open connection switcher |
167-
| `app:toggleAIPanel` | <Kbd k="Cmd:Shift:a"/> | Toggle WaveAI panel |
168-
| `app:toggleWidgetsSidebar` | <Kbd k="Cmd:b"/> | Toggle widgets sidebar |
169-
| `term:toggleMultiInput` | <Kbd k="Ctrl:Shift:i"/> | Toggle terminal multi-input |
170+
| `app:openconnection` | <Kbd k="Cmd:g"/> | Open connection switcher |
171+
| `app:toggleaipanel` | <Kbd k="Cmd:Shift:a"/> | Toggle WaveAI panel |
172+
| `app:togglewidgetssidebar` | <Kbd k="Cmd:b"/> | Toggle widgets sidebar |
173+
| `term:togglemultiinput` | <Kbd k="Ctrl:Shift:i"/> | Toggle terminal multi-input |
170174
| `generic:cancel` | <Kbd k="Escape"/> | Close modals/search |
171-
| `block:splitChord` | <Kbd k="Ctrl:Shift:s"/> | Initiate split chord |
175+
| `block:splitchord` | <Kbd k="Ctrl:Shift:s"/> | Initiate split chord |
172176

173177
Changes take effect immediately — no restart required.
174178

frontend/app/store/keymodel.ts

Lines changed: 59 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,24 @@ function switchBlockByBlockNum(index: number) {
260260
}, 10);
261261
}
262262

263+
function cycleBlockFocus(delta: 1 | -1) {
264+
const layoutModel = getLayoutModelForStaticTab();
265+
if (!layoutModel) {
266+
return;
267+
}
268+
const leafOrder = globalStore.get(layoutModel.leafOrder);
269+
if (leafOrder.length === 0) {
270+
return;
271+
}
272+
const focusedNodeId = layoutModel.focusedNodeId;
273+
const curIdx = leafOrder.findIndex((e) => e.nodeid === focusedNodeId);
274+
const nextIdx = (curIdx + delta + leafOrder.length) % leafOrder.length;
275+
layoutModel.focusNode(leafOrder[nextIdx].nodeid);
276+
setTimeout(() => {
277+
globalRefocus();
278+
}, 10);
279+
}
280+
263281
function switchBlockInDirection(direction: NavigateDirection) {
264282
const layoutModel = getLayoutModelForStaticTab();
265283
const focusType = FocusManager.getInstance().getFocusType();
@@ -591,15 +609,15 @@ const defaultActions: ActionDef[] = [
591609
},
592610
},
593611
{
594-
id: "block:splitRight",
612+
id: "block:splitright",
595613
defaultKeys: ["Cmd:d"],
596614
handler: () => {
597615
handleSplitHorizontal("after");
598616
return true;
599617
},
600618
},
601619
{
602-
id: "block:splitDown",
620+
id: "block:splitdown",
603621
defaultKeys: ["Shift:Cmd:d"],
604622
handler: () => {
605623
handleSplitVertical("after");
@@ -651,27 +669,43 @@ const defaultActions: ActionDef[] = [
651669
},
652670
},
653671
{
654-
id: "block:navUp",
672+
id: "block:navup",
655673
defaultKeys: ["Ctrl:Shift:ArrowUp", "Ctrl:Shift:k"],
656674
handler: makeBlockNavHandler(NavigateDirection.Up),
657675
},
658676
{
659-
id: "block:navDown",
677+
id: "block:navdown",
660678
defaultKeys: ["Ctrl:Shift:ArrowDown", "Ctrl:Shift:j"],
661679
handler: makeBlockNavHandler(NavigateDirection.Down),
662680
},
663681
{
664-
id: "block:navLeft",
682+
id: "block:navleft",
665683
defaultKeys: ["Ctrl:Shift:ArrowLeft", "Ctrl:Shift:h"],
666684
handler: makeBlockNavHandler(NavigateDirection.Left),
667685
},
668686
{
669-
id: "block:navRight",
687+
id: "block:navright",
670688
defaultKeys: ["Ctrl:Shift:ArrowRight", "Ctrl:Shift:l"],
671689
handler: makeBlockNavHandler(NavigateDirection.Right),
672690
},
673691
{
674-
id: "block:replaceWithLauncher",
692+
id: "block:focusnext",
693+
defaultKeys: ["Ctrl:Shift:]"],
694+
handler: () => {
695+
cycleBlockFocus(1);
696+
return true;
697+
},
698+
},
699+
{
700+
id: "block:focusprev",
701+
defaultKeys: ["Ctrl:Shift:["],
702+
handler: () => {
703+
cycleBlockFocus(-1);
704+
return true;
705+
},
706+
},
707+
{
708+
id: "block:replacewithlauncher",
675709
defaultKeys: ["Ctrl:Shift:x"],
676710
handler: () => {
677711
const blockId = getFocusedBlockId();
@@ -691,7 +725,7 @@ const defaultActions: ActionDef[] = [
691725
},
692726
},
693727
{
694-
id: "app:openConnection",
728+
id: "app:openconnection",
695729
defaultKeys: ["Cmd:g"],
696730
handler: () => {
697731
const bcm = getBlockComponentModel(getFocusedBlockInStaticTab());
@@ -704,7 +738,7 @@ const defaultActions: ActionDef[] = [
704738
},
705739
},
706740
{
707-
id: "term:toggleMultiInput",
741+
id: "term:togglemultiinput",
708742
defaultKeys: ["Ctrl:Shift:i"],
709743
handler: () => {
710744
const tabModel = getActiveTabModel();
@@ -740,7 +774,7 @@ const defaultActions: ActionDef[] = [
740774
},
741775
},
742776
{
743-
id: "app:toggleAIPanel",
777+
id: "app:toggleaipanel",
744778
defaultKeys: ["Cmd:Shift:a"],
745779
handler: () => {
746780
const currentVisible = WorkspaceLayoutModel.getInstance().getAIPanelVisible();
@@ -749,7 +783,7 @@ const defaultActions: ActionDef[] = [
749783
},
750784
},
751785
{
752-
id: "app:toggleWidgetsSidebar",
786+
id: "app:togglewidgetssidebar",
753787
defaultKeys: ["Cmd:b"],
754788
handler: () => {
755789
const current = WorkspaceLayoutModel.getInstance().getWidgetsSidebarVisible();
@@ -762,15 +796,15 @@ const defaultActions: ActionDef[] = [
762796
const idx = i + 1;
763797
return [
764798
{
765-
id: `tab:switchTo${idx}`,
799+
id: `tab:switchto${idx}`,
766800
defaultKeys: [`Cmd:${idx}`],
767801
handler: () => {
768802
switchTabAbs(idx);
769803
return true;
770804
},
771805
} as ActionDef,
772806
{
773-
id: `block:switchTo${idx}`,
807+
id: `block:switchto${idx}`,
774808
defaultKeys: [`Ctrl:Shift:c{Digit${idx}}`, `Ctrl:Shift:c{Numpad${idx}}`],
775809
handler: () => {
776810
switchBlockByBlockNum(idx);
@@ -781,7 +815,7 @@ const defaultActions: ActionDef[] = [
781815
}).flat(),
782816
// AI focus (block 0) — platform-dependent keys
783817
{
784-
id: "block:switchToAI",
818+
id: "block:switchtoai",
785819
defaultKeys: isWindows()
786820
? ["Alt:c{Digit0}", "Alt:c{Numpad0}"]
787821
: ["Ctrl:Shift:c{Digit0}", "Ctrl:Shift:c{Numpad0}"],
@@ -792,43 +826,43 @@ const defaultActions: ActionDef[] = [
792826
},
793827
// Chord initiator for block splitting
794828
{
795-
id: "block:splitChord",
829+
id: "block:splitchord",
796830
defaultKeys: ["Ctrl:Shift:s"],
797831
handler: () => true,
798832
},
799833
];
800834

801835
const defaultChordActions: ChordActionDef[] = [
802836
{
803-
id: "block:splitChordUp",
804-
parentId: "block:splitChord",
837+
id: "block:splitchordup",
838+
parentId: "block:splitchord",
805839
defaultKey: "ArrowUp",
806840
handler: () => {
807841
handleSplitVertical("before");
808842
return true;
809843
},
810844
},
811845
{
812-
id: "block:splitChordDown",
813-
parentId: "block:splitChord",
846+
id: "block:splitchorddown",
847+
parentId: "block:splitchord",
814848
defaultKey: "ArrowDown",
815849
handler: () => {
816850
handleSplitVertical("after");
817851
return true;
818852
},
819853
},
820854
{
821-
id: "block:splitChordLeft",
822-
parentId: "block:splitChord",
855+
id: "block:splitchordleft",
856+
parentId: "block:splitchord",
823857
defaultKey: "ArrowLeft",
824858
handler: () => {
825859
handleSplitHorizontal("before");
826860
return true;
827861
},
828862
},
829863
{
830-
id: "block:splitChordRight",
831-
parentId: "block:splitChord",
864+
id: "block:splitchordright",
865+
parentId: "block:splitchord",
832866
defaultKey: "ArrowRight",
833867
handler: () => {
834868
handleSplitHorizontal("after");
@@ -852,11 +886,11 @@ function buildKeyMaps(userOverrides: KeybindingEntry[]): void {
852886
}
853887

854888
// 2. Build chord bindings from defaults
855-
const chordInitiatorAction = defaultActions.find((a) => a.id === "block:splitChord");
889+
const chordInitiatorAction = defaultActions.find((a) => a.id === "block:splitchord");
856890
if (chordInitiatorAction) {
857891
const subKeys: KeyMapEntry[] = [];
858892
for (const chordDef of defaultChordActions) {
859-
if (chordDef.parentId === "block:splitChord") {
893+
if (chordDef.parentId === "block:splitchord") {
860894
actionHandlers.set(chordDef.id, chordDef.handler);
861895
subKeys.push({ key: chordDef.defaultKey, handler: chordDef.handler });
862896
}

schema/keybindings.json

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,34 +14,22 @@
1414
},
1515
"command": {
1616
"type": "string",
17-
"description": "Action ID. Prefix with - to unbind a default keybinding.",
17+
"description": "Action ID to bind.",
1818
"enum": [
1919
"tab:new", "tab:close", "tab:prev", "tab:next",
20-
"tab:switchTo1", "tab:switchTo2", "tab:switchTo3", "tab:switchTo4", "tab:switchTo5",
21-
"tab:switchTo6", "tab:switchTo7", "tab:switchTo8", "tab:switchTo9",
22-
"block:new", "block:close", "block:splitRight", "block:splitDown",
20+
"tab:switchto1", "tab:switchto2", "tab:switchto3", "tab:switchto4", "tab:switchto5",
21+
"tab:switchto6", "tab:switchto7", "tab:switchto8", "tab:switchto9",
22+
"block:new", "block:close", "block:splitright", "block:splitdown",
2323
"block:magnify", "block:refocus",
24-
"block:navUp", "block:navDown", "block:navLeft", "block:navRight",
25-
"block:switchTo1", "block:switchTo2", "block:switchTo3", "block:switchTo4", "block:switchTo5",
26-
"block:switchTo6", "block:switchTo7", "block:switchTo8", "block:switchTo9", "block:switchToAI",
27-
"block:replaceWithLauncher",
28-
"block:splitChord", "block:splitChordUp", "block:splitChordDown", "block:splitChordLeft", "block:splitChordRight",
29-
"app:search", "app:openConnection", "app:toggleAIPanel", "app:toggleWidgetsSidebar",
30-
"term:toggleMultiInput",
31-
"generic:cancel",
32-
"-tab:new", "-tab:close", "-tab:prev", "-tab:next",
33-
"-tab:switchTo1", "-tab:switchTo2", "-tab:switchTo3", "-tab:switchTo4", "-tab:switchTo5",
34-
"-tab:switchTo6", "-tab:switchTo7", "-tab:switchTo8", "-tab:switchTo9",
35-
"-block:new", "-block:close", "-block:splitRight", "-block:splitDown",
36-
"-block:magnify", "-block:refocus",
37-
"-block:navUp", "-block:navDown", "-block:navLeft", "-block:navRight",
38-
"-block:switchTo1", "-block:switchTo2", "-block:switchTo3", "-block:switchTo4", "-block:switchTo5",
39-
"-block:switchTo6", "-block:switchTo7", "-block:switchTo8", "-block:switchTo9", "-block:switchToAI",
40-
"-block:replaceWithLauncher",
41-
"-block:splitChord", "-block:splitChordUp", "-block:splitChordDown", "-block:splitChordLeft", "-block:splitChordRight",
42-
"-app:search", "-app:openConnection", "-app:toggleAIPanel", "-app:toggleWidgetsSidebar",
43-
"-term:toggleMultiInput",
44-
"-generic:cancel"
24+
"block:navup", "block:navdown", "block:navleft", "block:navright",
25+
"block:focusnext", "block:focusprev",
26+
"block:switchto1", "block:switchto2", "block:switchto3", "block:switchto4", "block:switchto5",
27+
"block:switchto6", "block:switchto7", "block:switchto8", "block:switchto9", "block:switchtoai",
28+
"block:replacewithlauncher",
29+
"block:splitchord", "block:splitchordup", "block:splitchorddown", "block:splitchordleft", "block:splitchordright",
30+
"app:search", "app:openconnection", "app:toggleaipanel", "app:togglewidgetssidebar",
31+
"term:togglemultiinput",
32+
"generic:cancel"
4533
]
4634
}
4735
},

0 commit comments

Comments
 (0)