Studio: move sidebar search into the header (#7304)

* Studio: move sidebar search into the header

Put the search action as an icon button next to the sidebar toggle in
the header instead of a full-width nav row, so New Chat is the only
fixed row above the scrolling list. The search row is kept for the
collapsed icon rail only. Also add a small bottom gap under New Chat
when it is pinned during scroll.

* Studio: keep search row on custom-titlebar platforms

The header search button only renders on mac/web where the brand row
shows. On win/linux custom titlebars there's no header button, so keep
the full-width search row visible instead of hiding it.

* Studio: address review on sidebar search tooltip

- Hide the search tooltip on mobile (hidden={isMobile}), matching the
  SidebarMenuButton tooltip convention.
- Show Cmd K on Mac and Ctrl K elsewhere instead of a hardcoded glyph;
  the search dialog binds both meta and ctrl. Uses getClientPlatform so
  it is correct on web too, not just Tauri.
This commit is contained in:
Michael Han 2026-07-21 23:25:56 -07:00 committed by GitHub
commit 8517721adb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 50 additions and 9 deletions

View file

@ -45,6 +45,7 @@ import { Spinner } from "@/components/ui/spinner";
import { Switch } from "@/components/ui/switch";
import { useAnimatedThemeToggle } from "@/components/ui/animated-theme-toggler";
import {
getClientPlatform,
shouldUseCustomWindowTitlebar,
shouldUseNativeMacWindowTitlebar,
} from "@/components/tauri/window-titlebar";
@ -343,6 +344,8 @@ export function AppSidebar() {
);
const [usesCustomTitlebar] = useState(shouldUseCustomWindowTitlebar);
const [usesNativeMacTitlebar] = useState(shouldUseNativeMacWindowTitlebar);
// Mac uses Cmd, others use Ctrl. Not Tauri-gated, so it's right on web too.
const [isMacPlatform] = useState(() => getClientPlatform().includes("mac"));
const { pathname, search } = useRouterState({
select: (s) => ({
pathname: s.location.pathname,
@ -1194,27 +1197,55 @@ export function AppSidebar() {
</span>
</Link>
)}
{!isMobile && (
<div className="flex items-center gap-0.5">
<Tooltip>
<TooltipPrimitive.Trigger asChild>
<button
type="button"
onClick={togglePinned}
onClick={() => {
useChatSearchStore.getState().open();
closeMobileIfOpen();
}}
className="inline-flex h-[33px] w-[32px] cursor-pointer items-center justify-center rounded-[10px] text-nav-icon-idle dark:text-nav-fg-muted transition-colors hover:bg-nav-surface-hover hover:text-black dark:hover:text-white focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
aria-label={t("shell.aria.closeSidebar")}
aria-label={t("shell.navigation.search")}
>
<HugeiconsIcon icon={LayoutAlignLeftIcon} strokeWidth={1.75} className="size-icon" />
<HugeiconsIcon icon={Search01Icon} strokeWidth={1.75} className="size-icon" />
</button>
</TooltipPrimitive.Trigger>
<TooltipContent
side="bottom"
sideOffset={6}
className="tooltip-compact"
className="tooltip-compact flex items-center gap-1.5"
hidden={isMobile}
>
{t("shell.aria.closeSidebar")}
{t("shell.navigation.search")}
<kbd className="rounded bg-black/10 px-1 py-px text-[10px] font-medium leading-none dark:bg-white/15">
{isMacPlatform ? "⌘K" : "Ctrl+K"}
</kbd>
</TooltipContent>
</Tooltip>
)}
{!isMobile && (
<Tooltip>
<TooltipPrimitive.Trigger asChild>
<button
type="button"
onClick={togglePinned}
className="inline-flex h-[33px] w-[32px] cursor-pointer items-center justify-center rounded-[10px] text-nav-icon-idle dark:text-nav-fg-muted transition-colors hover:bg-nav-surface-hover hover:text-black dark:hover:text-white focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
aria-label={t("shell.aria.closeSidebar")}
>
<HugeiconsIcon icon={LayoutAlignLeftIcon} strokeWidth={1.75} className="size-icon" />
</button>
</TooltipPrimitive.Trigger>
<TooltipContent
side="bottom"
sideOffset={6}
className="tooltip-compact"
>
{t("shell.aria.closeSidebar")}
</TooltipContent>
</Tooltip>
)}
</div>
</div>
{!isMobile && (
<div className="relative z-10 hidden group-data-[collapsible=icon]:flex h-[33px] items-center justify-center w-full">
@ -1246,8 +1277,10 @@ export function AppSidebar() {
{/* Uniform pl-1.5 pr-2 keeps every hover pill the same width, inset from the edge. */}
<SidebarGroup
className={cn(
"group-data-[collapsible=icon]:px-0 pl-1.5 pr-2 pb-px shrink-0",
"group-data-[collapsible=icon]:px-0 pl-1.5 pr-2 shrink-0 transition-[padding]",
showCompactMacBrand ? "pt-0" : "pt-[9px]",
// Scrolled: New Chat is pinned, give a little gap below it.
scrolled ? "pb-[5px]" : "pb-px",
)}
>
<SidebarGroupContent>
@ -1280,10 +1313,18 @@ export function AppSidebar() {
openNewChat(null);
}}
/>
{/* Search sits in the header when the brand row is shown (mac/web).
Hide this row there, but keep it in the collapsed rail. On custom
titlebars (win/linux) there's no header button, so keep the row. */}
<NavItem
icon={Search01Icon}
label={t("shell.navigation.search")}
active={false}
className={
showSidebarBrand
? "hidden group-data-[collapsible=icon]:block"
: undefined
}
onClick={() => {
useChatSearchStore.getState().open();
closeMobileIfOpen();

View file

@ -40,7 +40,7 @@ type NavigatorWithUserAgentData = Navigator & {
};
};
function getClientPlatform(): string {
export function getClientPlatform(): string {
if (typeof navigator === "undefined") {
return "";
}