From 33a63bded5775c2d06d0d789565d3ded27a4f26c Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Sun, 24 May 2026 21:18:59 -0400 Subject: [PATCH] feat: add inline global search topbar Co-authored-by: Cursor --- desktop/src/app/AppHeaderControls.tsx | 92 +++--- desktop/src/app/AppShell.tsx | 48 ++- desktop/src/app/AppShellOverlays.tsx | 3 + desktop/src/features/chat/ui/ChatHeader.tsx | 92 +++--- .../messages/ui/MessageThreadPanel.tsx | 15 +- .../src/features/search/ui/SearchDialog.tsx | 11 + .../src/features/search/ui/TopbarSearch.tsx | 307 ++++++++++++++++++ desktop/tests/e2e/smoke.spec.ts | 40 ++- 8 files changed, 482 insertions(+), 126 deletions(-) create mode 100644 desktop/src/features/search/ui/TopbarSearch.tsx diff --git a/desktop/src/app/AppHeaderControls.tsx b/desktop/src/app/AppHeaderControls.tsx index e7fd89c5f..c422b0679 100644 --- a/desktop/src/app/AppHeaderControls.tsx +++ b/desktop/src/app/AppHeaderControls.tsx @@ -1,58 +1,74 @@ -import { ChevronLeft, ChevronRight, Search } from "lucide-react"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import { TopbarSearch } from "@/features/search/ui/TopbarSearch"; import { Button } from "@/shared/ui/button"; +import type { Channel, SearchHit } from "@/shared/api/types"; import { SidebarTrigger } from "@/shared/ui/sidebar"; type AppHeaderControlsProps = { canGoBack: boolean; canGoForward: boolean; + channels: Channel[]; + currentPubkey?: string; onGoBack: () => void; onGoForward: () => void; - onOpenSearch: () => void; + onOpenChannel: (channelId: string) => void; + onOpenResult: (hit: SearchHit) => void; + onOpenUser: (pubkey: string) => void; }; export function AppHeaderControls({ canGoBack, canGoForward, + channels, + currentPubkey, onGoBack, onGoForward, - onOpenSearch, + onOpenChannel, + onOpenResult, + onOpenUser, }: AppHeaderControlsProps) { return ( -
- - - - -
+ <> +
+ +
+
+ + + +
+ +
+ +
+
+ ); } diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx index 36bdd8c44..8ba011009 100644 --- a/desktop/src/app/AppShell.tsx +++ b/desktop/src/app/AppShell.tsx @@ -169,6 +169,7 @@ export function AppShell() { const [isChannelManagementOpen, setIsChannelManagementOpen] = React.useState(false); const [isSearchOpen, setIsSearchOpen] = React.useState(false); + const [searchInitialQuery, setSearchInitialQuery] = React.useState(""); const [browseDialogType, setBrowseDialogType] = React.useState(null); const [isNewDmOpen, setIsNewDmOpen] = React.useState(false); @@ -313,10 +314,14 @@ export function AppShell() { setBrowseDialogType("forum"); void refetchChannels(); }, [refetchChannels]); - const handleOpenSearch = React.useCallback(() => { - setIsSearchOpen(true); - void refetchChannels(); - }, [refetchChannels]); + const handleOpenSearch = React.useCallback( + (query = "") => { + setSearchInitialQuery(query); + setIsSearchOpen(true); + void refetchChannels(); + }, + [refetchChannels], + ); const handleBrowseDialogOpenChange = React.useCallback((open: boolean) => { if (!open) { @@ -617,9 +622,13 @@ export function AppShell() { void goChannel(channelId)} + onOpenResult={handleOpenSearchResult} + onOpenUser={handleOpenSearchUser} /> { - void goAgents(); - }} - onSelectChannel={(channelId) => { - void goChannel(channelId); - }} - onSelectHome={() => { - void goHome(); - }} - onSelectProjects={() => { - void goProjects(); - }} - onSelectPulse={() => { - void goPulse(); - }} + onSelectAgents={() => void goAgents()} + onSelectChannel={(channelId) => void goChannel(channelId)} + onSelectHome={() => void goHome()} + onSelectProjects={() => void goProjects()} + onSelectPulse={() => void goPulse()} onSelectSettings={handleOpenSettings} - onSelectWorkflows={() => { - void goWorkflows(); - }} + onSelectWorkflows={() => void goWorkflows()} onSetPresenceStatus={(status) => presenceSession.setStatus(status) } @@ -751,6 +748,7 @@ export function AppShell() { currentPubkey={identityQuery.data?.pubkey} isChannelManagementOpen={isChannelManagementOpen} isSearchOpen={isSearchOpen} + searchInitialQuery={searchInitialQuery} onBrowseChannelJoin={handleBrowseChannelJoin} onBrowseDialogOpenChange={handleBrowseDialogOpenChange} onChannelManagementOpenChange={setIsChannelManagementOpen} @@ -761,9 +759,7 @@ export function AppShell() { onOpenSearchResult={handleOpenSearchResult} onOpenSearchUser={handleOpenSearchUser} onSearchOpenChange={setIsSearchOpen} - onSelectChannel={(channelId) => { - void goChannel(channelId); - }} + onSelectChannel={(channelId) => void goChannel(channelId)} /> {settingsOpen ? ( diff --git a/desktop/src/app/AppShellOverlays.tsx b/desktop/src/app/AppShellOverlays.tsx index 2c560c41c..edd29fc3e 100644 --- a/desktop/src/app/AppShellOverlays.tsx +++ b/desktop/src/app/AppShellOverlays.tsx @@ -26,6 +26,7 @@ type AppShellOverlaysProps = { currentPubkey?: string; isChannelManagementOpen: boolean; isSearchOpen: boolean; + searchInitialQuery: string; onBrowseChannelJoin: (channelId: string) => Promise; onBrowseDialogOpenChange: (open: boolean) => void; onChannelManagementOpenChange: (open: boolean) => void; @@ -43,6 +44,7 @@ export function AppShellOverlays({ currentPubkey, isChannelManagementOpen, isSearchOpen, + searchInitialQuery, onBrowseChannelJoin, onBrowseDialogOpenChange, onChannelManagementOpenChange, @@ -72,6 +74,7 @@ export function AppShellOverlays({ + + {actions ?
{actions}
: null} +
+ ); + const topbarActions = + overlaysContent && typeof document !== "undefined" + ? createPortal( +
{actionCluster}
, + document.body, + ) + : null; return ( -
-
-
- -

- {title} -

- {statusBadge ? ( -
- {statusBadge} -
- ) : null} -
-
+ <> + {topbarActions} +
+
-
- - {actions ?
{actions}
: null} -
-
+
+
+ +

+ {title} +

+ {statusBadge ? ( +
+ {statusBadge} +
+ ) : null} +
+
+ + {overlaysContent ? null : ( +
{actionCluster}
+ )} +
+ ); } diff --git a/desktop/src/features/messages/ui/MessageThreadPanel.tsx b/desktop/src/features/messages/ui/MessageThreadPanel.tsx index 5169c65c3..615f76e59 100644 --- a/desktop/src/features/messages/ui/MessageThreadPanel.tsx +++ b/desktop/src/features/messages/ui/MessageThreadPanel.tsx @@ -153,11 +153,7 @@ export function MessageThreadPanel({ <> {isOverlay && }