diff --git a/frontend/editor/public/locales/en-GB/translation.toml b/frontend/editor/public/locales/en-GB/translation.toml
index fcabd6942b..4f0f6bbd20 100644
--- a/frontend/editor/public/locales/en-GB/translation.toml
+++ b/frontend/editor/public/locales/en-GB/translation.toml
@@ -7896,14 +7896,10 @@ description = "Pipeline runs, deploys and agent events will appear here."
title = "Nothing here yet"
[portal.search]
-ariaLabel = "Search"
goTo = "Go to"
+hint = "Type to jump to any portal page"
placeholder = "Search the portal…"
-[portal.search.empty]
-noMatches = "No matches for \"{{query}}\""
-noMatchesDescription = "Try a different keyword."
-
[portal.settings.groups]
admin = "Admin"
diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml
index 4bb6237843..b70b4b47c2 100644
--- a/frontend/editor/public/locales/en-US/translation.toml
+++ b/frontend/editor/public/locales/en-US/translation.toml
@@ -7986,14 +7986,10 @@ description = "Pipeline runs, deploys and agent events will appear here."
title = "Nothing here yet"
[portal.search]
-ariaLabel = "Search"
goTo = "Go to"
+hint = "Type to jump to any portal page"
placeholder = "Search the portal…"
-[portal.search.empty]
-noMatches = "No matches for \"{{query}}\""
-noMatchesDescription = "Try a different keyword."
-
[portal.settings.groups]
admin = "Admin"
diff --git a/frontend/editor/src/core/components/shared/superSearch/SuperSearch.tsx b/frontend/editor/src/core/components/shared/superSearch/SuperSearch.tsx
index 2ec91bf825..6817eb6fa1 100644
--- a/frontend/editor/src/core/components/shared/superSearch/SuperSearch.tsx
+++ b/frontend/editor/src/core/components/shared/superSearch/SuperSearch.tsx
@@ -13,7 +13,11 @@ import { Button } from "@app/ui/Button";
import { TextInput } from "@app/components/shared/TextInput";
import LocalIcon from "@app/components/shared/LocalIcon";
import { isMacLike } from "@app/utils/hotkeys";
-import { useSuperSearch, SuperSearchResult } from "@app/hooks/useSuperSearch";
+import {
+ useSuperSearch,
+ SuperSearchResult,
+ type UseSuperSearchResult,
+} from "@app/hooks/useSuperSearch";
import "@app/components/shared/superSearch/SuperSearch.css";
interface DropdownRect {
@@ -22,15 +26,35 @@ interface DropdownRect {
width: number;
}
+interface SuperSearchProps {
+ /**
+ * Results provider, called as a hook — it MUST be referentially stable for
+ * the component's lifetime. Defaults to the editor's files/tools/settings/
+ * Processor provider; the portal passes its own destinations provider.
+ */
+ useResults?: (query: string, active: boolean) => UseSuperSearchResult;
+ /** Override the input placeholder (defaults to the editor copy). */
+ placeholder?: string;
+ /** Override the empty-query hint line (defaults to the editor copy). */
+ hint?: string;
+ /** DOM id for the input — external focus helpers target it. */
+ inputId?: string;
+}
+
/**
- * Global "super search": a single entry point that searches across My Files,
- * Tools, and Settings from the (now permanent) top bar. The results hang in a
- * dropdown directly below the input; Cmd/Ctrl+K focuses and opens it.
+ * Global "super search": a single entry point that searches across the host
+ * app's destinations from a persistent bar. The results hang in a dropdown
+ * directly below the input; Cmd/Ctrl+K focuses and opens it.
*
- * The dropdown is portalled to
: the workbench bar's inner wrapper sets
+ * The dropdown is portalled to : the host bar's inner wrapper may set
* `overflow: hidden`, which would otherwise clip it.
*/
-export default function SuperSearch() {
+export default function SuperSearch({
+ useResults = useSuperSearch,
+ placeholder,
+ hint,
+ inputId = "super-search-input",
+}: SuperSearchProps = {}) {
const { t } = useTranslation();
const [query, setQuery] = useState("");
const [open, setOpen] = useState(false);
@@ -41,7 +65,7 @@ export default function SuperSearch() {
const containerRef = useRef(null);
const dropdownRef = useRef(null);
- const { groups, flatResults, loadingFiles } = useSuperSearch(query, open);
+ const { groups, flatResults, loadingFiles } = useResults(query, open);
const trimmed = query.trim();
const hasQuery = trimmed.length > 0;
@@ -166,10 +190,11 @@ export default function SuperSearch() {
>
{!hasQuery && (
- {t(
- "superSearch.hint",
- "Type to search across your files, tools and settings",
- )}
+ {hint ??
+ t(
+ "superSearch.hint",
+ "Type to search across your files, tools and settings",
+ )}
}
diff --git a/frontend/editor/src/core/hooks/useSuperSearch.ts b/frontend/editor/src/core/hooks/useSuperSearch.ts
index 8484c8183b..bafaa6f1b0 100644
--- a/frontend/editor/src/core/hooks/useSuperSearch.ts
+++ b/frontend/editor/src/core/hooks/useSuperSearch.ts
@@ -28,7 +28,8 @@ export type SuperSearchGroupId = "files" | "tools" | "settings" | "processor";
export interface SuperSearchResult {
/** Stable unique key across all groups. */
key: string;
- group: SuperSearchGroupId;
+ /** Group id — the editor uses SuperSearchGroupId; other hosts use their own. */
+ group: string;
title: string;
subtitle?: string;
/** LocalIcon name (files/settings); tools provide a React node via `icon`. */
@@ -39,7 +40,7 @@ export interface SuperSearchResult {
}
export interface SuperSearchGroup {
- id: SuperSearchGroupId;
+ id: string;
label: string;
results: SuperSearchResult[];
}
diff --git a/frontend/editor/src/portal/components/AppShell.tsx b/frontend/editor/src/portal/components/AppShell.tsx
index b3744d36bf..bdb508ddac 100644
--- a/frontend/editor/src/portal/components/AppShell.tsx
+++ b/frontend/editor/src/portal/components/AppShell.tsx
@@ -1,17 +1,19 @@
import type { ReactNode } from "react";
import { Sidebar } from "@portal/components/Sidebar";
+import { PortalSearchBar } from "@portal/components/PortalSearchBar";
import "@portal/components/AppShell.css";
/**
* Two-column layout: fixed-width sidebar on the left, a scrolling main column on
- * the right. The Sidebar reads its state from context, so this shell stays
- * prop-free.
+ * the right (topped by the global search bar). The Sidebar reads its state from
+ * context, so this shell stays prop-free.
*/
export function AppShell({ children }: { children: ReactNode }) {
return (
+ {children}
diff --git a/frontend/editor/src/portal/components/PortalChrome.tsx b/frontend/editor/src/portal/components/PortalChrome.tsx
index 1262c84b8e..05d59a52b8 100644
--- a/frontend/editor/src/portal/components/PortalChrome.tsx
+++ b/frontend/editor/src/portal/components/PortalChrome.tsx
@@ -1,39 +1,10 @@
-import { useEffect } from "react";
import { useLocation } from "react-router-dom";
import { ToolRegistryProvider } from "@app/contexts/ToolRegistryProvider";
import { ErrorBoundary } from "@portal/components/ErrorBoundary";
-import { useUI } from "@portal/contexts/UIContext";
import { AppShell } from "@portal/components/AppShell";
-import { SearchModal } from "@portal/components/SearchModal";
import { PortalSettingsHost } from "@portal/components/PortalSettingsHost";
import { ViewRouter } from "@portal/ViewRouter";
-/**
- * Global keyboard shortcuts. Lives below the UIProvider so it can dispatch into
- * the overlay state. Currently just ⌘K / Ctrl+K to toggle the search palette.
- */
-function GlobalShortcuts() {
- const { toggleSearch, closeSearch } = useUI();
-
- useEffect(() => {
- function onKey(e: KeyboardEvent) {
- const isCmdK = (e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k";
- if (isCmdK) {
- e.preventDefault();
- toggleSearch();
- return;
- }
- if (e.key === "Escape") {
- closeSearch();
- }
- }
- document.addEventListener("keydown", onKey);
- return () => document.removeEventListener("keydown", onKey);
- }, [toggleSearch, closeSearch]);
-
- return null;
-}
-
/**
* The routed view, wrapped in an error boundary so a single view crashing can't
* white-screen the portal (the shell + nav stay alive). Keyed by route so
@@ -49,23 +20,21 @@ function RoutedContent() {
}
/**
- * The flavor-agnostic portal chrome: the shell (sidebar + header + routed view)
- * plus the global overlays that every flavor shares. Requires only the Tier and
- * UI contexts above it — both flavors provide those. Flavor-specific overlays
- * (e.g. the self-hosted account-link modal) are mounted by PortalProviders, not
- * here.
+ * The flavor-agnostic portal chrome: the shell (sidebar + search bar + routed
+ * view) plus the global overlays that every flavor shares. Requires only the
+ * Tier and UI contexts above it — both flavors provide those. Flavor-specific
+ * overlays (e.g. the self-hosted account-link modal) are mounted by
+ * PortalProviders, not here.
*/
export function PortalChrome() {
return (
<>
-
{/* The pipeline builder reads the tool registry to list and configure operations. */}
-
>
);
diff --git a/frontend/editor/src/portal/components/PortalSearchBar.css b/frontend/editor/src/portal/components/PortalSearchBar.css
new file mode 100644
index 0000000000..dfa43fa300
--- /dev/null
+++ b/frontend/editor/src/portal/components/PortalSearchBar.css
@@ -0,0 +1,11 @@
+/* Slim strip at the top of the main column hosting the shared search bar. */
+.portal-searchbar {
+ display: flex;
+ justify-content: center;
+ padding: var(--space-2) var(--space-3) 0;
+}
+
+.portal-searchbar .super-search {
+ width: 100%;
+ max-width: 34rem;
+}
diff --git a/frontend/editor/src/portal/components/PortalSearchBar.tsx b/frontend/editor/src/portal/components/PortalSearchBar.tsx
new file mode 100644
index 0000000000..be2415dc04
--- /dev/null
+++ b/frontend/editor/src/portal/components/PortalSearchBar.tsx
@@ -0,0 +1,23 @@
+import { useTranslation } from "react-i18next";
+import SuperSearch from "@app/components/shared/superSearch/SuperSearch";
+import { usePortalSearchResults } from "@portal/hooks/usePortalSearchResults";
+import "@portal/components/PortalSearchBar.css";
+
+/**
+ * The portal face of the global super search — the same bar the editor's
+ * workbench shows, fed by the portal's destinations provider. Cmd/Ctrl+K
+ * focuses it (the bar registers its own shortcut).
+ */
+export function PortalSearchBar() {
+ const { t } = useTranslation();
+ return (
+