From 3614dbc22ddc9540cb045b30bf83dc4189f65942 Mon Sep 17 00:00:00 2001 From: al4xdev Date: Tue, 30 Jun 2026 21:10:27 -0300 Subject: [PATCH 1/9] feat(core): add KeybindMap type and keybinds to settings resolution - Add KeybindMap type (Record) for configurable keybinds - Add keybinds field to DeepcodingSettings and ResolvedDeepcodingSettings - Add normalizeKeybinds() to validate keybind entries - Add mergeKeybinds() with project-over-user precedence - Export KeybindMap from core public API --- packages/core/src/index.ts | 1 + packages/core/src/settings.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 832d2444..8be5bcb3 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -29,6 +29,7 @@ export type { StatusLineSettings, ResolvedStatusLineSettings, StatusLineProviderConfig, + KeybindMap, } from "./settings"; // Session diff --git a/packages/core/src/settings.ts b/packages/core/src/settings.ts index 5dab3b5a..7d7d7086 100644 --- a/packages/core/src/settings.ts +++ b/packages/core/src/settings.ts @@ -45,6 +45,8 @@ export type PermissionSettings = { export type EnabledSkillsSettings = Record; +export type KeybindMap = Record; + export type StatusLineProviderConfig = | { type: "command"; @@ -94,6 +96,7 @@ export type DeepcodingSettings = { permissions?: PermissionSettings; enabledSkills?: EnabledSkillsSettings; statusline?: StatusLineSettings; + keybinds?: KeybindMap; }; export type ResolvedDeepcodingSettings = { @@ -112,6 +115,7 @@ export type ResolvedDeepcodingSettings = { permissions: Required; enabledSkills: EnabledSkillsSettings; statusline: ResolvedStatusLineSettings; + keybinds: KeybindMap; }; export type ModelConfigSelection = { @@ -253,6 +257,30 @@ function mergeEnabledSkills( }; } +function normalizeKeybinds(value: unknown): KeybindMap { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return {}; + } + const result: KeybindMap = {}; + for (const [shortcut, action] of Object.entries(value)) { + if (!shortcut || typeof action !== "string" || !action) { + continue; + } + result[shortcut] = action; + } + return result; +} + +function mergeKeybinds( + userSettings: DeepcodingSettings | null | undefined, + projectSettings: DeepcodingSettings | null | undefined +): KeybindMap { + return { + ...normalizeKeybinds(userSettings?.keybinds), + ...normalizeKeybinds(projectSettings?.keybinds), + }; +} + const DEFAULT_STATUSLINE_REFRESH_MS = 2000; const MIN_STATUSLINE_REFRESH_MS = 500; const DEFAULT_STATUSLINE_SEPARATOR = " · "; @@ -547,6 +575,7 @@ export function resolveSettingsSources( permissions: mergePermissions(userSettings, projectSettings), enabledSkills: mergeEnabledSkills(userSettings, projectSettings), statusline: mergeStatusLine(userSettings, projectSettings), + keybinds: mergeKeybinds(userSettings, projectSettings), }; } From 40b96acc552cb748c661fda07dab92d821db67e5 Mon Sep 17 00:00:00 2001 From: al4xdev Date: Tue, 30 Jun 2026 21:10:34 -0300 Subject: [PATCH 2/9] feat(cli): add keybind parser module - Add matchKeybind() to match shortcut strings (e.g. "ctrl+shift+g") against raw input and InputKey with exact modifier matching - Add buildKeybindMatchers() to pre-compile a KeybindMap into an array of matchers for fast per-keystroke lookup --- packages/cli/src/ui/core/keybinds.ts | 42 ++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 packages/cli/src/ui/core/keybinds.ts diff --git a/packages/cli/src/ui/core/keybinds.ts b/packages/cli/src/ui/core/keybinds.ts new file mode 100644 index 00000000..7a29c5f9 --- /dev/null +++ b/packages/cli/src/ui/core/keybinds.ts @@ -0,0 +1,42 @@ +import type { InputKey } from "../hooks"; +import type { KeybindMap } from "@vegamo/deepcode-core"; + +/** + * Match a keybind shortcut string (e.g. "ctrl+shift+g") against + * the raw input character and parsed InputKey. + * + * Supported modifiers: ctrl, shift, meta. + * The key part is case-insensitive. + */ +export function matchKeybind(shortcut: string, input: string, key: InputKey): boolean { + const parts = shortcut.toLowerCase().split("+"); + const keyChar = parts.pop()!; + + // Must have at least one modifier + a key + if (parts.length === 0 || !keyChar) return false; + + const modifiers = new Set(parts); + + // Each modifier in the shortcut must be present in the key event. + // Modifiers NOT in the shortcut must NOT be present (exact match). + if (modifiers.has("ctrl") !== key.ctrl) return false; + if (modifiers.has("shift") !== key.shift) return false; + if (modifiers.has("meta") !== key.meta) return false; + + return input.toLowerCase() === keyChar; +} + +export type KeybindMatcher = { + match: (input: string, key: InputKey) => boolean; + action: string; +}; + +/** + * Pre-compile a KeybindMap into an array of matchers for fast per-keystroke lookup. + */ +export function buildKeybindMatchers(keybinds: KeybindMap): KeybindMatcher[] { + return Object.entries(keybinds).map(([shortcut, action]) => ({ + match: (input: string, key: InputKey) => matchKeybind(shortcut, input, key), + action, + })); +} From b0c662becc8f8f102cd999fd143322d7b56ffdc7 Mon Sep 17 00:00:00 2001 From: al4xdev Date: Tue, 30 Jun 2026 21:10:41 -0300 Subject: [PATCH 3/9] feat(cli): add /keybind command and integrate keybinds into input handling - Add "keybind" to SlashCommandKind and BUILTIN_SLASH_COMMANDS with args hint (add , remove , list) - Implement /keybind handler with add/remove/list subcommands that read/write settings.json (project-level if present, user-level fallback) - Integrate keybind checking in useTerminalInput callback: custom keybinds are matched before hardcoded shortcuts, only when no dropdown/menu is open to avoid conflicts - Resolve keybind action to SlashCommandItem and dispatch via handleSlashSelection (supports all built-in commands and skills) - Pass resolved settings keybinds from App to PromptInput - Export matchKeybind, buildKeybindMatchers, and KeybindMatcher from the UI barrel --- packages/cli/src/ui/core/slash-commands.ts | 8 ++ packages/cli/src/ui/index.ts | 1 + packages/cli/src/ui/views/App.tsx | 1 + packages/cli/src/ui/views/PromptInput.tsx | 117 ++++++++++++++++++++- 4 files changed, 126 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/ui/core/slash-commands.ts b/packages/cli/src/ui/core/slash-commands.ts index ba5ae6ec..94b75b30 100644 --- a/packages/cli/src/ui/core/slash-commands.ts +++ b/packages/cli/src/ui/core/slash-commands.ts @@ -11,6 +11,7 @@ export type SlashCommandKind = | "undo" | "mcp" | "raw" + | "keybind" | "exit"; export type SlashCommandItem = { @@ -78,6 +79,13 @@ export const BUILTIN_SLASH_COMMANDS: SlashCommandItem[] = [ args: ["lite", "normal", "raw-scrollback"], description: "Toggle display mode for viewing or collapsing reasoning content", }, + { + kind: "keybind", + name: "keybind", + label: "/keybind", + args: ["add ", "remove ", "list"], + description: "Add, remove, or list custom keybinds", + }, { kind: "exit", name: "exit", diff --git a/packages/cli/src/ui/index.ts b/packages/cli/src/ui/index.ts index 65415464..6169d345 100644 --- a/packages/cli/src/ui/index.ts +++ b/packages/cli/src/ui/index.ts @@ -80,6 +80,7 @@ export { type SlashCommandKind, type SlashCommandItem, } from "./core/slash-commands"; +export { matchKeybind, buildKeybindMatchers, type KeybindMatcher } from "./core/keybinds"; export { filterFileMentionItems, formatFileMentionPath, diff --git a/packages/cli/src/ui/views/App.tsx b/packages/cli/src/ui/views/App.tsx index 3b2886cd..fa17b62a 100644 --- a/packages/cli/src/ui/views/App.tsx +++ b/packages/cli/src/ui/views/App.tsx @@ -983,6 +983,7 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp onInterrupt={handleInterrupt} onToggleProcessStdout={handleToggleProcessStdout} onExitShortcut={handleExitShortcut} + keybinds={resolvedSettings.keybinds} placeholder="Type your message..." statusLineSegments={statusLineSegments} statusLineSeparator={resolvedSettings.statusline.separator} diff --git a/packages/cli/src/ui/views/PromptInput.tsx b/packages/cli/src/ui/views/PromptInput.tsx index 3f548def..91be0d01 100644 --- a/packages/cli/src/ui/views/PromptInput.tsx +++ b/packages/cli/src/ui/views/PromptInput.tsx @@ -36,6 +36,7 @@ import { } from "../core/prompt-undo-redo"; import { buildSlashCommands, filterSlashCommands, findExactSlashCommand } from "../core/slash-commands"; import type { SlashCommandItem } from "../core/slash-commands"; +import { buildKeybindMatchers } from "../core/keybinds"; import { filterFileMentionItems, getCurrentFileMentionToken, @@ -60,7 +61,8 @@ import { useTerminalFocusReporting, } from "../hooks"; import SlashCommandMenu, { isSkillSelected } from "./SlashCommandMenu"; -import type { ModelConfigSelection, PermissionScope } from "@vegamo/deepcode-core"; +import type { ModelConfigSelection, PermissionScope, KeybindMap } from "@vegamo/deepcode-core"; +import { readSettings, writeSettings, readProjectSettings, writeProjectSettings } from "@vegamo/deepcode-core"; import { FileMentionMenu, ModelsDropdown, RawModelDropdown, SkillsDropdown } from "../components"; import type { SessionEntry, SkillInfo } from "@vegamo/deepcode-core"; import type { UserToolPermission } from "@vegamo/deepcode-core"; @@ -102,6 +104,7 @@ type Props = { onInterrupt: () => void; onToggleProcessStdout?: () => void; onExitShortcut?: () => void; + keybinds?: KeybindMap; }; const PROMPT_PREFIX_WIDTH = 2; @@ -135,6 +138,7 @@ export const PromptInput = React.memo(function PromptInput({ onToggleProcessStdout, onExitShortcut, onRawModeChange, + keybinds, }: Props): React.ReactElement { const { stdout } = useStdout(); const inputTextRef = useRef(null); @@ -179,6 +183,10 @@ export const PromptInput = React.memo(function PromptInput({ fileMentionToken !== null && fileMentionKey !== dismissedFileMentionKey; const slashItems = React.useMemo(() => buildSlashCommands(skills), [skills]); + const keybindMatchers = React.useMemo( + () => (keybinds && Object.keys(keybinds).length > 0 ? buildKeybindMatchers(keybinds) : []), + [keybinds] + ); const slashToken = getCurrentSlashToken(buffer); const slashMenu = React.useMemo( () => @@ -321,6 +329,20 @@ export const PromptInput = React.memo(function PromptInput({ return; } + // Check custom keybinds before hardcoded shortcuts. + // Only match when no dropdown/menu is open to avoid conflicts. + if (!openRawModelDropdown && !showSkillsDropdown && !showModelDropdown && !showMenu && !showFileMentionMenu) { + for (const { match, action } of keybindMatchers) { + if (match(input, key)) { + const item = slashItems.find((s) => s.name === action); + if (item) { + handleSlashSelection(item); + return; + } + } + } + } + if (key.escape) { if (openRawModelDropdown) { return; @@ -713,6 +735,10 @@ export const PromptInput = React.memo(function PromptInput({ resetPromptInput(); return; } + if (item.kind === "keybind") { + handleKeybindCommand(); + return; + } if (item.kind === "exit") { onSubmit({ text: "/exit", imageUrls: [], command: "exit" }); setBuffer(EMPTY_BUFFER); @@ -721,6 +747,95 @@ export const PromptInput = React.memo(function PromptInput({ } } + function handleKeybindCommand(): void { + const parts = buffer.text.trim().split(/\s+/); + const subcommand = parts[1] ?? "list"; + const existingProjectSettings = readProjectSettings(projectRoot); + + if (subcommand === "list") { + clearSlashToken(); + const kbs = keybinds ?? {}; + const entries = Object.entries(kbs); + if (entries.length === 0) { + setStatusMessage("No custom keybinds configured. Use /keybind add "); + } else { + const lines = entries.map(([shortcut, action]) => ` ${shortcut} → /${action}`); + setStatusMessage(`Keybinds:\n${lines.join("\n")}`); + } + return; + } + + if (subcommand === "add") { + const shortcut = parts[2]; + const action = parts[3]; + if (!shortcut || !action) { + setStatusMessage("Usage: /keybind add (e.g. /keybind add ctrl+e exit)"); + clearSlashToken(); + return; + } + // Validate the shortcut string + if (!/^(ctrl|shift|meta)(\+(ctrl|shift|meta))*\+[a-z0-9-]$/i.test(shortcut)) { + setStatusMessage( + `Invalid shortcut "${shortcut}". Format: ctrl+key, ctrl+shift+key (modifiers: ctrl, shift, meta)` + ); + clearSlashToken(); + return; + } + // Validate action is a known slash command or skill + const knownNames = new Set(slashItems.map((s) => s.name)); + if (!knownNames.has(action)) { + setStatusMessage(`Unknown action "/${action}". Use a slash command or skill name.`); + clearSlashToken(); + return; + } + + const useProject = existingProjectSettings !== null; + const rawSettings = useProject ? existingProjectSettings : readSettings(); + const current: Record = { ...(rawSettings?.keybinds ?? {}) }; + current[shortcut] = action; + const updated = { ...(rawSettings ?? {}), keybinds: current }; + if (useProject) { + writeProjectSettings(updated, projectRoot); + } else { + writeSettings(updated); + } + setStatusMessage(`Keybind added: ${shortcut} → /${action}`); + clearSlashToken(); + return; + } + + if (subcommand === "remove") { + const shortcut = parts[2]; + if (!shortcut) { + setStatusMessage("Usage: /keybind remove "); + clearSlashToken(); + return; + } + + const useProject = existingProjectSettings !== null; + const rawSettings = useProject ? existingProjectSettings : readSettings(); + const current: Record = { ...(rawSettings?.keybinds ?? {}) }; + if (!(shortcut in current)) { + setStatusMessage(`Keybind "${shortcut}" not found.`); + clearSlashToken(); + return; + } + delete current[shortcut]; + const updated = { ...(rawSettings ?? {}), keybinds: current }; + if (useProject) { + writeProjectSettings(updated, projectRoot); + } else { + writeSettings(updated); + } + setStatusMessage(`Keybind removed: ${shortcut}`); + clearSlashToken(); + return; + } + + setStatusMessage(`Unknown subcommand "${subcommand}". Use: add, remove, list`); + clearSlashToken(); + } + function submitCurrentBuffer(): void { if (busy) { setStatusMessage("wait for the current response or press esc to interrupt"); From 9e878add7f09cca3bd02912e773529bba100d49a Mon Sep 17 00:00:00 2001 From: al4xdev Date: Tue, 30 Jun 2026 21:10:48 -0300 Subject: [PATCH 4/9] test: add keybind tests and update existing test suites - Add 19 unit tests for matchKeybind covering ctrl, shift, meta modifiers, exact matching, case insensitivity, digit/non-alpha keys, and edge cases - Add 4 integration tests for keybinds settings resolution: default empty, user-only, project-over-user merge, and invalid entry filtering - Update slash-commands test to include "keybind" in built-in names --- packages/cli/src/tests/keybinds.test.ts | 121 ++++++++++++++++++ packages/cli/src/tests/slash-commands.test.ts | 1 + .../src/tests/settings-and-notify.test.ts | 54 ++++++++ 3 files changed, 176 insertions(+) create mode 100644 packages/cli/src/tests/keybinds.test.ts diff --git a/packages/cli/src/tests/keybinds.test.ts b/packages/cli/src/tests/keybinds.test.ts new file mode 100644 index 00000000..ff691d5d --- /dev/null +++ b/packages/cli/src/tests/keybinds.test.ts @@ -0,0 +1,121 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { matchKeybind, buildKeybindMatchers } from "../ui"; +import type { InputKey } from "../ui/hooks/useTerminalInput"; + +function key(overrides: Partial = {}): InputKey { + return { + upArrow: false, + downArrow: false, + leftArrow: false, + rightArrow: false, + home: false, + end: false, + pageDown: false, + pageUp: false, + return: false, + escape: false, + ctrl: false, + shift: false, + tab: false, + backspace: false, + delete: false, + meta: false, + focusIn: false, + focusOut: false, + paste: false, + ...overrides, + }; +} + +// --------------- matchKeybind --------------- + +test("matchKeybind: ctrl+e matches with ctrl modifier", () => { + assert.equal(matchKeybind("ctrl+e", "e", key({ ctrl: true })), true); + assert.equal(matchKeybind("ctrl+e", "E", key({ ctrl: true })), true); +}); + +test("matchKeybind: ctrl+e does not match with extra shift", () => { + assert.equal(matchKeybind("ctrl+e", "E", key({ ctrl: true, shift: true })), false); +}); + +test("matchKeybind: ctrl+e does not match without ctrl", () => { + assert.equal(matchKeybind("ctrl+e", "e", key()), false); +}); + +test("matchKeybind: ctrl+e does not match wrong key", () => { + assert.equal(matchKeybind("ctrl+e", "f", key({ ctrl: true })), false); +}); + +test("matchKeybind: ctrl+shift+g matches with both modifiers", () => { + assert.equal(matchKeybind("ctrl+shift+g", "g", key({ ctrl: true, shift: true })), true); + assert.equal(matchKeybind("ctrl+shift+g", "G", key({ ctrl: true, shift: true })), true); +}); + +test("matchKeybind: ctrl+shift+g does not match with only ctrl", () => { + assert.equal(matchKeybind("ctrl+shift+g", "g", key({ ctrl: true })), false); +}); + +test("matchKeybind: ctrl+shift+g does not match extra meta", () => { + assert.equal(matchKeybind("ctrl+shift+g", "g", key({ ctrl: true, shift: true, meta: true })), false); +}); + +test("matchKeybind: meta+b matches with meta modifier", () => { + assert.equal(matchKeybind("meta+b", "b", key({ meta: true })), true); + assert.equal(matchKeybind("meta+b", "B", key({ meta: true })), true); +}); + +test("matchKeybind: meta+b does not match with extra shift", () => { + assert.equal(matchKeybind("meta+b", "B", key({ meta: true, shift: true })), false); +}); + +test("matchKeybind: meta+b does not match without meta", () => { + assert.equal(matchKeybind("meta+b", "b", key()), false); +}); + +test("matchKeybind: handles non-alpha keys like minus", () => { + assert.equal(matchKeybind("ctrl+-", "-", key({ ctrl: true })), true); +}); + +test("matchKeybind: handles digit keys", () => { + assert.equal(matchKeybind("ctrl+1", "1", key({ ctrl: true })), true); +}); + +test("matchKeybind: empty shortcut returns false", () => { + assert.equal(matchKeybind("", "e", key({ ctrl: true })), false); +}); + +test("matchKeybind: shortcut with only modifier returns false", () => { + assert.equal(matchKeybind("ctrl", "", key({ ctrl: true })), false); +}); + +test("matchKeybind: shortcut without modifier returns false", () => { + assert.equal(matchKeybind("e", "e", key()), false); +}); + +test("matchKeybind: case-insensitive shortcut definition", () => { + assert.equal(matchKeybind("Ctrl+E", "e", key({ ctrl: true })), true); + assert.equal(matchKeybind("CTRL+SHIFT+G", "g", key({ ctrl: true, shift: true })), true); +}); + +// --------------- buildKeybindMatchers --------------- + +test("buildKeybindMatchers: builds matchers from KeybindMap", () => { + const keybinds = { "ctrl+e": "exit", "ctrl+n": "new" }; + const matchers = buildKeybindMatchers(keybinds); + assert.equal(matchers.length, 2); + assert.equal(matchers[0]!.action, "exit"); + assert.equal(matchers[1]!.action, "new"); +}); + +test("buildKeybindMatchers: matchers work correctly", () => { + const keybinds = { "ctrl+s": "skills" }; + const matchers = buildKeybindMatchers(keybinds); + assert.equal(matchers[0]!.match("s", key({ ctrl: true })), true); + assert.equal(matchers[0]!.match("s", key()), false); +}); + +test("buildKeybindMatchers: empty map returns empty array", () => { + const matchers = buildKeybindMatchers({}); + assert.equal(matchers.length, 0); +}); diff --git a/packages/cli/src/tests/slash-commands.test.ts b/packages/cli/src/tests/slash-commands.test.ts index 420e5a48..e570f98d 100644 --- a/packages/cli/src/tests/slash-commands.test.ts +++ b/packages/cli/src/tests/slash-commands.test.ts @@ -29,6 +29,7 @@ test("buildSlashCommands prefixes skills before built-ins", () => { "undo", "mcp", "raw", + "keybind", "exit", ]); }); diff --git a/packages/core/src/tests/settings-and-notify.test.ts b/packages/core/src/tests/settings-and-notify.test.ts index ceddc43e..c57c963c 100644 --- a/packages/core/src/tests/settings-and-notify.test.ts +++ b/packages/core/src/tests/settings-and-notify.test.ts @@ -482,6 +482,60 @@ test("applyModelConfigSelection leaves settings untouched when the effective sel assert.equal(result.settings.model, undefined); }); +// ------ keybinds ------ + +test("resolveSettingsSources defaults keybinds to empty object", () => { + const resolved = resolveSettingsSources( + {}, + null, + { model: "default-model", baseURL: "https://default.example.com" }, + TEST_PROCESS_ENV + ); + assert.deepEqual(resolved.keybinds, {}); +}); + +test("resolveSettingsSources reads keybinds from user settings", () => { + const resolved = resolveSettingsSources( + { keybinds: { "ctrl+e": "exit", "ctrl+n": "new" } }, + null, + { model: "default-model", baseURL: "https://default.example.com" }, + TEST_PROCESS_ENV + ); + assert.deepEqual(resolved.keybinds, { "ctrl+e": "exit", "ctrl+n": "new" }); +}); + +test("resolveSettingsSources merges keybinds with project precedence", () => { + const resolved = resolveSettingsSources( + { keybinds: { "ctrl+e": "exit", "ctrl+s": "skills" } }, + { keybinds: { "ctrl+e": "new", "ctrl+p": "plan" } }, + { model: "default-model", baseURL: "https://default.example.com" }, + TEST_PROCESS_ENV + ); + assert.deepEqual(resolved.keybinds, { + "ctrl+e": "new", + "ctrl+s": "skills", + "ctrl+p": "plan", + }); +}); + +test("resolveSettingsSources filters invalid keybind entries", () => { + const resolved = resolveSettingsSources( + { + keybinds: { + "ctrl+e": "exit", + "": "exit", + invalid: "" as never, + "ctrl+x": 123 as never, + "ctrl+g": null as never, + }, + }, + null, + { model: "default-model", baseURL: "https://default.example.com" }, + TEST_PROCESS_ENV + ); + assert.deepEqual(resolved.keybinds, { "ctrl+e": "exit" }); +}); + test("formatDurationSeconds preserves sub-second precision and trims trailing zeros", () => { assert.equal(formatDurationSeconds(0), "0"); assert.equal(formatDurationSeconds(1250), "1"); From f2cd22516201f6c32fbd8af4a3789cdd49440495 Mon Sep 17 00:00:00 2001 From: al4xdev Date: Tue, 30 Jun 2026 21:21:50 -0300 Subject: [PATCH 5/9] docs: add keybinds configuration reference (zh + en) Document the keybinds settings field following the same format as enabledSkills and mcpServers: - Shortcut format: ctrl+key, ctrl+shift+key, meta+key - Project-over-user merge precedence - Runtime management via /keybind add|remove|list --- docs/configuration.md | 21 +++++++++++++++++++++ docs/configuration_en.md | 21 +++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index d942dc04..809043ed 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -120,6 +120,27 @@ Deep Code 内置免费可用的 Web Search 工具。如果需要自定义搜索 - 将某个 skill 设置为 `false` 后,所有项目级和用户级目录中解析名称相同的 skill 都会被隐藏。 - 项目设置会按 skill 覆盖用户设置。如果项目设置没有配置某个 skill,则使用用户设置。 +#### `keybinds` — 自定义快捷键 + +将键盘快捷键绑定到斜杠命令(如 `/exit`、`/new`、`/skills` 等)或 skill 名称,无需输入 `/` 即可触发操作。 + +```json +{ + "keybinds": { + "ctrl+e": "exit", + "ctrl+n": "new", + "ctrl+s": "skills", + "ctrl+m": "model" + } +} +``` + +- 快捷键格式:`ctrl+key`、`ctrl+shift+key` 或 `meta+key`(例如 `ctrl+e`、`ctrl+shift+i`、`meta+b`)。 +- 键名区分大小写匹配(`ctrl+e` 需按 `Ctrl+E`)。 +- 项目设置会按快捷键覆盖用户设置。如果项目设置中存在同名快捷键,则使用项目设置的值。 +- 自定义快捷键仅在无下拉菜单/弹窗打开时生效,以避免与菜单导航冲突。 +- 也可通过 `/keybind add|remove|list` 命令在运行时管理快捷键。 + #### `mcpServers` — MCP 服务器 MCP(Model Context Protocol)服务器配置。值是键值对,键为服务名称,值为服务器配置对象。 diff --git a/docs/configuration_en.md b/docs/configuration_en.md index a078c428..f3fab5cc 100644 --- a/docs/configuration_en.md +++ b/docs/configuration_en.md @@ -120,6 +120,27 @@ Controls whether skills are included during skill scanning. Keys are resolved sk - Setting a skill to `false` hides every skill with that resolved `name`, across project and user skill roots. - Project settings override user settings per skill. If the project setting omits a skill, the user setting is used. +#### `keybinds` — Custom Keybinds + +Bind keyboard shortcuts to slash commands (e.g. `/exit`, `/new`, `/skills`) or skill names, triggering actions without typing `/`. + +```json +{ + "keybinds": { + "ctrl+e": "exit", + "ctrl+n": "new", + "ctrl+s": "skills", + "ctrl+m": "model" + } +} +``` + +- Shortcut format: `ctrl+key`, `ctrl+shift+key`, or `meta+key` (e.g. `ctrl+e`, `ctrl+shift+i`, `meta+b`). +- Letter matching is case-insensitive (`ctrl+e` matches both `Ctrl+E` and `Ctrl+Shift+E` with exact modifier match). +- Project settings override user settings per shortcut. If a shortcut exists in both, the project value wins. +- Custom keybinds only fire when no dropdown/menu is open, to avoid conflicts with menu navigation. +- You can also manage keybinds at runtime via `/keybind add|remove|list`. + #### `mcpServers` — MCP Servers Configuration for MCP (Model Context Protocol) servers. The value is a key-value pair, where the key is the service name and the value is a server configuration object. From e0cc9e2852e9dccb909a00f65600e90ab5b279d0 Mon Sep 17 00:00:00 2001 From: al4xdev Date: Tue, 30 Jun 2026 21:26:43 -0300 Subject: [PATCH 6/9] fix(cli): refresh resolved settings after /keybind add or remove Add onKeybindsChanged callback so that keybinds added or removed via the /keybind slash command take effect immediately without requiring a session restart. The callback re-resolves settings from disk, matching the pattern used by handleModelConfigChange. --- .plan/backlog.md | 24 ++++ .plan/contribution-plan.md | 161 ++++++++++++++++++++++ packages/cli/src/ui/views/App.tsx | 6 + packages/cli/src/ui/views/PromptInput.tsx | 4 + 4 files changed, 195 insertions(+) create mode 100644 .plan/backlog.md create mode 100644 .plan/contribution-plan.md diff --git a/.plan/backlog.md b/.plan/backlog.md new file mode 100644 index 00000000..292c64a8 --- /dev/null +++ b/.plan/backlog.md @@ -0,0 +1,24 @@ +# DeepCode CLI — Backlog de Contribuições + +## Keybindings / UX + +- [ ] `exit` command — sair da CLI de forma limpa +- [ ] `Esc Esc` — atalho para undo (mais ergonômico que `/undo`) + +## Skills bundled (PR upstream) + +- [ ] **plan-verify** — skill de verificação pós-implementação que: + - Relê o `` original da sessão + - Extrai todos os deliverables prometidos (seções: Implementation Changes, + Test Plan, etc.) + - Compara contra o que foi realmente entregue (arquivos alterados, testes + escritos, commits feitos) + - Lista itens faltantes e cobra do agente antes de declarar "pronto" + - Trigger: final de implementação, quando UpdatePlan marca tudo `[x]` + - Problema que resolve: agente cumpre checklist de código mas ignora + deliverables do plano (ex: promete testes no Test Plan e não escreve) + +- [ ] **engineering-standards** (versão genérica) — concurrency, file I/O, + atomicity, testing, pre-commit checklist. Adaptado do que já funciona em + `~/.agents/skills/engineering-standards/SKILL.md`, sem referências a projetos + específicos. diff --git a/.plan/contribution-plan.md b/.plan/contribution-plan.md new file mode 100644 index 00000000..e97163d1 --- /dev/null +++ b/.plan/contribution-plan.md @@ -0,0 +1,161 @@ +# DeepCode CLI — Plano de Contribuição + +## Por que contribuir + +- Ferramenta artesanal, poucos devs, poucas skills bundled +- O gap principal é de **prompt/guidelines**, não de runtime — e isso é meu forte +- Já mapeei os internals: cada prompt, skill, template +- Meus system prompts (AGENTS.md + skills) levaram output de 6.5/10 → 9/10 +- Já uso a API DeepSeek que é o target principal da ferramenta +- Contribuições teriam impacto desproporcional no projeto + +## Estado atual do built-in + +### O que o DeepCode tem (pouco) + +``` +templates/ +├── prompts/init_command.md.ejs → gera AGENTS.md inicial (45 linhas) +├── tools/*.md → descrição das 6 tools (bash, read, write, edit, etc.) +└── skills/karpathy-guidelines.md → 72 linhas, puramente comportamental + +bundled/ +├── plan/SKILL.md → 134 linhas, bom (3 fases, decision-complete) +├── skill-writer/SKILL.md → 403 linhas, completo +├── skill-digester/SKILL.md → review/install skills +└── deepcode-self-refer/SKILL.md → auto-referência +``` + +### O que falta (e eu já resolvi no userland) + +| Gap | Minha solução | Onde tá | +|---|---|---| +| Concurrency / locks | engineering-standards | `~/.agents/skills/` | +| File I/O performance | engineering-standards | `~/.agents/skills/` | +| Atomic writes | python-fastapi-patterns | `~/.deepcode/skills/` | +| Testing enforcement | engineering-standards | `~/.agents/skills/` | +| Pre-commit checklist | engineering-standards | `~/.agents/skills/` | +| Plan adherence | engineering-standards | `~/.agents/skills/` | +| Migration patterns | python-fastapi-patterns | `~/.deepcode/skills/` | +| Config resolution isolation | python-fastapi-patterns | `~/.deepcode/skills/` | +| Plan verification pós-impl | NÃO EXISTE AINDA | backlog | + +## Filosofia do Mantenedor e Perfil de Review + +Análise de PRs fechados/ignorados (como o PR #132 de temas e a recusa do PR de File Mention) revela a mentalidade do mantenedor (`lessweb`): + +1. **"Menos Framework, Mais LLM"**: Ele prefere manter o runtime (backend Node) o mais simples e burro possível. Exemplo: prefere que o frontend apenas ajude a autocompletar caminhos (`@`) e que a IA decida chamar a tool `Read` de forma autônoma, em vez de o framework ler e injetar o arquivo de forma "não inteligente" no contexto. +2. **Aversão a Bloat de Código**: Ele rejeita PRs que introduzem muitas mudanças no backend por medo de aumentar a dívida de manutenção ("Read tool mudando de ferramenta do agente para dependência do framework"). +3. **Foco em UX no Frontend**: Ele valoriza a TUI, mas é extremamente cauteloso com edge-cases de interação (ex: o que acontece se o usuário apagar caracteres no meio do caminho de um arquivo mention de forma dinâmica). +4. **Perfil Altamente Seletivo (Lento)**: PRs simples e com testes demoram semanas/meses para serem analisados ou são reescritos por ele mesmo na branch `main`. + +### Implicação para a Estratégia: +- **Upstream PRs**: Precisam ser ultra-minimalistas, sem mexer na lógica do core do backend. Foco em customizações que não impõem decisões dogmáticas ao mantenedor (como aliases no settings). +- **Personal Fork**: Destinado a features que violam a regra de "simplicidade do framework" mas aumentam a sua produtividade pessoal (como o gatekeeper e a integração automática com o `agy` CLI). + +## Estratégia de Contribuição: Upstream vs. Personal Fork + +Dividido em duas trilhas: **Upstream PRs** (mudanças genéricas de alta aceitação) e **Personal Fork** (customizações profundas, integrações proprietárias e wrappers de agentes). + +--- + +## Trilha A — Upstream PRs (Mudanças de Alta Aceitação) + +Foco em UX simples e habilidades genéricas que complementam o que já existe sem impor decisões dogmáticas ao mantenedor. + +### PR 1 — UX & Keybindings +- `exit` command no console da CLI para sair de forma limpa. +- `Esc Esc` como atalho rápido para desfazer a última ação (`/undo`). +- *Status*: Baixo risco, atrativo para novos usuários. + +### PR 2 — Melhorar a descrição da ferramenta `bash` +- Atualizar a descrição da tool `bash` (em `templates/tools/bash.md`) injetando as boas práticas operacionais do seu `AGENTS.md`: + - Instruir explicitamente o modelo a sempre capturar saídas (`set st $status` ou `; or report_error`). + - Mapear para comandos silenciosos (`mv`, `mkdir`, `chmod`) o append de `&& echo ok` para confirmação de runtime. +- *Status*: Corrige o comportamento padrão de execução cega de comandos. + +### PR 3 — `engineering-standards` como Skill Bundled +- Propor uma versão genérica do seu `~/.agents/skills/engineering-standards/SKILL.md`. +- Remover referências específicas a este projeto (`sessions.json`, `state_lock`). +- Foco em: locks para concorrência de arquivos, evitar I/O em loops estáticos, e checklist pré-commit. +- *Status*: Fica ao lado do Karpathy Guidelines como guia técnico (Karpathy = conceitual; este = engenharia). + +--- + +## Trilha B — Personal Fork (Minha Customização de Liderança/Staff) + +Para o que o upstream recusar por ser muito específico, ou para integrações profundas com sua stack e fluxos locais. + +### Feature 1 — `python-tooling` e `fish-tooling` Skills +- **`python-tooling`**: Skill bundled nativa para forçar o loop de validação (`ruff check .`, `mypy`, `pytest` via `uv` no `.venv/`). Evita erros de sintaxe ou imports que quebram o runtime silenciosamente. +- **`fish-tooling`**: Configuração e escrita de scripts executáveis nativos em `.fish` em vez de `.sh`, alinhado com o shell padrão do ambiente. +- *Status*: Muito específico do seu setup de desenvolvimento local. + +### Feature 2 — Gatekeeper Determinístico no Runtime (PR 5) +- Modificação em TypeScript no runtime do DeepCode. +- Quando o `UpdatePlan` marcar tudo `[x]`, o runtime intercepta a finalização: + - Parseia o `` original usando regex. + - Verifica no filesystem se os arquivos da seção "Implementation Changes" têm diff no git. + - Verifica se arquivos de teste em "Test Plan" foram criados ou alterados. + - Se faltar algo, aborta o commit e injeta: `[Gatekeeper] Itens não entregues: X. Corrija antes de prosseguir.` +- *Status*: Transforma prompt (confiança) em código determinístico (enforcement). + +### Feature 3 — Wrapper Skill `antigravity-critic` (Integração com Claude/Gemini) +- Criação de uma skill local que atua como ponte para a CLI do Antigravity (`agy`). +- Permite ao DeepCode invocar de forma autônoma revisões do Opus/Gemini no meio do seu próprio loop de escrita: + ```bash + git diff | agy "Faça uma auditoria de segurança focada em concorrência neste diff." + ``` +- O DeepCode lê o output da auditoria e auto-corrige o código antes do seu commit final. +- *Status*: Orquestração multi-agente local de baixo custo. + +--- + +## Design Patterns do meta_2028 aplicáveis + +Referência: `~/git/my/meta_2028/` — multi-agent Actor-Critic para otimização de CV com sandbox containerizado. + +### 1. "Fails silently → code, fails loudly → prose" + +Heurística central do meta_2028 para decidir o que vive em prose (runbooks .md) vs o que vive em código (Python determinístico): + +| Tipo de falha | Onde vive | Exemplo no DeepCode | +|---|---|---| +| **Fails loudly** (erro visível, agent self-heals) | Prose (skill) | "use locks" — se não usar, o bug aparece em testes | +| **Fails silently** (completa sem erro, resultado errado) | Code (runtime) | Agente marca tudo `[x]` mas não escreveu os testes prometidos | + +O DeepCode hoje é 100% prose (skills). O gap é que falhas silenciosas (plano não cumprido, testes não escritos) passam despercebidas porque não tem código determinístico validando. + +### 2. Actor-Critic loop → plan-verify + +O meta_2028 usa Karen (critic) → Bill (editor) → loop até score atingir target. No DeepCode, o equivalente seria: + +``` +Agente implementa → plan-verify (critic) audita deliverables + → se faltou algo → injeta feedback → agente corrige → loop + → se tudo OK → permite commit +``` + +Hoje o DeepCode tem o Actor (agente implementa) mas não tem o Critic (ninguém valida se o plano foi cumprido). A skill `plan-verify` resolve em prose; a Feature 2 (gatekeeper) resolve em código. + +### 3. Gatekeeper com exit codes + +O `gatekeeper.py` do meta_2028 é um módulo Python puro que: +- Parseia output do critic com regex robusto. +- Retorna exit codes determinísticos (0=success, 1=max loops, 2=continue, 3=error). +- O orchestrator nunca confia no modelo pra decidir se o loop continua. + +Aplicação no DeepCode: o runtime do seu fork pode ter um mini-gatekeeper que parseia o `` e valida com checagem de filesystem (git diff, file exists) antes de aceitar o "pronto" do agente. + +## Complexidade real + +- **Trilha Upstream (PRs 1-4)**: Baixa. Em sua maioria apenas alteração de arquivos de markdown (`bundled/` ou `templates/`). O PR 1 envolve poucas linhas no tratador de teclado do console Node. +- **Trilha Fork (Features 1-3)**: Média/Alta. Envolve alterações no interpretador de estado do terminal da CLI (TypeScript/Node) para criar ganchos no `UpdatePlan` e interceptar o fluxo, além de scripts wrappers para o `agy`. + +## Referências + +- `~/.deepcode/AGENTS.md` — regras operacionais (a base) +- `~/.deepcode/skills/python-fastapi-patterns/SKILL.md` — patterns específicos +- `~/.agents/skills/engineering-standards/SKILL.md` — standards genéricos +- `~/git/my/meta_2028/` — design patterns (Actor-Critic, prose vs code, gatekeeper) +- Review que motivou tudo: commit 75fde122 no orchestrator (6.5/10 → 9/10) diff --git a/packages/cli/src/ui/views/App.tsx b/packages/cli/src/ui/views/App.tsx index fa17b62a..07661b7d 100644 --- a/packages/cli/src/ui/views/App.tsx +++ b/packages/cli/src/ui/views/App.tsx @@ -500,6 +500,11 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp handleExit({ showCommand: false, showSummary: false }); }, [handleExit]); + const handleKeybindsChanged = useCallback(() => { + const next = resolveCurrentSettings(projectRoot); + setResolvedSettings(next); + }, [projectRoot]); + const reloadActiveSessionView = useCallback( (sessionId: string): void => { resetStaticView(loadVisibleMessages(sessionManager, sessionId), { clearScreen: true }); @@ -983,6 +988,7 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp onInterrupt={handleInterrupt} onToggleProcessStdout={handleToggleProcessStdout} onExitShortcut={handleExitShortcut} + onKeybindsChanged={handleKeybindsChanged} keybinds={resolvedSettings.keybinds} placeholder="Type your message..." statusLineSegments={statusLineSegments} diff --git a/packages/cli/src/ui/views/PromptInput.tsx b/packages/cli/src/ui/views/PromptInput.tsx index 91be0d01..54f5c73f 100644 --- a/packages/cli/src/ui/views/PromptInput.tsx +++ b/packages/cli/src/ui/views/PromptInput.tsx @@ -104,6 +104,7 @@ type Props = { onInterrupt: () => void; onToggleProcessStdout?: () => void; onExitShortcut?: () => void; + onKeybindsChanged?: () => void; keybinds?: KeybindMap; }; @@ -138,6 +139,7 @@ export const PromptInput = React.memo(function PromptInput({ onToggleProcessStdout, onExitShortcut, onRawModeChange, + onKeybindsChanged, keybinds, }: Props): React.ReactElement { const { stdout } = useStdout(); @@ -799,6 +801,7 @@ export const PromptInput = React.memo(function PromptInput({ } else { writeSettings(updated); } + onKeybindsChanged?.(); setStatusMessage(`Keybind added: ${shortcut} → /${action}`); clearSlashToken(); return; @@ -827,6 +830,7 @@ export const PromptInput = React.memo(function PromptInput({ } else { writeSettings(updated); } + onKeybindsChanged?.(); setStatusMessage(`Keybind removed: ${shortcut}`); clearSlashToken(); return; From fb745d67a7d2f1e3942a4ae57d991a2dc7ddfd9f Mon Sep 17 00:00:00 2001 From: al4xdev Date: Tue, 30 Jun 2026 22:52:59 -0300 Subject: [PATCH 7/9] feat(keybinds): add custom keybind management with --global flag - Add /keybind slash command with add, remove, list subcommands - Auto-complete partial matches (/key -> /keybind) - Show usage hint on bare /keybind instead of opening view - /keybind list opens KeybindsView showing all configured keybinds - --global flag forces operations on user-level settings - Cross-level duplicate detection (merged keybind lookup) - KeybindsView shows [global]/[local] origin indicator - Custom keybind matchers trigger slash actions on shortcut press - Status message timeout increased to 8s for readability --- packages/cli/src/ui/views/App.tsx | 9 ++- packages/cli/src/ui/views/KeybindsView.tsx | 72 +++++++++++++++++ packages/cli/src/ui/views/PromptInput.tsx | 92 +++++++++++++++------- 3 files changed, 143 insertions(+), 30 deletions(-) create mode 100644 packages/cli/src/ui/views/KeybindsView.tsx diff --git a/packages/cli/src/ui/views/App.tsx b/packages/cli/src/ui/views/App.tsx index 07661b7d..54be297b 100644 --- a/packages/cli/src/ui/views/App.tsx +++ b/packages/cli/src/ui/views/App.tsx @@ -13,6 +13,7 @@ import { findExpandedThinkingId } from "../core/thinking-state"; import { WelcomeScreen } from "./WelcomeScreen"; import { AskUserQuestionPrompt } from "./AskUserQuestionPrompt"; import { McpStatusList } from "./McpStatusList"; +import { KeybindsView } from "./KeybindsView"; import { ProcessStdoutView } from "./ProcessStdoutView"; import { type AskUserQuestionAnswers, @@ -50,7 +51,7 @@ import { SessionManager } from "@vegamo/deepcode-core"; import { getCompactPromptTokenThreshold } from "@vegamo/deepcode-core"; import { writeStdout, writeStdoutLine } from "../../utils/stdio-helpers"; -type View = "chat" | "session-list" | "undo" | "mcp-status"; +type View = "chat" | "session-list" | "undo" | "keybinds" | "mcp-status"; const STATUS_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; @@ -361,6 +362,10 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp navigateToSubView("mcp-status"); return; } + if (submission.command === "keybind") { + navigateToSubView("keybinds"); + return; + } const prompt: UserPromptContent = { text: submission.text, @@ -945,6 +950,8 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp setView("chat"); }} /> + ) : view === "keybinds" ? ( + setView("chat")} /> ) : view === "mcp-status" ? ( void; +}; + +function getKeybindLevel( + shortcut: string, + userSettings: DeepcodingSettings | null, + projectSettings: DeepcodingSettings | null +): "local" | "global" { + if (projectSettings?.keybinds?.[shortcut] !== undefined) return "local"; + if (userSettings?.keybinds?.[shortcut] !== undefined) return "global"; + return "global"; +} + +export function KeybindsView({ keybinds, projectRoot, onCancel }: Props): React.ReactElement { + const entries = Object.entries(keybinds); + const userSettings = readSettings(); + const projectSettings = readProjectSettings(projectRoot); + + useInput((_input, key) => { + if (key.escape) { + onCancel(); + } + }); + + return ( + + + + /keybind + + + + {entries.length === 0 ? ( + + (no keybinds configured) + + ) : ( + + {entries.map(([shortcut, action]) => { + const level = getKeybindLevel(shortcut, userSettings, projectSettings); + return ( + + {shortcut} + → /{action} + + [{level}] + + + ); + })} + + )} + + + /keybind add <shortcut> <action> to add + /keybind --global add <shortcut> <action> (user-level) + /keybind remove <shortcut> to remove + /keybind --global remove <shortcut> (user-level) + + + Esc to close + + ); +} diff --git a/packages/cli/src/ui/views/PromptInput.tsx b/packages/cli/src/ui/views/PromptInput.tsx index 54f5c73f..ab32a9fe 100644 --- a/packages/cli/src/ui/views/PromptInput.tsx +++ b/packages/cli/src/ui/views/PromptInput.tsx @@ -74,7 +74,7 @@ export type PromptSubmission = { selectedSkills?: SkillInfo[]; permissions?: UserToolPermission[]; alwaysAllows?: PermissionScope[]; - command?: "new" | "resume" | "continue" | "undo" | "mcp" | "exit"; + command?: "new" | "resume" | "continue" | "undo" | "mcp" | "keybind" | "exit"; }; export type PromptDraft = { @@ -293,7 +293,7 @@ export const PromptInput = React.memo(function PromptInput({ if (!statusMessage) { return; } - const timer = setTimeout(() => setStatusMessage(null), 2500); + const timer = setTimeout(() => setStatusMessage(null), 8000); return () => clearTimeout(timer); }, [statusMessage]); @@ -738,7 +738,18 @@ export const PromptInput = React.memo(function PromptInput({ return; } if (item.kind === "keybind") { - handleKeybindCommand(); + const parts = buffer.text.trim().split(/\s+/); + if (parts.length > 1) { + // Has args: process add/remove/list subcommand + handleKeybindCommand(); + } else if (parts[0] && parts[0] !== "/keybind") { + // Partial match from menu (e.g. /key): auto-complete to /keybind + setBuffer({ text: "/keybind ", cursor: "/keybind ".length }); + clearUndoRedoStacks(); + } else { + // Exact match with no args: show usage hint + setStatusMessage("Usage: /keybind add | remove | list"); + } return; } if (item.kind === "exit") { @@ -750,20 +761,19 @@ export const PromptInput = React.memo(function PromptInput({ } function handleKeybindCommand(): void { - const parts = buffer.text.trim().split(/\s+/); + const rawParts = buffer.text.trim().split(/\s+/); + const isGlobal = rawParts.includes("--global"); + const parts = rawParts.filter((p) => p !== "--global"); const subcommand = parts[1] ?? "list"; const existingProjectSettings = readProjectSettings(projectRoot); + const useProject = existingProjectSettings !== null; + const userSettings = readSettings(); + const kbs = keybinds ?? {}; + const levelLabel = isGlobal ? " (global)" : useProject ? " (project)" : " (global)"; if (subcommand === "list") { - clearSlashToken(); - const kbs = keybinds ?? {}; - const entries = Object.entries(kbs); - if (entries.length === 0) { - setStatusMessage("No custom keybinds configured. Use /keybind add "); - } else { - const lines = entries.map(([shortcut, action]) => ` ${shortcut} → /${action}`); - setStatusMessage(`Keybinds:\n${lines.join("\n")}`); - } + onSubmit({ text: "/keybind list", imageUrls: [], command: "keybind" }); + resetPromptInput(); return; } @@ -786,52 +796,76 @@ export const PromptInput = React.memo(function PromptInput({ // Validate action is a known slash command or skill const knownNames = new Set(slashItems.map((s) => s.name)); if (!knownNames.has(action)) { - setStatusMessage(`Unknown action "/${action}". Use a slash command or skill name.`); + const available = slashItems + .filter((s) => s.kind !== "skill") + .map((s) => s.name) + .slice(0, 10) + .join(", "); + setStatusMessage(`Unknown action "/${action}". Available: ${available}…`); clearSlashToken(); return; } - const useProject = existingProjectSettings !== null; - const rawSettings = useProject ? existingProjectSettings : readSettings(); + // Check duplicates across all levels (merged) + const existingMerged = kbs[shortcut]; + if (existingMerged === action) { + setStatusMessage(`Keybind already set: ${shortcut} → /${action}`); + clearSlashToken(); + return; + } + + // Determine write target + const rawSettings = isGlobal ? userSettings : useProject ? existingProjectSettings : userSettings; const current: Record = { ...(rawSettings?.keybinds ?? {}) }; current[shortcut] = action; const updated = { ...(rawSettings ?? {}), keybinds: current }; - if (useProject) { - writeProjectSettings(updated, projectRoot); - } else { + if (isGlobal || !useProject) { writeSettings(updated); + } else { + writeProjectSettings(updated, projectRoot); } onKeybindsChanged?.(); - setStatusMessage(`Keybind added: ${shortcut} → /${action}`); + if (existingMerged) { + setStatusMessage(`Keybind updated: ${shortcut} → /${action} (was /${existingMerged})${levelLabel}`); + } else { + setStatusMessage(`Keybind added: ${shortcut} → /${action}${levelLabel}`); + } clearSlashToken(); return; } if (subcommand === "remove") { const shortcut = parts[2]; + const rawSettings = isGlobal ? userSettings : useProject ? existingProjectSettings : userSettings; + const current: Record = { ...(rawSettings?.keybinds ?? {}) }; + const existingShortcuts = Object.keys(kbs); if (!shortcut) { - setStatusMessage("Usage: /keybind remove "); + if (existingShortcuts.length === 0) { + setStatusMessage("No custom keybinds to remove."); + } else { + const list = existingShortcuts.map((s) => `${s} → /${kbs[s]}`).join(", "); + setStatusMessage(`Usage: /keybind remove \nExisting: ${list}`); + } clearSlashToken(); return; } - const useProject = existingProjectSettings !== null; - const rawSettings = useProject ? existingProjectSettings : readSettings(); - const current: Record = { ...(rawSettings?.keybinds ?? {}) }; if (!(shortcut in current)) { - setStatusMessage(`Keybind "${shortcut}" not found.`); + const list = + existingShortcuts.length > 0 ? existingShortcuts.map((s) => `${s} → /${kbs[s]}`).join(", ") : "(none)"; + setStatusMessage(`Keybind "${shortcut}" not found${levelLabel}.\nExisting: ${list}`); clearSlashToken(); return; } delete current[shortcut]; const updated = { ...(rawSettings ?? {}), keybinds: current }; - if (useProject) { - writeProjectSettings(updated, projectRoot); - } else { + if (isGlobal || !useProject) { writeSettings(updated); + } else { + writeProjectSettings(updated, projectRoot); } onKeybindsChanged?.(); - setStatusMessage(`Keybind removed: ${shortcut}`); + setStatusMessage(`Keybind removed: ${shortcut}${levelLabel}`); clearSlashToken(); return; } From 351daf56d41c61be7e72975f4ee9e0aae45ec739 Mon Sep 17 00:00:00 2001 From: al4xdev Date: Tue, 30 Jun 2026 23:07:11 -0300 Subject: [PATCH 8/9] chore: remove plan files --- .plan/backlog.md | 24 ------ .plan/contribution-plan.md | 161 ------------------------------------- 2 files changed, 185 deletions(-) delete mode 100644 .plan/backlog.md delete mode 100644 .plan/contribution-plan.md diff --git a/.plan/backlog.md b/.plan/backlog.md deleted file mode 100644 index 292c64a8..00000000 --- a/.plan/backlog.md +++ /dev/null @@ -1,24 +0,0 @@ -# DeepCode CLI — Backlog de Contribuições - -## Keybindings / UX - -- [ ] `exit` command — sair da CLI de forma limpa -- [ ] `Esc Esc` — atalho para undo (mais ergonômico que `/undo`) - -## Skills bundled (PR upstream) - -- [ ] **plan-verify** — skill de verificação pós-implementação que: - - Relê o `` original da sessão - - Extrai todos os deliverables prometidos (seções: Implementation Changes, - Test Plan, etc.) - - Compara contra o que foi realmente entregue (arquivos alterados, testes - escritos, commits feitos) - - Lista itens faltantes e cobra do agente antes de declarar "pronto" - - Trigger: final de implementação, quando UpdatePlan marca tudo `[x]` - - Problema que resolve: agente cumpre checklist de código mas ignora - deliverables do plano (ex: promete testes no Test Plan e não escreve) - -- [ ] **engineering-standards** (versão genérica) — concurrency, file I/O, - atomicity, testing, pre-commit checklist. Adaptado do que já funciona em - `~/.agents/skills/engineering-standards/SKILL.md`, sem referências a projetos - específicos. diff --git a/.plan/contribution-plan.md b/.plan/contribution-plan.md deleted file mode 100644 index e97163d1..00000000 --- a/.plan/contribution-plan.md +++ /dev/null @@ -1,161 +0,0 @@ -# DeepCode CLI — Plano de Contribuição - -## Por que contribuir - -- Ferramenta artesanal, poucos devs, poucas skills bundled -- O gap principal é de **prompt/guidelines**, não de runtime — e isso é meu forte -- Já mapeei os internals: cada prompt, skill, template -- Meus system prompts (AGENTS.md + skills) levaram output de 6.5/10 → 9/10 -- Já uso a API DeepSeek que é o target principal da ferramenta -- Contribuições teriam impacto desproporcional no projeto - -## Estado atual do built-in - -### O que o DeepCode tem (pouco) - -``` -templates/ -├── prompts/init_command.md.ejs → gera AGENTS.md inicial (45 linhas) -├── tools/*.md → descrição das 6 tools (bash, read, write, edit, etc.) -└── skills/karpathy-guidelines.md → 72 linhas, puramente comportamental - -bundled/ -├── plan/SKILL.md → 134 linhas, bom (3 fases, decision-complete) -├── skill-writer/SKILL.md → 403 linhas, completo -├── skill-digester/SKILL.md → review/install skills -└── deepcode-self-refer/SKILL.md → auto-referência -``` - -### O que falta (e eu já resolvi no userland) - -| Gap | Minha solução | Onde tá | -|---|---|---| -| Concurrency / locks | engineering-standards | `~/.agents/skills/` | -| File I/O performance | engineering-standards | `~/.agents/skills/` | -| Atomic writes | python-fastapi-patterns | `~/.deepcode/skills/` | -| Testing enforcement | engineering-standards | `~/.agents/skills/` | -| Pre-commit checklist | engineering-standards | `~/.agents/skills/` | -| Plan adherence | engineering-standards | `~/.agents/skills/` | -| Migration patterns | python-fastapi-patterns | `~/.deepcode/skills/` | -| Config resolution isolation | python-fastapi-patterns | `~/.deepcode/skills/` | -| Plan verification pós-impl | NÃO EXISTE AINDA | backlog | - -## Filosofia do Mantenedor e Perfil de Review - -Análise de PRs fechados/ignorados (como o PR #132 de temas e a recusa do PR de File Mention) revela a mentalidade do mantenedor (`lessweb`): - -1. **"Menos Framework, Mais LLM"**: Ele prefere manter o runtime (backend Node) o mais simples e burro possível. Exemplo: prefere que o frontend apenas ajude a autocompletar caminhos (`@`) e que a IA decida chamar a tool `Read` de forma autônoma, em vez de o framework ler e injetar o arquivo de forma "não inteligente" no contexto. -2. **Aversão a Bloat de Código**: Ele rejeita PRs que introduzem muitas mudanças no backend por medo de aumentar a dívida de manutenção ("Read tool mudando de ferramenta do agente para dependência do framework"). -3. **Foco em UX no Frontend**: Ele valoriza a TUI, mas é extremamente cauteloso com edge-cases de interação (ex: o que acontece se o usuário apagar caracteres no meio do caminho de um arquivo mention de forma dinâmica). -4. **Perfil Altamente Seletivo (Lento)**: PRs simples e com testes demoram semanas/meses para serem analisados ou são reescritos por ele mesmo na branch `main`. - -### Implicação para a Estratégia: -- **Upstream PRs**: Precisam ser ultra-minimalistas, sem mexer na lógica do core do backend. Foco em customizações que não impõem decisões dogmáticas ao mantenedor (como aliases no settings). -- **Personal Fork**: Destinado a features que violam a regra de "simplicidade do framework" mas aumentam a sua produtividade pessoal (como o gatekeeper e a integração automática com o `agy` CLI). - -## Estratégia de Contribuição: Upstream vs. Personal Fork - -Dividido em duas trilhas: **Upstream PRs** (mudanças genéricas de alta aceitação) e **Personal Fork** (customizações profundas, integrações proprietárias e wrappers de agentes). - ---- - -## Trilha A — Upstream PRs (Mudanças de Alta Aceitação) - -Foco em UX simples e habilidades genéricas que complementam o que já existe sem impor decisões dogmáticas ao mantenedor. - -### PR 1 — UX & Keybindings -- `exit` command no console da CLI para sair de forma limpa. -- `Esc Esc` como atalho rápido para desfazer a última ação (`/undo`). -- *Status*: Baixo risco, atrativo para novos usuários. - -### PR 2 — Melhorar a descrição da ferramenta `bash` -- Atualizar a descrição da tool `bash` (em `templates/tools/bash.md`) injetando as boas práticas operacionais do seu `AGENTS.md`: - - Instruir explicitamente o modelo a sempre capturar saídas (`set st $status` ou `; or report_error`). - - Mapear para comandos silenciosos (`mv`, `mkdir`, `chmod`) o append de `&& echo ok` para confirmação de runtime. -- *Status*: Corrige o comportamento padrão de execução cega de comandos. - -### PR 3 — `engineering-standards` como Skill Bundled -- Propor uma versão genérica do seu `~/.agents/skills/engineering-standards/SKILL.md`. -- Remover referências específicas a este projeto (`sessions.json`, `state_lock`). -- Foco em: locks para concorrência de arquivos, evitar I/O em loops estáticos, e checklist pré-commit. -- *Status*: Fica ao lado do Karpathy Guidelines como guia técnico (Karpathy = conceitual; este = engenharia). - ---- - -## Trilha B — Personal Fork (Minha Customização de Liderança/Staff) - -Para o que o upstream recusar por ser muito específico, ou para integrações profundas com sua stack e fluxos locais. - -### Feature 1 — `python-tooling` e `fish-tooling` Skills -- **`python-tooling`**: Skill bundled nativa para forçar o loop de validação (`ruff check .`, `mypy`, `pytest` via `uv` no `.venv/`). Evita erros de sintaxe ou imports que quebram o runtime silenciosamente. -- **`fish-tooling`**: Configuração e escrita de scripts executáveis nativos em `.fish` em vez de `.sh`, alinhado com o shell padrão do ambiente. -- *Status*: Muito específico do seu setup de desenvolvimento local. - -### Feature 2 — Gatekeeper Determinístico no Runtime (PR 5) -- Modificação em TypeScript no runtime do DeepCode. -- Quando o `UpdatePlan` marcar tudo `[x]`, o runtime intercepta a finalização: - - Parseia o `` original usando regex. - - Verifica no filesystem se os arquivos da seção "Implementation Changes" têm diff no git. - - Verifica se arquivos de teste em "Test Plan" foram criados ou alterados. - - Se faltar algo, aborta o commit e injeta: `[Gatekeeper] Itens não entregues: X. Corrija antes de prosseguir.` -- *Status*: Transforma prompt (confiança) em código determinístico (enforcement). - -### Feature 3 — Wrapper Skill `antigravity-critic` (Integração com Claude/Gemini) -- Criação de uma skill local que atua como ponte para a CLI do Antigravity (`agy`). -- Permite ao DeepCode invocar de forma autônoma revisões do Opus/Gemini no meio do seu próprio loop de escrita: - ```bash - git diff | agy "Faça uma auditoria de segurança focada em concorrência neste diff." - ``` -- O DeepCode lê o output da auditoria e auto-corrige o código antes do seu commit final. -- *Status*: Orquestração multi-agente local de baixo custo. - ---- - -## Design Patterns do meta_2028 aplicáveis - -Referência: `~/git/my/meta_2028/` — multi-agent Actor-Critic para otimização de CV com sandbox containerizado. - -### 1. "Fails silently → code, fails loudly → prose" - -Heurística central do meta_2028 para decidir o que vive em prose (runbooks .md) vs o que vive em código (Python determinístico): - -| Tipo de falha | Onde vive | Exemplo no DeepCode | -|---|---|---| -| **Fails loudly** (erro visível, agent self-heals) | Prose (skill) | "use locks" — se não usar, o bug aparece em testes | -| **Fails silently** (completa sem erro, resultado errado) | Code (runtime) | Agente marca tudo `[x]` mas não escreveu os testes prometidos | - -O DeepCode hoje é 100% prose (skills). O gap é que falhas silenciosas (plano não cumprido, testes não escritos) passam despercebidas porque não tem código determinístico validando. - -### 2. Actor-Critic loop → plan-verify - -O meta_2028 usa Karen (critic) → Bill (editor) → loop até score atingir target. No DeepCode, o equivalente seria: - -``` -Agente implementa → plan-verify (critic) audita deliverables - → se faltou algo → injeta feedback → agente corrige → loop - → se tudo OK → permite commit -``` - -Hoje o DeepCode tem o Actor (agente implementa) mas não tem o Critic (ninguém valida se o plano foi cumprido). A skill `plan-verify` resolve em prose; a Feature 2 (gatekeeper) resolve em código. - -### 3. Gatekeeper com exit codes - -O `gatekeeper.py` do meta_2028 é um módulo Python puro que: -- Parseia output do critic com regex robusto. -- Retorna exit codes determinísticos (0=success, 1=max loops, 2=continue, 3=error). -- O orchestrator nunca confia no modelo pra decidir se o loop continua. - -Aplicação no DeepCode: o runtime do seu fork pode ter um mini-gatekeeper que parseia o `` e valida com checagem de filesystem (git diff, file exists) antes de aceitar o "pronto" do agente. - -## Complexidade real - -- **Trilha Upstream (PRs 1-4)**: Baixa. Em sua maioria apenas alteração de arquivos de markdown (`bundled/` ou `templates/`). O PR 1 envolve poucas linhas no tratador de teclado do console Node. -- **Trilha Fork (Features 1-3)**: Média/Alta. Envolve alterações no interpretador de estado do terminal da CLI (TypeScript/Node) para criar ganchos no `UpdatePlan` e interceptar o fluxo, além de scripts wrappers para o `agy`. - -## Referências - -- `~/.deepcode/AGENTS.md` — regras operacionais (a base) -- `~/.deepcode/skills/python-fastapi-patterns/SKILL.md` — patterns específicos -- `~/.agents/skills/engineering-standards/SKILL.md` — standards genéricos -- `~/git/my/meta_2028/` — design patterns (Actor-Critic, prose vs code, gatekeeper) -- Review que motivou tudo: commit 75fde122 no orchestrator (6.5/10 → 9/10) From 7ac9d367230b4d5750d0700d967f048a20cbd372 Mon Sep 17 00:00:00 2001 From: al4xdev Date: Tue, 30 Jun 2026 23:12:28 -0300 Subject: [PATCH 9/9] chore: revert global statusMessage timeout to 2500ms --- packages/cli/src/ui/views/PromptInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/ui/views/PromptInput.tsx b/packages/cli/src/ui/views/PromptInput.tsx index ab32a9fe..c3e02599 100644 --- a/packages/cli/src/ui/views/PromptInput.tsx +++ b/packages/cli/src/ui/views/PromptInput.tsx @@ -293,7 +293,7 @@ export const PromptInput = React.memo(function PromptInput({ if (!statusMessage) { return; } - const timer = setTimeout(() => setStatusMessage(null), 8000); + const timer = setTimeout(() => setStatusMessage(null), 2500); return () => clearTimeout(timer); }, [statusMessage]);