From e5bc87b7c19740f421db9b19f17d781443f768b9 Mon Sep 17 00:00:00 2001 From: Federico Jaramillo Martinez Date: Wed, 20 May 2026 15:15:36 +0200 Subject: [PATCH] feat: add terminal navigation action shortcut --- .changeset/terminal-shortcut-action.md | 5 ++++ docs/plugins.md | 10 ++++++- src/client/src/plugins/core/actions.ts | 8 ++++++ src/client/src/plugins/registry.test.ts | 36 +++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 .changeset/terminal-shortcut-action.md diff --git a/.changeset/terminal-shortcut-action.md b/.changeset/terminal-shortcut-action.md new file mode 100644 index 0000000..3d27ac7 --- /dev/null +++ b/.changeset/terminal-shortcut-action.md @@ -0,0 +1,5 @@ +--- +"@jmfederico/pi-web": patch +--- + +Add a Go to Terminal action with a keyboard shortcut and clarify that plugin shortcuts are default keybindings attached to actions. diff --git a/docs/plugins.md b/docs/plugins.md index 609fbbc..ad04872 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -326,7 +326,15 @@ Notes: - Other `state` fields may exist at runtime, but they are Pi Web internals and can change quickly. - `enabled` is evaluated when the action palette asks for actions. - `selectWorkspaceTool()` expects a qualified panel id such as `my-plugin:workspace.info`. -- `shortcut` is displayed/handled the same way app actions are; choose shortcuts carefully to avoid conflicts. + +#### Keyboard shortcuts + +- App-level keyboard shortcuts must be attached to actions. Pi Web does not support standalone plugin keyboard commands; contribute an action first, then add a `shortcut` if it needs a keybinding. +- `shortcut` is the action's default keybinding. It is displayed in the action palette and handled by the global shortcut dispatcher when the action is enabled. +- Use modified shortcuts such as `mod+shift+p`; plain letter shortcuts are intentionally ignored so normal typing is never captured. +- Future Pi Web versions may allow users to override or disable action shortcuts by action id, so plugins should treat `shortcut` as a default rather than a guaranteed final binding. +- Choose shortcuts carefully to avoid conflicts. There is no user-facing shortcut override or conflict resolver yet. +- Local text input, terminal input, list navigation, and dialog keys such as Enter, Escape, and arrow keys do not need to be plugin actions unless they are app-level commands. ### Workspace panels diff --git a/src/client/src/plugins/core/actions.ts b/src/client/src/plugins/core/actions.ts index c0f6b4d..73f3a80 100644 --- a/src/client/src/plugins/core/actions.ts +++ b/src/client/src/plugins/core/actions.ts @@ -70,6 +70,14 @@ export function createCoreActions(): PluginAction[] { enabled: hasGitWorkspace, run: (context) => { context.selectMainView("core:workspace.git"); }, }, + { + id: "view.terminal", + title: "Go to Terminal", + shortcut: "mod+4", + group: "Navigation", + enabled: hasWorkspace, + run: (context) => { context.selectMainView("core:workspace.terminal"); }, + }, { id: "workspace.refresh-files", title: "Refresh Files", diff --git a/src/client/src/plugins/registry.test.ts b/src/client/src/plugins/registry.test.ts index f2b6222..f99b4da 100644 --- a/src/client/src/plugins/registry.test.ts +++ b/src/client/src/plugins/registry.test.ts @@ -66,7 +66,9 @@ describe("PluginRegistry", () => { const active = registry.getActions(createContext({ selectedWorkspace: testWorkspace() }).context); expect(inactive.find((action) => action.id === "core:view.files")?.enabled).toBe(false); + expect(inactive.find((action) => action.id === "core:view.terminal")?.enabled).toBe(false); expect(active.find((action) => action.id === "core:view.files")?.enabled).toBe(true); + expect(active.find((action) => action.id === "core:view.terminal")?.enabled).toBe(true); }); it("routes refresh current to the active core workspace panel", () => { @@ -83,6 +85,40 @@ describe("PluginRegistry", () => { expect(calls).toEqual(["refreshGit"]); }); + it("exposes terminal navigation as a shortcut-backed action", () => { + const registry = new PluginRegistry(); + registry.register({ id: "core", plugin: corePlugin }); + const { context, calls } = createContext({ selectedWorkspace: testWorkspace() }); + const action = registry.getActions(context).find((candidate) => candidate.id === "core:view.terminal"); + + expect(action?.shortcut).toBe("mod+4"); + if (action !== undefined) void action.run(); + + expect(calls).toEqual(["selectMainView:core:workspace.terminal"]); + }); + + it("keeps built-in keyboard shortcuts unique and action-backed", () => { + const registry = new PluginRegistry(); + registry.register({ id: "core", plugin: corePlugin }); + const shortcuts = registry.getActions(createContext({ selectedWorkspace: testWorkspace() }).context) + .filter((action) => action.shortcut !== undefined) + .map((action) => [action.id, action.shortcut]); + + expect(shortcuts).toEqual([ + ["core:actions.show", "mod+k"], + ["core:view.chat", "mod+1"], + ["core:view.files", "mod+2"], + ["core:view.git", "mod+3"], + ["core:view.terminal", "mod+4"], + ["core:workspace.refresh-files", "mod+shift+f"], + ["core:workspace.refresh-git", "mod+shift+g"], + ["core:workspace.refresh-current", "mod+shift+r"], + ["core:session.start", "mod+enter"], + ["core:session.stop", "mod+."], + ]); + expect(new Set(shortcuts.map(([, shortcut]) => shortcut)).size).toBe(shortcuts.length); + }); + it("collects built-in Pi Web themes from an in-app plugin", () => { const registry = new PluginRegistry(); registry.register({ id: "themes", plugin: themePackPlugin });