mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Switch projects from New Workspace with ⌘P/Ctrl+P (#2110)
* feat(app): add ⌘P shortcut to switch project on New Workspace screen Opens the existing project picker with its search focused so the project can be switched from the keyboard (type + Enter) instead of clicking the badge and then the project. Wires a new "workspace.project.pick" action through the standard keyboard pipeline (binding -> route passthrough -> dispatcher), handled by a screen-scoped handler on the New Workspace screen that is only registered while the screen is mounted and there are projects to pick. Because preventDefault only fires when a handler handles the key, ⌘P/Ctrl+P still triggers native print everywhere else. Adds a Settings -> Shortcuts help row (rebindable) and the "Switch project" label across all locales. * test(app): cover project picker shortcut --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
committed by
GitHub
parent
b55acfc60f
commit
f8dd0fc2e0
@@ -177,6 +177,14 @@ export async function openGlobalNewWorkspaceComposer(page: Page): Promise<void>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function openNewWorkspaceProjectPickerWithShortcut(page: Page): Promise<void> {
|
||||||
|
await page.keyboard.press("Control+P");
|
||||||
|
|
||||||
|
const searchInput = page.getByPlaceholder("Search projects");
|
||||||
|
await expect(searchInput).toBeVisible({ timeout: 30_000 });
|
||||||
|
await expect(searchInput).toBeFocused();
|
||||||
|
}
|
||||||
|
|
||||||
export async function expectNewWorkspaceProjectSelected(
|
export async function expectNewWorkspaceProjectSelected(
|
||||||
page: Page,
|
page: Page,
|
||||||
projectDisplayName: string,
|
projectDisplayName: string,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
expectNewWorkspaceProjectSelected,
|
expectNewWorkspaceProjectSelected,
|
||||||
openGlobalNewWorkspaceComposer,
|
openGlobalNewWorkspaceComposer,
|
||||||
openNewWorkspaceComposer,
|
openNewWorkspaceComposer,
|
||||||
|
openNewWorkspaceProjectPickerWithShortcut,
|
||||||
} from "./helpers/new-workspace";
|
} from "./helpers/new-workspace";
|
||||||
import { getE2EDaemonPort } from "./helpers/daemon-port";
|
import { getE2EDaemonPort } from "./helpers/daemon-port";
|
||||||
import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client";
|
import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client";
|
||||||
@@ -106,6 +107,20 @@ test.describe("New workspace entry points", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Ctrl+P opens the project picker with search focused", async ({ page }) => {
|
||||||
|
const seeded: SeededWorkspace = await seedWorkspace({ repoPrefix: "entry-shortcut-" });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await gotoAppShell(page);
|
||||||
|
await waitForSidebarHydration(page);
|
||||||
|
await openGlobalNewWorkspaceComposer(page);
|
||||||
|
|
||||||
|
await openNewWorkspaceProjectPickerWithShortcut(page);
|
||||||
|
} finally {
|
||||||
|
await seeded.cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("keeps the in-progress form when the remembered workspace is archived elsewhere", async ({
|
test("keeps the in-progress form when the remembered workspace is archived elsewhere", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@@ -1831,6 +1831,7 @@ export const ar: TranslationResources = {
|
|||||||
sendMessage: "أرسل رسالة",
|
sendMessage: "أرسل رسالة",
|
||||||
queueMessage: "رسالة قائمة الانتظار",
|
queueMessage: "رسالة قائمة الانتظار",
|
||||||
muteUnmuteVoiceMode: "كتم وضع الصوت /unmute",
|
muteUnmuteVoiceMode: "كتم وضع الصوت /unmute",
|
||||||
|
switchProject: "تبديل المشروع",
|
||||||
},
|
},
|
||||||
helpNotes: {
|
helpNotes: {
|
||||||
showKeyboardShortcuts: "متاح عندما لا يكون التركيز في حقل نص أو محطة طرفية.",
|
showKeyboardShortcuts: "متاح عندما لا يكون التركيز في حقل نص أو محطة طرفية.",
|
||||||
|
|||||||
@@ -1841,6 +1841,7 @@ export const en = {
|
|||||||
sendMessage: "Send message",
|
sendMessage: "Send message",
|
||||||
queueMessage: "Queue message",
|
queueMessage: "Queue message",
|
||||||
muteUnmuteVoiceMode: "Mute/unmute voice mode",
|
muteUnmuteVoiceMode: "Mute/unmute voice mode",
|
||||||
|
switchProject: "Switch project",
|
||||||
},
|
},
|
||||||
helpNotes: {
|
helpNotes: {
|
||||||
showKeyboardShortcuts: "Available when focus is not in a text field or terminal.",
|
showKeyboardShortcuts: "Available when focus is not in a text field or terminal.",
|
||||||
|
|||||||
@@ -1879,6 +1879,7 @@ export const es: TranslationResources = {
|
|||||||
sendMessage: "enviar mensaje",
|
sendMessage: "enviar mensaje",
|
||||||
queueMessage: "mensaje de cola",
|
queueMessage: "mensaje de cola",
|
||||||
muteUnmuteVoiceMode: "Silenciar el modo de voz/unmute",
|
muteUnmuteVoiceMode: "Silenciar el modo de voz/unmute",
|
||||||
|
switchProject: "Cambiar proyecto",
|
||||||
},
|
},
|
||||||
helpNotes: {
|
helpNotes: {
|
||||||
showKeyboardShortcuts: "Disponible cuando el foco no está en un campo de texto o terminal.",
|
showKeyboardShortcuts: "Disponible cuando el foco no está en un campo de texto o terminal.",
|
||||||
|
|||||||
@@ -1881,6 +1881,7 @@ export const fr: TranslationResources = {
|
|||||||
sendMessage: "Envoyer un message",
|
sendMessage: "Envoyer un message",
|
||||||
queueMessage: "Message de file d'attente",
|
queueMessage: "Message de file d'attente",
|
||||||
muteUnmuteVoiceMode: "Mode vocal/unmutemuet",
|
muteUnmuteVoiceMode: "Mode vocal/unmutemuet",
|
||||||
|
switchProject: "Changer de projet",
|
||||||
},
|
},
|
||||||
helpNotes: {
|
helpNotes: {
|
||||||
showKeyboardShortcuts:
|
showKeyboardShortcuts:
|
||||||
|
|||||||
@@ -1847,6 +1847,7 @@ export const ja: TranslationResources = {
|
|||||||
sendMessage: "メッセージを送信",
|
sendMessage: "メッセージを送信",
|
||||||
queueMessage: "メッセージをキューに追加",
|
queueMessage: "メッセージをキューに追加",
|
||||||
muteUnmuteVoiceMode: "音声モードのミュートを切り替え",
|
muteUnmuteVoiceMode: "音声モードのミュートを切り替え",
|
||||||
|
switchProject: "プロジェクトを切り替え",
|
||||||
},
|
},
|
||||||
helpNotes: {
|
helpNotes: {
|
||||||
showKeyboardShortcuts:
|
showKeyboardShortcuts:
|
||||||
|
|||||||
@@ -1862,6 +1862,7 @@ export const ptBR: TranslationResources = {
|
|||||||
sendMessage: "Enviar mensagem",
|
sendMessage: "Enviar mensagem",
|
||||||
queueMessage: "Enfileirar mensagem",
|
queueMessage: "Enfileirar mensagem",
|
||||||
muteUnmuteVoiceMode: "Silenciar/ativar modo de voz",
|
muteUnmuteVoiceMode: "Silenciar/ativar modo de voz",
|
||||||
|
switchProject: "Trocar projeto",
|
||||||
},
|
},
|
||||||
helpNotes: {
|
helpNotes: {
|
||||||
showKeyboardShortcuts:
|
showKeyboardShortcuts:
|
||||||
|
|||||||
@@ -1869,6 +1869,7 @@ export const ru: TranslationResources = {
|
|||||||
sendMessage: "Отправить сообщение",
|
sendMessage: "Отправить сообщение",
|
||||||
queueMessage: "Сообщение в очереди",
|
queueMessage: "Сообщение в очереди",
|
||||||
muteUnmuteVoiceMode: "Отключить голосовой режим /unmute",
|
muteUnmuteVoiceMode: "Отключить голосовой режим /unmute",
|
||||||
|
switchProject: "Сменить проект",
|
||||||
},
|
},
|
||||||
helpNotes: {
|
helpNotes: {
|
||||||
showKeyboardShortcuts: "Доступно, когда фокус находится не в текстовом поле или терминале.",
|
showKeyboardShortcuts: "Доступно, когда фокус находится не в текстовом поле или терминале.",
|
||||||
|
|||||||
@@ -1810,6 +1810,7 @@ export const zhCN: TranslationResources = {
|
|||||||
sendMessage: "发送消息",
|
sendMessage: "发送消息",
|
||||||
queueMessage: "消息排队",
|
queueMessage: "消息排队",
|
||||||
muteUnmuteVoiceMode: "静音/取消静音语音模式",
|
muteUnmuteVoiceMode: "静音/取消静音语音模式",
|
||||||
|
switchProject: "切换项目",
|
||||||
},
|
},
|
||||||
helpNotes: {
|
helpNotes: {
|
||||||
showKeyboardShortcuts: "焦点不在文本输入框或终端内时可用。",
|
showKeyboardShortcuts: "焦点不在文本输入框或终端内时可用。",
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export type KeyboardActionId =
|
|||||||
| "shortcuts.dialog.toggle"
|
| "shortcuts.dialog.toggle"
|
||||||
| "workspace.terminal.new"
|
| "workspace.terminal.new"
|
||||||
| "workspace.new"
|
| "workspace.new"
|
||||||
|
| "workspace.project.pick"
|
||||||
| "worktree.new"
|
| "worktree.new"
|
||||||
| "workspace.archive"
|
| "workspace.archive"
|
||||||
| "workspace.pin"
|
| "workspace.pin"
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export type KeyboardActionId =
|
|||||||
| "workspace.terminal.new"
|
| "workspace.terminal.new"
|
||||||
| "sidebar.toggle.right"
|
| "sidebar.toggle.right"
|
||||||
| "workspace.new"
|
| "workspace.new"
|
||||||
|
| "workspace.project.pick"
|
||||||
| "worktree.new"
|
| "worktree.new"
|
||||||
| "workspace.archive"
|
| "workspace.archive"
|
||||||
| "workspace.pin";
|
| "workspace.pin";
|
||||||
@@ -62,6 +63,7 @@ export type KeyboardActionDefinition =
|
|||||||
| { id: "workspace.terminal.new"; scope: KeyboardActionScope }
|
| { id: "workspace.terminal.new"; scope: KeyboardActionScope }
|
||||||
| { id: "sidebar.toggle.right"; scope: KeyboardActionScope }
|
| { id: "sidebar.toggle.right"; scope: KeyboardActionScope }
|
||||||
| { id: "workspace.new"; scope: KeyboardActionScope }
|
| { id: "workspace.new"; scope: KeyboardActionScope }
|
||||||
|
| { id: "workspace.project.pick"; scope: KeyboardActionScope }
|
||||||
| { id: "worktree.new"; scope: KeyboardActionScope }
|
| { id: "worktree.new"; scope: KeyboardActionScope }
|
||||||
| { id: "workspace.archive"; scope: KeyboardActionScope }
|
| { id: "workspace.archive"; scope: KeyboardActionScope }
|
||||||
| { id: "workspace.pin"; scope: KeyboardActionScope };
|
| { id: "workspace.pin"; scope: KeyboardActionScope };
|
||||||
|
|||||||
@@ -142,6 +142,18 @@ describe("keyboard-shortcuts", () => {
|
|||||||
context: { isMac: false, commandCenterOpen: false, focusScope: "other" },
|
context: { isMac: false, commandCenterOpen: false, focusScope: "other" },
|
||||||
action: "workspace.new",
|
action: "workspace.new",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "matches Cmd+P to switch project on mac",
|
||||||
|
event: { key: "p", code: "KeyP", metaKey: true },
|
||||||
|
context: { isMac: true, commandCenterOpen: false },
|
||||||
|
action: "workspace.project.pick",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "matches Ctrl+P to switch project on non-mac",
|
||||||
|
event: { key: "p", code: "KeyP", ctrlKey: true },
|
||||||
|
context: { isMac: false, commandCenterOpen: false, focusScope: "other" },
|
||||||
|
action: "workspace.project.pick",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "matches question-mark shortcut to toggle the shortcuts dialog",
|
name: "matches question-mark shortcut to toggle the shortcuts dialog",
|
||||||
event: { key: "?", code: "Slash", shiftKey: true },
|
event: { key: "?", code: "Slash", shiftKey: true },
|
||||||
@@ -410,6 +422,16 @@ describe("keyboard-shortcuts", () => {
|
|||||||
event: { key: "?", code: "Slash", shiftKey: true },
|
event: { key: "?", code: "Slash", shiftKey: true },
|
||||||
context: { focusScope: "message-input" },
|
context: { focusScope: "message-input" },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "does not switch project with Ctrl+P on non-mac while terminal is focused",
|
||||||
|
event: { key: "p", code: "KeyP", ctrlKey: true },
|
||||||
|
context: { isMac: false, focusScope: "terminal" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "does not switch project with Cmd+P while the command center is open",
|
||||||
|
event: { key: "p", code: "KeyP", metaKey: true },
|
||||||
|
context: { isMac: true, commandCenterOpen: true },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "does not close tab with Ctrl+W on mac desktop (Cmd+W only)",
|
name: "does not close tab with Ctrl+W on mac desktop (Cmd+W only)",
|
||||||
event: { key: "w", code: "KeyW", ctrlKey: true },
|
event: { key: "w", code: "KeyW", ctrlKey: true },
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ const SHORTCUT_HELP_SECTION_LABEL_KEYS: Record<ShortcutSectionId, string> = {
|
|||||||
const SHORTCUT_HELP_LABEL_KEYS: Record<string, string> = {
|
const SHORTCUT_HELP_LABEL_KEYS: Record<string, string> = {
|
||||||
"new-agent": "settings.shortcuts.help.openProject",
|
"new-agent": "settings.shortcuts.help.openProject",
|
||||||
"new-workspace": "settings.shortcuts.help.newWorkspace",
|
"new-workspace": "settings.shortcuts.help.newWorkspace",
|
||||||
|
"switch-project": "settings.shortcuts.help.switchProject",
|
||||||
"archive-workspace": "settings.shortcuts.help.archiveWorkspace",
|
"archive-workspace": "settings.shortcuts.help.archiveWorkspace",
|
||||||
"workspace-tab-new": "settings.shortcuts.help.newTab",
|
"workspace-tab-new": "settings.shortcuts.help.newTab",
|
||||||
"workspace-tab-close-current": "settings.shortcuts.help.closeCurrentTab",
|
"workspace-tab-close-current": "settings.shortcuts.help.closeCurrentTab",
|
||||||
@@ -231,6 +232,32 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// --- Switch project (New Workspace screen) ---
|
||||||
|
{
|
||||||
|
id: "workspace-project-pick-cmd-p-mac",
|
||||||
|
action: "workspace.project.pick",
|
||||||
|
combo: "Cmd+P",
|
||||||
|
when: { mac: true, commandCenter: false },
|
||||||
|
help: {
|
||||||
|
id: "switch-project",
|
||||||
|
section: "projects",
|
||||||
|
label: "Switch project",
|
||||||
|
keys: ["mod", "P"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "workspace-project-pick-ctrl-p-non-mac",
|
||||||
|
action: "workspace.project.pick",
|
||||||
|
combo: "Ctrl+P",
|
||||||
|
when: { mac: false, commandCenter: false, terminal: false },
|
||||||
|
help: {
|
||||||
|
id: "switch-project",
|
||||||
|
section: "projects",
|
||||||
|
label: "Switch project",
|
||||||
|
keys: ["mod", "P"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
// --- Archive workspace ---
|
// --- Archive workspace ---
|
||||||
{
|
{
|
||||||
// COMPAT(workspaceArchiveShortcutOverride): added in v0.1.106; remove after
|
// COMPAT(workspaceArchiveShortcutOverride): added in v0.1.106; remove after
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ describe("routeKeyboardShortcut — dispatch passthroughs", () => {
|
|||||||
["agent.interrupt", { id: "agent.interrupt", scope: "global" }],
|
["agent.interrupt", { id: "agent.interrupt", scope: "global" }],
|
||||||
["workspace.tab.new", { id: "workspace.tab.new", scope: "workspace" }],
|
["workspace.tab.new", { id: "workspace.tab.new", scope: "workspace" }],
|
||||||
["workspace.new", { id: "workspace.new", scope: "sidebar" }],
|
["workspace.new", { id: "workspace.new", scope: "sidebar" }],
|
||||||
|
["workspace.project.pick", { id: "workspace.project.pick", scope: "workspace" }],
|
||||||
["workspace.archive", { id: "workspace.archive", scope: "sidebar" }],
|
["workspace.archive", { id: "workspace.archive", scope: "sidebar" }],
|
||||||
["workspace.pin", { id: "workspace.pin", scope: "sidebar" }],
|
["workspace.pin", { id: "workspace.pin", scope: "sidebar" }],
|
||||||
["worktree.new", { id: "worktree.new", scope: "sidebar" }],
|
["worktree.new", { id: "worktree.new", scope: "sidebar" }],
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ const PASSTHROUGH_DISPATCH: Record<string, KeyboardActionDefinition> = {
|
|||||||
"agent.interrupt": { id: "agent.interrupt", scope: "global" },
|
"agent.interrupt": { id: "agent.interrupt", scope: "global" },
|
||||||
"workspace.tab.new": { id: "workspace.tab.new", scope: "workspace" },
|
"workspace.tab.new": { id: "workspace.tab.new", scope: "workspace" },
|
||||||
"workspace.new": { id: "workspace.new", scope: "sidebar" },
|
"workspace.new": { id: "workspace.new", scope: "sidebar" },
|
||||||
|
"workspace.project.pick": { id: "workspace.project.pick", scope: "workspace" },
|
||||||
"workspace.archive": { id: "workspace.archive", scope: "sidebar" },
|
"workspace.archive": { id: "workspace.archive", scope: "sidebar" },
|
||||||
"workspace.pin": { id: "workspace.pin", scope: "sidebar" },
|
"workspace.pin": { id: "workspace.pin", scope: "sidebar" },
|
||||||
"worktree.new": { id: "worktree.new", scope: "sidebar" },
|
"worktree.new": { id: "worktree.new", scope: "sidebar" },
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ import {
|
|||||||
type PendingWorkspaceDraftSetup,
|
type PendingWorkspaceDraftSetup,
|
||||||
} from "@/stores/workspace-draft-submission-store";
|
} from "@/stores/workspace-draft-submission-store";
|
||||||
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
|
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
|
||||||
|
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
|
||||||
|
import type { KeyboardActionId } from "@/keyboard/keyboard-action-dispatcher";
|
||||||
import { useFormPreferences } from "@/hooks/use-form-preferences";
|
import { useFormPreferences } from "@/hooks/use-form-preferences";
|
||||||
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
||||||
import { getForgePresentation } from "@/git/forge";
|
import { getForgePresentation } from "@/git/forge";
|
||||||
@@ -181,6 +183,8 @@ interface PickerOptionData {
|
|||||||
const BRANCH_OPTION_PREFIX = "branch:";
|
const BRANCH_OPTION_PREFIX = "branch:";
|
||||||
const PR_OPTION_PREFIX = "github-pr:";
|
const PR_OPTION_PREFIX = "github-pr:";
|
||||||
const PROJECT_ICON_FALLBACK_FONT_SIZE = 10;
|
const PROJECT_ICON_FALLBACK_FONT_SIZE = 10;
|
||||||
|
// Stable reference so the keyboard-action handler doesn't re-register each render.
|
||||||
|
const PROJECT_PICK_ACTIONS: readonly KeyboardActionId[] = ["workspace.project.pick"];
|
||||||
// Height of a single picker-trigger badge. The Base-row spacer reserves exactly
|
// Height of a single picker-trigger badge. The Base-row spacer reserves exactly
|
||||||
// this so toggling Isolation to Local hides the row without shifting the form.
|
// this so toggling Isolation to Local hides the row without shifting the form.
|
||||||
const BADGE_HEIGHT = 28;
|
const BADGE_HEIGHT = 28;
|
||||||
@@ -1788,6 +1792,22 @@ export function NewWorkspaceScreen({
|
|||||||
setProjectPickerOpen(true);
|
setProjectPickerOpen(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Cmd/Ctrl+P opens the project picker with its search focused so the user can
|
||||||
|
// switch projects from the keyboard. Registered only while this screen is
|
||||||
|
// mounted, so the shortcut doesn't swallow the browser's native print
|
||||||
|
// elsewhere; gated on having projects to pick.
|
||||||
|
const handleProjectPick = useCallback(() => {
|
||||||
|
openProjectPicker();
|
||||||
|
return true;
|
||||||
|
}, [openProjectPicker]);
|
||||||
|
useKeyboardActionHandler({
|
||||||
|
handlerId: "new-workspace-project-pick",
|
||||||
|
actions: PROJECT_PICK_ACTIONS,
|
||||||
|
enabled: projectPickerOptions.length > 0,
|
||||||
|
priority: 0,
|
||||||
|
handle: handleProjectPick,
|
||||||
|
});
|
||||||
|
|
||||||
const openIsolationPicker = useCallback(() => {
|
const openIsolationPicker = useCallback(() => {
|
||||||
setIsolationPickerOpen(true);
|
setIsolationPickerOpen(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
Reference in New Issue
Block a user