From 61654f816524f763832004a1faab0b426586dd85 Mon Sep 17 00:00:00 2001 From: Don Kendall Date: Fri, 14 Aug 2026 11:33:52 -0400 Subject: [PATCH] fix(web): stop the command palette re-filtering server search results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The palette asks the search API for results and then discards the ones whose match it cannot see. Both the Power-K modal and the top-nav palette pass cmdk a filter that keeps an item only when the raw query is a contiguous substring of the item's value, and that value is built from the item's title, project identifier and sequence id. The search endpoint does not match on those fields alone. It also matches a work item by `sequence_id` against any number in the query, and such an item's title need not contain the query at all — so the filter drops it. Reproduction on a stock instance: search ` `, for example `level 3 rate` where some work item is numbered 3. The API responds with that work item, and the palette renders nothing. The network tab shows results arriving while the UI says there are none. The same applies to any future match on a field the palette does not render, which is the more general defect: the server has already decided the match, against columns the client cannot inspect, so re-deciding it on the title can only ever discard correct results. Static commands do need client-side filtering, so this keeps it for them and passes server-driven results through untouched. Results carry a `server-result:` value prefix, following the escape hatch the existing `no-results` sentinel already established. The filter was also duplicated verbatim between the two palettes, so it is extracted to one `powerKCommandFilter` rather than fixed twice. --- .../components/navigation/top-nav-power-k.tsx | 7 ++--- .../components/power-k/ui/modal/filter.ts | 28 +++++++++++++++++++ .../power-k/ui/modal/search-results.tsx | 3 +- .../components/power-k/ui/modal/wrapper.tsx | 7 ++--- 4 files changed, 34 insertions(+), 11 deletions(-) create mode 100644 apps/web/core/components/power-k/ui/modal/filter.ts diff --git a/apps/web/core/components/navigation/top-nav-power-k.tsx b/apps/web/core/components/navigation/top-nav-power-k.tsx index 22825d4b7e3..af3e1d2651c 100644 --- a/apps/web/core/components/navigation/top-nav-power-k.tsx +++ b/apps/web/core/components/navigation/top-nav-power-k.tsx @@ -14,6 +14,7 @@ import { cn } from "@plane/utils"; // power-k import type { TPowerKCommandConfig, TPowerKContext } from "@/components/power-k/core/types"; import { ProjectsAppPowerKCommandsList } from "@/components/power-k/ui/modal/commands-list"; +import { powerKCommandFilter } from "@/components/power-k/ui/modal/filter"; import { PowerKModalFooter } from "@/components/power-k/ui/modal/footer"; import { useIssueDetail } from "@/hooks/store/use-issue-detail"; import { usePowerK } from "@/hooks/store/use-power-k"; @@ -256,11 +257,7 @@ export const TopNavPowerK = observer(() => { > {isOpen && ( { - if (i18nValue === "no-results") return 1; - if (i18nValue.toLowerCase().includes(search.toLowerCase())) return 1; - return 0; - }} + filter={powerKCommandFilter} shouldFilter={searchTerm.length > 0} className="flex h-full w-full flex-col" > diff --git a/apps/web/core/components/power-k/ui/modal/filter.ts b/apps/web/core/components/power-k/ui/modal/filter.ts new file mode 100644 index 00000000000..160643df6fa --- /dev/null +++ b/apps/web/core/components/power-k/ui/modal/filter.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +/** + * Prefix marking a `Command.Item` whose match was decided by the search API + * rather than by the text cmdk can see. + */ +export const POWER_K_SERVER_RESULT_PREFIX = "server-result:"; + +/** + * Filter for the Power-K palette. + * + * Static commands still match on their visible label. Server-driven search + * results are passed through untouched: the API matched them against fields + * the palette never renders — a work item's description, for one — so + * re-filtering here on the visible title would silently discard exactly the + * results the search was meant to surface. It would also drop multi-word + * matches whose words are not adjacent in the title, since this is a + * contiguous substring test. + */ +export const powerKCommandFilter = (value: string, search: string): number => { + if (value === "no-results") return 1; + if (value.startsWith(POWER_K_SERVER_RESULT_PREFIX)) return 1; + return value.toLowerCase().includes(search.toLowerCase()) ? 1 : 0; +}; diff --git a/apps/web/core/components/power-k/ui/modal/search-results.tsx b/apps/web/core/components/power-k/ui/modal/search-results.tsx index eaa5eb9b876..c6e51bed8d4 100644 --- a/apps/web/core/components/power-k/ui/modal/search-results.tsx +++ b/apps/web/core/components/power-k/ui/modal/search-results.tsx @@ -13,6 +13,7 @@ import type { IWorkspaceSearchResults } from "@plane/types"; import { useAppRouter } from "@/hooks/use-app-router"; // helpers import { PowerKModalCommandItem } from "./command-item"; +import { POWER_K_SERVER_RESULT_PREFIX } from "./filter"; import { POWER_K_SEARCH_RESULTS_GROUPS_MAP } from "./search-results-map"; type Props = { @@ -40,7 +41,7 @@ export const PowerKModalSearchResults = observer(function PowerKModalSearchResul return ( {section.map((item) => { - let value = `${key}-${item?.id}-${item.name}`; + let value = `${POWER_K_SERVER_RESULT_PREFIX}${key}-${item?.id}-${item.name}`; if ("project__identifier" in item) { value = `${value}-${item.project__identifier}`; diff --git a/apps/web/core/components/power-k/ui/modal/wrapper.tsx b/apps/web/core/components/power-k/ui/modal/wrapper.tsx index 0c2a0c9b15c..8250cf0fa05 100644 --- a/apps/web/core/components/power-k/ui/modal/wrapper.tsx +++ b/apps/web/core/components/power-k/ui/modal/wrapper.tsx @@ -13,6 +13,7 @@ import { usePowerK } from "@/hooks/store/use-power-k"; // local imports import type { TPowerKCommandConfig, TPowerKContext } from "../../core/types"; import type { TPowerKCommandsListProps } from "./commands-list"; +import { powerKCommandFilter } from "./filter"; import { PowerKModalFooter } from "./footer"; import { PowerKModalHeader } from "./header"; @@ -145,11 +146,7 @@ export const ProjectsAppPowerKModalWrapper = observer(function ProjectsAppPowerK > { - if (i18nValue === "no-results") return 1; - if (i18nValue.toLowerCase().includes(search.toLowerCase())) return 1; - return 0; - }} + filter={powerKCommandFilter} shouldFilter={searchTerm.length > 0} onKeyDown={handleKeyDown} className="w-full"