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 <triagent@localhost>
This commit is contained in:
robopipi[bot] 2026-11-04 09:59:49 +01:00 • committed by GitHub
parent 081a6e87f6
commit b0c64f33c8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 78 additions and 1 deletions

View file

@ -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<void> {
// Initialize theme before showing TUI
initTheme(options.settingsManager.getTheme(), true);
setKeybindings(KeybindingsManager.create(options.agentDir));
return new Promise((resolve) => {
const ui: TUI = new TuiMainScreen(

View file

@ -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<typeof import("@earendil-works/pi-tui")>();
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");
});
});