fix(keybindings): migrate to namespaced ids closes #2391
This commit is contained in:
@@ -10,7 +10,7 @@ import { AuthStorage } from "../src/core/auth-storage.js";
|
||||
import { createExtensionRuntime, discoverAndLoadExtensions } from "../src/core/extensions/loader.js";
|
||||
import { ExtensionRunner } from "../src/core/extensions/runner.js";
|
||||
import type { ExtensionActions, ExtensionContextActions, ProviderConfig } from "../src/core/extensions/types.js";
|
||||
import { DEFAULT_KEYBINDINGS, type KeyId } from "../src/core/keybindings.js";
|
||||
import { KeybindingsManager, type KeyId } from "../src/core/keybindings.js";
|
||||
import { ModelRegistry } from "../src/core/model-registry.js";
|
||||
import { SessionManager } from "../src/core/session-manager.js";
|
||||
|
||||
@@ -19,6 +19,7 @@ describe("ExtensionRunner", () => {
|
||||
let extensionsDir: string;
|
||||
let sessionManager: SessionManager;
|
||||
let modelRegistry: ModelRegistry;
|
||||
const defaultKeybindings = new KeybindingsManager().getEffectiveConfig();
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-runner-test-"));
|
||||
@@ -94,7 +95,7 @@ describe("ExtensionRunner", () => {
|
||||
|
||||
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
|
||||
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
|
||||
const shortcuts = runner.getShortcuts(DEFAULT_KEYBINDINGS);
|
||||
const shortcuts = runner.getShortcuts(defaultKeybindings);
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("conflicts with built-in"));
|
||||
expect(shortcuts.has("ctrl+c")).toBe(false);
|
||||
@@ -117,7 +118,7 @@ describe("ExtensionRunner", () => {
|
||||
|
||||
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
|
||||
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
|
||||
const keybindings = { ...DEFAULT_KEYBINDINGS, cycleModelForward: "ctrl+n" as KeyId };
|
||||
const keybindings = { ...defaultKeybindings, "app.model.cycleForward": "ctrl+n" as KeyId };
|
||||
const shortcuts = runner.getShortcuts(keybindings);
|
||||
|
||||
expect(shortcuts.has("ctrl+p")).toBe(true);
|
||||
@@ -127,9 +128,9 @@ describe("ExtensionRunner", () => {
|
||||
});
|
||||
|
||||
it("warns but allows when extension uses non-reserved built-in shortcut", async () => {
|
||||
const pasteImageKey = Array.isArray(DEFAULT_KEYBINDINGS.pasteImage)
|
||||
? (DEFAULT_KEYBINDINGS.pasteImage[0] ?? "")
|
||||
: DEFAULT_KEYBINDINGS.pasteImage;
|
||||
const pasteImageKey = Array.isArray(defaultKeybindings["app.clipboard.pasteImage"])
|
||||
? (defaultKeybindings["app.clipboard.pasteImage"][0] ?? "")
|
||||
: defaultKeybindings["app.clipboard.pasteImage"];
|
||||
const extCode = `
|
||||
export default function(pi) {
|
||||
pi.registerShortcut("${pasteImageKey}", {
|
||||
@@ -144,9 +145,11 @@ describe("ExtensionRunner", () => {
|
||||
|
||||
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
|
||||
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
|
||||
const shortcuts = runner.getShortcuts(DEFAULT_KEYBINDINGS);
|
||||
const shortcuts = runner.getShortcuts(defaultKeybindings);
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("built-in shortcut for pasteImage"));
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("built-in shortcut for app.clipboard.pasteImage"),
|
||||
);
|
||||
expect(shortcuts.has(pasteImageKey as KeyId)).toBe(true);
|
||||
|
||||
warnSpy.mockRestore();
|
||||
@@ -167,7 +170,7 @@ describe("ExtensionRunner", () => {
|
||||
|
||||
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
|
||||
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
|
||||
const keybindings = { ...DEFAULT_KEYBINDINGS, interrupt: "ctrl+x" as KeyId };
|
||||
const keybindings = { ...defaultKeybindings, "app.interrupt": "ctrl+x" as KeyId };
|
||||
const shortcuts = runner.getShortcuts(keybindings);
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("conflicts with built-in"));
|
||||
@@ -191,7 +194,7 @@ describe("ExtensionRunner", () => {
|
||||
|
||||
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
|
||||
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
|
||||
const keybindings = { ...DEFAULT_KEYBINDINGS, clear: ["ctrl+x", "ctrl+y"] as KeyId[] };
|
||||
const keybindings = { ...defaultKeybindings, "app.clear": ["ctrl+x", "ctrl+y"] as KeyId[] };
|
||||
const shortcuts = runner.getShortcuts(keybindings);
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("conflicts with built-in"));
|
||||
@@ -215,10 +218,12 @@ describe("ExtensionRunner", () => {
|
||||
|
||||
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
|
||||
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
|
||||
const keybindings = { ...DEFAULT_KEYBINDINGS, pasteImage: ["ctrl+x", "ctrl+y"] as KeyId[] };
|
||||
const keybindings = { ...defaultKeybindings, "app.clipboard.pasteImage": ["ctrl+x", "ctrl+y"] as KeyId[] };
|
||||
const shortcuts = runner.getShortcuts(keybindings);
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("built-in shortcut for pasteImage"));
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("built-in shortcut for app.clipboard.pasteImage"),
|
||||
);
|
||||
expect(shortcuts.has("ctrl+y")).toBe(true);
|
||||
|
||||
warnSpy.mockRestore();
|
||||
@@ -249,7 +254,7 @@ describe("ExtensionRunner", () => {
|
||||
|
||||
const result = await discoverAndLoadExtensions([], tempDir, tempDir);
|
||||
const runner = new ExtensionRunner(result.extensions, result.runtime, tempDir, sessionManager, modelRegistry);
|
||||
const shortcuts = runner.getShortcuts(DEFAULT_KEYBINDINGS);
|
||||
const shortcuts = runner.getShortcuts(defaultKeybindings);
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("shortcut conflict"));
|
||||
// Last one wins
|
||||
|
||||
74
packages/coding-agent/test/keybindings-migration.test.ts
Normal file
74
packages/coding-agent/test/keybindings-migration.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { KeybindingsManager, migrateKeybindingsConfigFile } from "../src/core/keybindings.js";
|
||||
|
||||
describe("keybindings migration", () => {
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
function createAgentDir(config: Record<string, unknown>): string {
|
||||
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-keybindings-test-"));
|
||||
tempDirs.push(agentDir);
|
||||
fs.writeFileSync(path.join(agentDir, "keybindings.json"), `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
||||
return agentDir;
|
||||
}
|
||||
|
||||
it("rewrites old key names to namespaced ids", () => {
|
||||
const agentDir = createAgentDir({
|
||||
cursorUp: ["up", "ctrl+p"],
|
||||
expandTools: "ctrl+x",
|
||||
});
|
||||
|
||||
expect(migrateKeybindingsConfigFile(agentDir)).toBe(true);
|
||||
|
||||
const migrated = JSON.parse(fs.readFileSync(path.join(agentDir, "keybindings.json"), "utf-8")) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(migrated).toEqual({
|
||||
"tui.editor.cursorUp": ["up", "ctrl+p"],
|
||||
"app.tools.expand": "ctrl+x",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the namespaced value when old and new names both exist", () => {
|
||||
const agentDir = createAgentDir({
|
||||
expandTools: "ctrl+x",
|
||||
"app.tools.expand": "ctrl+y",
|
||||
});
|
||||
|
||||
expect(migrateKeybindingsConfigFile(agentDir)).toBe(true);
|
||||
|
||||
const migrated = JSON.parse(fs.readFileSync(path.join(agentDir, "keybindings.json"), "utf-8")) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expect(migrated).toEqual({
|
||||
"app.tools.expand": "ctrl+y",
|
||||
});
|
||||
});
|
||||
|
||||
it("loads old key names in memory before the file is rewritten", () => {
|
||||
const agentDir = createAgentDir({
|
||||
selectConfirm: "enter",
|
||||
interrupt: "ctrl+x",
|
||||
});
|
||||
|
||||
const keybindings = KeybindingsManager.create(agentDir);
|
||||
|
||||
expect(keybindings.getUserBindings()).toEqual({
|
||||
"tui.select.confirm": "enter",
|
||||
"app.interrupt": "ctrl+x",
|
||||
});
|
||||
const effective = keybindings.getEffectiveConfig();
|
||||
expect(effective["tui.select.confirm"]).toBe("enter");
|
||||
expect(effective["app.interrupt"]).toBe("ctrl+x");
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DEFAULT_EDITOR_KEYBINDINGS, EditorKeybindingsManager, setEditorKeybindings } from "@mariozechner/pi-tui";
|
||||
import { setKeybindings } from "@mariozechner/pi-tui";
|
||||
import { beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { KeybindingsManager } from "../src/core/keybindings.js";
|
||||
import type { SessionInfo } from "../src/core/session-manager.js";
|
||||
@@ -45,11 +45,11 @@ const CTRL_D = "\x04";
|
||||
const CTRL_BACKSPACE = "\x1b[127;5u";
|
||||
|
||||
describe("session selector path/delete interactions", () => {
|
||||
const keybindings = KeybindingsManager.inMemory();
|
||||
const keybindings = new KeybindingsManager();
|
||||
|
||||
beforeEach(() => {
|
||||
// Ensure test isolation: editor keybindings are a global singleton
|
||||
setEditorKeybindings(new EditorKeybindingsManager(DEFAULT_EDITOR_KEYBINDINGS));
|
||||
// Ensure test isolation: keybindings are a global singleton
|
||||
setKeybindings(new KeybindingsManager());
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DEFAULT_EDITOR_KEYBINDINGS, EditorKeybindingsManager, setEditorKeybindings } from "@mariozechner/pi-tui";
|
||||
import { setKeybindings } from "@mariozechner/pi-tui";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { KeybindingsManager } from "../src/core/keybindings.js";
|
||||
import type { SessionInfo } from "../src/core/session-manager.js";
|
||||
@@ -34,13 +34,13 @@ describe("session selector rename", () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Ensure test isolation: editor keybindings are a global singleton
|
||||
setEditorKeybindings(new EditorKeybindingsManager(DEFAULT_EDITOR_KEYBINDINGS));
|
||||
// Ensure test isolation: keybindings are a global singleton
|
||||
setKeybindings(new KeybindingsManager());
|
||||
});
|
||||
|
||||
it("shows rename hint in interactive /resume picker configuration", async () => {
|
||||
const sessions = [makeSession({ id: "a" })];
|
||||
const keybindings = KeybindingsManager.inMemory();
|
||||
const keybindings = new KeybindingsManager();
|
||||
const selector = new SessionSelectorComponent(
|
||||
async () => sessions,
|
||||
async () => [],
|
||||
@@ -59,7 +59,7 @@ describe("session selector rename", () => {
|
||||
|
||||
it("does not show rename hint in --resume picker configuration", async () => {
|
||||
const sessions = [makeSession({ id: "a" })];
|
||||
const keybindings = KeybindingsManager.inMemory();
|
||||
const keybindings = new KeybindingsManager();
|
||||
const selector = new SessionSelectorComponent(
|
||||
async () => sessions,
|
||||
async () => [],
|
||||
@@ -80,7 +80,7 @@ describe("session selector rename", () => {
|
||||
const sessions = [makeSession({ id: "a", name: "Old" })];
|
||||
const renameSession = vi.fn(async () => {});
|
||||
|
||||
const keybindings = KeybindingsManager.inMemory();
|
||||
const keybindings = new KeybindingsManager();
|
||||
const selector = new SessionSelectorComponent(
|
||||
async () => sessions,
|
||||
async () => [],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DEFAULT_EDITOR_KEYBINDINGS, EditorKeybindingsManager, setEditorKeybindings } from "@mariozechner/pi-tui";
|
||||
import { setKeybindings } from "@mariozechner/pi-tui";
|
||||
import { beforeAll, beforeEach, describe, expect, test } from "vitest";
|
||||
import { KeybindingsManager } from "../src/core/keybindings.js";
|
||||
import type {
|
||||
ModelChangeEntry,
|
||||
SessionEntry,
|
||||
@@ -14,8 +15,8 @@ beforeAll(() => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Ensure test isolation: editor keybindings are a global singleton
|
||||
setEditorKeybindings(new EditorKeybindingsManager(DEFAULT_EDITOR_KEYBINDINGS));
|
||||
// Ensure test isolation: keybindings are a global singleton
|
||||
setKeybindings(new KeybindingsManager());
|
||||
});
|
||||
|
||||
// Helper to create a user message entry
|
||||
|
||||
Reference in New Issue
Block a user