From b0c64f33c8c003212c487ee9a1d38fa4a9fc335d Mon Sep 17 00:00:00 2001 From: "robopipi[bot]" <336283572+robopipi[bot]@users.noreply.github.com> Date: Wed, 4 Nov 2026 09:59:49 +0100 Subject: [PATCH] fix(coding-agent): load user keybindings in pi config (#11788) `pi config` ignored `~/.pi/agent/keybindings.json`. `selectConfig()` never called `setKeybindings()`, so the config selector's `getKeybindings()` fell back to the default TUI bindings. Remaps such as `"tui.select.down": ["down", "ctrl+n"]` did nothing there, although they work in `/settings` and the `--resume` picker. `selectConfig()` now installs `KeybindingsManager.create(options.agentDir)` before it builds the UI, the same way the session picker and startup UI do. **Testing** - Added `test/config-selector-keybindings.test.ts`. It runs `selectConfig()` with a stubbed terminal and component and an agent dir whose `keybindings.json` remaps `tui.select.up` and `tui.select.down`, then checks that the active keybindings include `ctrl+p` and `ctrl+n`. It fails without the change and passes with it. - Manual check: ran `pi config` in tmux with that `keybindings.json`. Before the change, Ctrl+N did nothing. After it, Ctrl+N moves the selection like Down. - `npm run check` reports only two errors, both missing modules in `packages/schema/test/bench/candidates.ts`. They occur on `main` without this change. Fixes #11651 Requested by @badlogic Co-authored-by: triagent --- .../coding-agent/src/cli/config-selector.ts | 4 +- .../test/config-selector-keybindings.test.ts | 75 +++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 packages/coding-agent/test/config-selector-keybindings.test.ts diff --git a/packages/coding-agent/src/cli/config-selector.ts b/packages/coding-agent/src/cli/config-selector.ts index 261547acd..53c88f8da 100644 --- a/packages/coding-agent/src/cli/config-selector.ts +++ b/packages/coding-agent/src/cli/config-selector.ts @@ -2,7 +2,8 @@ * TUI config selector for `pi config` command */ -import { ProcessTerminal, type TUI, TuiMainScreen } from "@earendil-works/pi-tui"; +import { ProcessTerminal, setKeybindings, type TUI, TuiMainScreen } from "@earendil-works/pi-tui"; +import { KeybindingsManager } from "../core/keybindings.ts"; import type { SettingsManager } from "../core/settings-manager.ts"; import { ConfigSelectorComponent, type ScopedResolvedPaths } from "../modes/interactive/components/config-selector.ts"; import { initTheme, stopThemeWatcher } from "../modes/interactive/theme/theme.ts"; @@ -20,6 +21,7 @@ export interface ConfigSelectorOptions { export async function selectConfig(options: ConfigSelectorOptions): Promise { // Initialize theme before showing TUI initTheme(options.settingsManager.getTheme(), true); + setKeybindings(KeybindingsManager.create(options.agentDir)); return new Promise((resolve) => { const ui: TUI = new TuiMainScreen( diff --git a/packages/coding-agent/test/config-selector-keybindings.test.ts b/packages/coding-agent/test/config-selector-keybindings.test.ts new file mode 100644 index 000000000..b22805afa --- /dev/null +++ b/packages/coding-agent/test/config-selector-keybindings.test.ts @@ -0,0 +1,75 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { getKeybindings, setKeybindings } from "@earendil-works/pi-tui"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +vi.mock("@earendil-works/pi-tui", async (importOriginal) => { + const actual = await importOriginal(); + class FakeTui { + terminal = { rows: 24 }; + setClearOnShrink() {} + addChild() {} + setFocus() {} + start() {} + stop() {} + requestRender() {} + } + return { ...actual, ProcessTerminal: class {}, TuiMainScreen: FakeTui }; +}); + +vi.mock("../src/modes/interactive/components/config-selector.ts", () => ({ + ConfigSelectorComponent: class { + constructor(...args: unknown[]) { + const onClose = args[4] as () => void; + queueMicrotask(onClose); + } + getResourceList() { + return {}; + } + }, +})); + +vi.mock("../src/modes/interactive/theme/theme.ts", () => ({ + initTheme: () => {}, + stopThemeWatcher: () => {}, +})); + +import { selectConfig } from "../src/cli/config-selector.ts"; +import { KeybindingsManager } from "../src/core/keybindings.ts"; +import type { SettingsManager } from "../src/core/settings-manager.ts"; + +describe("selectConfig keybindings", () => { + let agentDir: string | undefined; + + afterEach(() => { + if (agentDir) rmSync(agentDir, { recursive: true, force: true }); + agentDir = undefined; + setKeybindings(new KeybindingsManager({})); + }); + + it("uses the user's keybindings.json", async () => { + agentDir = mkdtempSync(join(tmpdir(), "pi-config-keys-")); + writeFileSync( + join(agentDir, "keybindings.json"), + JSON.stringify({ "tui.select.up": ["up", "ctrl+p"], "tui.select.down": ["down", "ctrl+n"] }), + ); + const settingsManager = { + getTheme: () => undefined, + getShowHardwareCursor: () => false, + getClearOnShrink: () => false, + } as unknown as SettingsManager; + + await selectConfig({ + resolvedPaths: {} as never, + settingsManager, + cwd: agentDir, + agentDir, + writeScope: "global", + projectModeAvailable: false, + }); + + expect(getKeybindings().getKeys("tui.select.down")).toContain("ctrl+n"); + expect(getKeybindings().getKeys("tui.select.up")).toContain("ctrl+p"); + }); +});