From 1de1d02f571ebbbbcd004eae0a628b6d8e265bf3 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 28 Oct 2026 04:24:05 +0100 Subject: [PATCH] feat: list plugin pages in the command palette's Go to commands --- .../__tests__/command-palette.test.tsx | 12 ++++++++++++ components/command-palette/command-palette.tsx | 15 ++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/components/command-palette/__tests__/command-palette.test.tsx b/components/command-palette/__tests__/command-palette.test.tsx index e7e2d2b49..274da3805 100644 --- a/components/command-palette/__tests__/command-palette.test.tsx +++ b/components/command-palette/__tests__/command-palette.test.tsx @@ -211,3 +211,15 @@ describe('command palette on a phone', () => { expect(screen.getByRole('option', { name: 'compose' })).toBeInTheDocument(); }); }); + +describe('command palette and plugin pages', () => { + it('goes to a plugin page like to the built-in apps', async () => { + const pages = await import('@/lib/plugin-sandbox/pages'); + act(() => pages.setPluginPages('kanban', 'Kanban', [{ id: 'board', title: 'Board' }])); + render(); + pressCtrlK(); + act(() => { fireEvent.click(screen.getByRole('option', { name: /go_to\(Board\)/ })); }); + expect(nav.push).toHaveBeenCalledWith('/apps/kanban/board'); + pages.resetPages(); + }); +}); diff --git a/components/command-palette/command-palette.tsx b/components/command-palette/command-palette.tsx index 298b9de77..9cd32331d 100644 --- a/components/command-palette/command-palette.tsx +++ b/components/command-palette/command-palette.tsx @@ -28,6 +28,7 @@ import { useCalendarStore } from "@/stores/calendar-store"; import { usePolicyStore } from "@/stores/policy-store"; import { useThemeStore } from "@/stores/theme-store"; import { usePluginActions } from "@/components/plugins/plugin-actions"; +import { usePluginPages, pluginPageHref } from "@/hooks/use-plugin-pages"; import { KeyboardShortcutsModal } from "@/components/keyboard-shortcuts-modal"; import { GlobalSearchPalette } from "@/components/global-search/global-search-palette"; import { isContextMenuOpen } from "@/components/ui/context-menu"; @@ -151,6 +152,7 @@ function CommandPalette({ onClose, onShowShortcuts }: { onClose: () => void; onS const colorModeLocked = usePolicyStore((s) => s.getColorModePolicy().locked); const resolvedTheme = useThemeStore((s) => s.resolvedTheme); const pluginActions = usePluginActions(surface); + const pluginPages = usePluginPages(); const registeredGroups = useRegisteredCommandGroups(); const accounts = useAccountStore((s) => s.accounts); const activeAccountId = useAuthStore((s) => s.activeAccountId); @@ -193,6 +195,17 @@ function CommandPalette({ onClose, onShowShortcuts }: { onClose: () => void; onS if ((client?.supportsFiles?.() ?? false) && isFeatureEnabled("filesEnabled")) { list.push({ id: "go-files", group: navigation, icon: Folder, label: t("go_to", { name: tSidebar("files") }), run: go("/files") }); } + // Plugin pages sit next to the built-in apps in the rail and the tab bar. + for (const page of pluginPages) { + list.push({ + id: `go-page:${page.pluginId}:${page.id}`, + group: navigation, + icon: iconForName(page.icon) ?? Puzzle, + label: t("go_to", { name: page.title }), + source: page.pluginName, + run: go(pluginPageHref(page)), + }); + } list.push( { id: "go-settings", group: navigation, icon: Settings, label: t("go_to", { name: tSidebar("settings") }), run: go("/settings") }, { id: "compose", group: actions, icon: PenSquare, label: t("compose"), shortcut: "c", run: onMail("compose") }, @@ -244,7 +257,7 @@ function CommandPalette({ onClose, onShowShortcuts }: { onClose: () => void; onS }); } return list; - }, [client, supportsCalendar, isFeatureEnabled, colorModeLocked, resolvedTheme, pluginActions, registeredGroups, accounts, activeAccountId, router, t, tSidebar, tGlobalSearch, onShowShortcuts, touch]); + }, [client, supportsCalendar, isFeatureEnabled, colorModeLocked, resolvedTheme, pluginActions, pluginPages, registeredGroups, accounts, activeAccountId, router, t, tSidebar, tGlobalSearch, onShowShortcuts, touch]); const needle = query.trim(); const visible = needle