Skip to content

Commit 241a1bc

Browse files
committed
feat(cli): add interactive keybind menu for /keybind without args
When /keybind is selected from the slash menu (no extra args), show an inline menu listing all configured keybinds with hints for add/remove subcommands. Matches the pattern used by /skills and /model dropdowns. Typing /keybind add/remove with explicit args still processes the subcommand directly.
1 parent e0cc9e2 commit 241a1bc

1 file changed

Lines changed: 68 additions & 7 deletions

File tree

packages/cli/src/ui/views/PromptInput.tsx

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ export const PromptInput = React.memo(function PromptInput({
153153
const [showSkillsDropdown, setShowSkillsDropdown] = useState(false);
154154
const [openRawModelDropdown, setOpenRawModelDropdown] = useState(false);
155155
const [showModelDropdown, setShowModelDropdown] = useState(false);
156+
const [showKeybindMenu, setShowKeybindMenu] = useState(false);
156157
const [fileMentionItems, setFileMentionItems] = useState<FileMentionItem[]>(() => scanFileMentionItems(projectRoot));
157158
const [dismissedFileMentionKey, setDismissedFileMentionKey] = useState<string | null>(null);
158159
const [hasTerminalFocus, setHasTerminalFocus] = useState(true);
@@ -182,6 +183,7 @@ export const PromptInput = React.memo(function PromptInput({
182183
!showSkillsDropdown &&
183184
!showModelDropdown &&
184185
!openRawModelDropdown &&
186+
!showKeybindMenu &&
185187
fileMentionToken !== null &&
186188
fileMentionKey !== dismissedFileMentionKey;
187189
const slashItems = React.useMemo(() => buildSlashCommands(skills), [skills]);
@@ -192,12 +194,20 @@ export const PromptInput = React.memo(function PromptInput({
192194
const slashToken = getCurrentSlashToken(buffer);
193195
const slashMenu = React.useMemo(
194196
() =>
195-
showSkillsDropdown || showModelDropdown || openRawModelDropdown || showFileMentionMenu
197+
showSkillsDropdown || showModelDropdown || openRawModelDropdown || showFileMentionMenu || showKeybindMenu
196198
? []
197199
: slashToken
198200
? filterSlashCommands(slashItems, slashToken)
199201
: [],
200-
[showSkillsDropdown, showModelDropdown, openRawModelDropdown, showFileMentionMenu, slashToken, slashItems]
202+
[
203+
showSkillsDropdown,
204+
showModelDropdown,
205+
openRawModelDropdown,
206+
showFileMentionMenu,
207+
showKeybindMenu,
208+
slashToken,
209+
slashItems,
210+
]
201211
);
202212
const showMenu = slashMenu.length > 0;
203213
const promptHistoryKey = React.useMemo(() => promptHistory.join("\0"), [promptHistory]);
@@ -219,8 +229,14 @@ export const PromptInput = React.memo(function PromptInput({
219229
? busyStatusText
220230
: `enter send · shift+enter newline · @ files · ctrl+v image · / commands · ctrl+d exit${processOrPasteHint}`;
221231
const showFooterText = useMemo(
222-
() => showMenu || showSkillsDropdown || openRawModelDropdown || showModelDropdown || showFileMentionMenu,
223-
[showMenu, showSkillsDropdown, showModelDropdown, openRawModelDropdown, showFileMentionMenu]
232+
() =>
233+
showMenu ||
234+
showSkillsDropdown ||
235+
openRawModelDropdown ||
236+
showModelDropdown ||
237+
showFileMentionMenu ||
238+
showKeybindMenu,
239+
[showMenu, showSkillsDropdown, showModelDropdown, openRawModelDropdown, showFileMentionMenu, showKeybindMenu]
224240
);
225241
const inputContentWidth = Math.max(1, screenWidth - PROMPT_PREFIX_WIDTH);
226242

@@ -333,7 +349,14 @@ export const PromptInput = React.memo(function PromptInput({
333349

334350
// Check custom keybinds before hardcoded shortcuts.
335351
// Only match when no dropdown/menu is open to avoid conflicts.
336-
if (!openRawModelDropdown && !showSkillsDropdown && !showModelDropdown && !showMenu && !showFileMentionMenu) {
352+
if (
353+
!openRawModelDropdown &&
354+
!showSkillsDropdown &&
355+
!showModelDropdown &&
356+
!showMenu &&
357+
!showFileMentionMenu &&
358+
!showKeybindMenu
359+
) {
337360
for (const { match, action } of keybindMatchers) {
338361
if (match(input, key)) {
339362
const item = slashItems.find((s) => s.name === action);
@@ -352,6 +375,10 @@ export const PromptInput = React.memo(function PromptInput({
352375
if (showFileMentionMenu) {
353376
return;
354377
}
378+
if (showKeybindMenu) {
379+
setShowKeybindMenu(false);
380+
return;
381+
}
355382
if (busy) {
356383
onInterrupt();
357384
setStatusMessage("Interrupting…");
@@ -409,7 +436,7 @@ export const PromptInput = React.memo(function PromptInput({
409436
setPendingExit(false);
410437
}
411438

412-
if (openRawModelDropdown || showSkillsDropdown || showModelDropdown) {
439+
if (openRawModelDropdown || showSkillsDropdown || showModelDropdown || showKeybindMenu) {
413440
return;
414441
}
415442

@@ -675,6 +702,7 @@ export const PromptInput = React.memo(function PromptInput({
675702
setImageUrls([]);
676703
setSelectedSkills([]);
677704
setShowSkillsDropdown(false);
705+
setShowKeybindMenu(false);
678706
exitHistoryBrowsing();
679707
resetPastes();
680708
}
@@ -738,7 +766,15 @@ export const PromptInput = React.memo(function PromptInput({
738766
return;
739767
}
740768
if (item.kind === "keybind") {
741-
handleKeybindCommand();
769+
const parts = buffer.text.trim().split(/\s+/);
770+
// If the user typed extra args (add/remove), process as subcommand.
771+
// Otherwise open the interactive menu.
772+
if (parts.length > 1) {
773+
handleKeybindCommand();
774+
} else {
775+
clearSlashToken();
776+
setShowKeybindMenu(true);
777+
}
742778
return;
743779
}
744780
if (item.kind === "exit") {
@@ -946,6 +982,31 @@ export const PromptInput = React.memo(function PromptInput({
946982
onModelConfigChange={onModelConfigChange}
947983
onStatusMessage={setStatusMessage}
948984
/>
985+
{showKeybindMenu ? (
986+
<Box flexDirection="column" marginBottom={1} width={screenWidth}>
987+
<Box marginLeft={2}>
988+
<Text bold>Keybinds</Text>
989+
</Box>
990+
{(keybinds && Object.keys(keybinds).length > 0
991+
? Object.entries(keybinds).map(([shortcut, action]) => (
992+
<Box key={shortcut} marginLeft={4}>
993+
<Text>{shortcut}</Text>
994+
<Text dimColor> → /{action}</Text>
995+
</Box>
996+
))
997+
: null) || (
998+
<Box marginLeft={4}>
999+
<Text dimColor>(no keybinds configured)</Text>
1000+
</Box>
1001+
)}
1002+
<Box marginLeft={2} marginTop={1}>
1003+
<Text dimColor>/keybind add &lt;shortcut&gt; &lt;action&gt; to add</Text>
1004+
</Box>
1005+
<Box marginLeft={2}>
1006+
<Text dimColor>/keybind remove &lt;shortcut&gt; to remove</Text>
1007+
</Box>
1008+
</Box>
1009+
) : null}
9491010
<FileMentionMenu
9501011
open={showFileMentionMenu}
9511012
width={screenWidth}

0 commit comments

Comments
 (0)