Skip to content

Commit bf532df

Browse files
committed
feat(cli): add dedicated KeybindsView for /keybind command
Replace the inline subcommand-only /keybind handler with a dedicated view component (following the /usage pattern from feat-usage-command): - KeybindsView renders as a replacement view with border, listing all configured keybinds - Esc returns to chat (useInput hook, matching UsageView pattern) - /keybind without args opens the view; with args still processes add/remove subcommands inline - View type extended with "keybinds" option
1 parent e0cc9e2 commit bf532df

3 files changed

Lines changed: 105 additions & 3 deletions

File tree

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { buildLoadingText } from "../core/loading-text";
1212
import { findExpandedThinkingId } from "../core/thinking-state";
1313
import { WelcomeScreen } from "./WelcomeScreen";
1414
import { AskUserQuestionPrompt } from "./AskUserQuestionPrompt";
15+
import { KeybindsView } from "./KeybindsView";
1516
import { McpStatusList } from "./McpStatusList";
1617
import { ProcessStdoutView } from "./ProcessStdoutView";
1718
import {
@@ -50,7 +51,7 @@ import { SessionManager } from "@vegamo/deepcode-core";
5051
import { getCompactPromptTokenThreshold } from "@vegamo/deepcode-core";
5152
import { writeStdout, writeStdoutLine } from "../../utils/stdio-helpers";
5253

53-
type View = "chat" | "session-list" | "undo" | "mcp-status";
54+
type View = "chat" | "session-list" | "undo" | "keybinds" | "mcp-status";
5455

5556
const STATUS_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
5657

@@ -361,6 +362,10 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp
361362
navigateToSubView("mcp-status");
362363
return;
363364
}
365+
if (submission.command === "keybind") {
366+
navigateToSubView("keybinds");
367+
return;
368+
}
364369

365370
const prompt: UserPromptContent = {
366371
text: submission.text,
@@ -945,6 +950,16 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp
945950
setView("chat");
946951
}}
947952
/>
953+
) : view === "keybinds" ? (
954+
<KeybindsView
955+
keybinds={resolvedSettings.keybinds}
956+
projectRoot={projectRoot}
957+
onCancel={() => setView("chat")}
958+
onChange={() => {
959+
const next = resolveCurrentSettings(projectRoot);
960+
setResolvedSettings(next);
961+
}}
962+
/>
948963
) : view === "mcp-status" ? (
949964
<McpStatusList
950965
statuses={mcpStatuses}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import React, { useState } from "react";
2+
import { Box, Text, useInput } from "ink";
3+
import type { KeybindMap } from "@vegamo/deepcode-core";
4+
import { readSettings, writeSettings, readProjectSettings, writeProjectSettings } from "@vegamo/deepcode-core";
5+
6+
type Props = {
7+
keybinds: KeybindMap;
8+
projectRoot: string;
9+
onCancel: () => void;
10+
onChange: () => void;
11+
};
12+
13+
export function KeybindsView({ keybinds, projectRoot, onCancel, onChange }: Props): React.ReactElement {
14+
const [status, setStatus] = useState<string | null>(null);
15+
const entries = Object.entries(keybinds);
16+
17+
useInput((_input, key) => {
18+
if (key.escape) {
19+
onCancel();
20+
}
21+
});
22+
23+
function writeKeybinds(next: KeybindMap): void {
24+
const existingProjectSettings = readProjectSettings(projectRoot);
25+
const useProject = existingProjectSettings !== null;
26+
const rawSettings = useProject ? existingProjectSettings : readSettings();
27+
const updated = { ...(rawSettings ?? {}), keybinds: next };
28+
if (useProject) {
29+
writeProjectSettings(updated, projectRoot);
30+
} else {
31+
writeSettings(updated);
32+
}
33+
onChange();
34+
}
35+
36+
function handleRemove(shortcut: string): void {
37+
const next = { ...keybinds };
38+
delete next[shortcut];
39+
writeKeybinds(next);
40+
setStatus(`Removed "${shortcut}".`);
41+
}
42+
43+
return (
44+
<Box flexDirection="column" marginLeft={1} paddingX={1} gap={1} borderStyle="round" borderDimColor>
45+
<Box>
46+
<Text color="#229ac3" bold>
47+
/keybind
48+
</Text>
49+
</Box>
50+
51+
{status ? (
52+
<Box>
53+
<Text color="yellow">{status}</Text>
54+
</Box>
55+
) : null}
56+
57+
{entries.length === 0 ? (
58+
<Box>
59+
<Text dimColor>(no keybinds configured)</Text>
60+
</Box>
61+
) : (
62+
<Box flexDirection="column">
63+
{entries.map(([shortcut, action]) => (
64+
<Box key={shortcut} gap={1}>
65+
<Text>{shortcut}</Text>
66+
<Text dimColor>→ /{action}</Text>
67+
<Text dimColor>{" "}Del to remove</Text>
68+
</Box>
69+
))}
70+
</Box>
71+
)}
72+
73+
<Box flexDirection="column">
74+
<Text dimColor>Add via settings.json:</Text>
75+
<Text dimColor>{` "keybinds": { "ctrl+e": "exit" }`}</Text>
76+
</Box>
77+
78+
<Text dimColor>Esc to close</Text>
79+
</Box>
80+
);
81+
}

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export type PromptSubmission = {
7474
selectedSkills?: SkillInfo[];
7575
permissions?: UserToolPermission[];
7676
alwaysAllows?: PermissionScope[];
77-
command?: "new" | "resume" | "continue" | "undo" | "mcp" | "exit";
77+
command?: "new" | "resume" | "continue" | "undo" | "mcp" | "keybind" | "exit";
7878
};
7979

8080
export type PromptDraft = {
@@ -738,7 +738,13 @@ export const PromptInput = React.memo(function PromptInput({
738738
return;
739739
}
740740
if (item.kind === "keybind") {
741-
handleKeybindCommand();
741+
const parts = buffer.text.trim().split(/\s+/);
742+
if (parts.length > 1) {
743+
handleKeybindCommand();
744+
} else {
745+
onSubmit({ text: "/keybind", imageUrls: [], command: "keybind" });
746+
resetPromptInput();
747+
}
742748
return;
743749
}
744750
if (item.kind === "exit") {

0 commit comments

Comments
 (0)