Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/app/views/NavigationPanesView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ export function NavigationPanesView({
onSettingsSectionChange={onSettingsSectionChange}
renderInlineSessionList={
!collapsed
? (searchQuery) => (
? () => (
<SessionListCapability
activeSessionId={activeSessionId}
collapsed={collapsed}
Expand All @@ -391,7 +391,6 @@ export function NavigationPanesView({
onSelectSession={onSelectSession}
onSessionSelectForScroll={handleSessionSelectForScroll}
projects={projects}
searchQuery={searchQuery}
surface={{
renderDragHandle: () => null,
showTopDivider: true,
Expand Down
49 changes: 3 additions & 46 deletions src/app/views/__tests__/NavigationPanesView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -630,59 +630,16 @@ describe("NavigationPanesView", () => {
).toBeInTheDocument();
});

it("shows the sidebar search field by default", () => {
it("does not show a search field in the sidebar", () => {
renderSidebar();

expect(screen.queryByRole("searchbox")).not.toBeInTheDocument();
expect(
screen.getByRole("searchbox", { name: "Jump to a chat" }),
).toHaveAttribute("spellcheck", "false");
expect(
screen.getByRole("searchbox", { name: "Jump to a chat" }),
).toHaveAttribute("autocorrect", "off");
expect(
screen.getByRole("searchbox", { name: "Jump to a chat" }),
).toHaveAttribute("autocapitalize", "none");
expect(
screen.queryByRole("button", { name: "Clear" }),
screen.queryByRole("button", { name: "Jump to a chat" }),
).not.toBeInTheDocument();
expect(screen.getByTestId("nav-settings")).toHaveAccessibleName("Settings");
});

it("clears the sidebar search query with the clear button", async () => {
const user = userEvent.setup();
renderSidebar();

const search = screen.getByRole("searchbox", { name: "Jump to a chat" });
await user.type(search, "profile");
await user.click(screen.getByRole("button", { name: "Clear" }));

expect(search).toHaveValue("");
expect(search).toHaveFocus();
expect(
screen.queryByRole("button", { name: "Clear" }),
).not.toBeInTheDocument();
expect(screen.getByTestId("nav-settings")).toBeInTheDocument();
});

it("selects a filtered chat before search collapses on blur", async () => {
const user = userEvent.setup();
const onSelectSession = vi.fn();
seedSessions({
id: "profile-chat",
title: "Profile polish",
updatedAt: "2026-04-09T12:00:00.000Z",
messageCount: 3,
});

renderSidebar({ onSelectSession });

const search = screen.getByRole("searchbox", { name: "Jump to a chat" });
await user.type(search, "profile");
await user.click(screen.getByRole("button", { name: "Profile polish" }));

expect(onSelectSession).toHaveBeenCalledWith("profile-chat");
});

it("moves roving focus through main sidebar rows", () => {
renderSidebar();

Expand Down
143 changes: 31 additions & 112 deletions src/features/navigation/ui/PrimaryNavigationSurface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,9 @@ import {
type KeyboardEventHandler,
type ReactNode,
type Ref,
useEffect,
useRef,
useState,
} from "react";
import { useTranslation } from "react-i18next";
import {
IconArrowLeft,
IconSearch,
IconServer,
IconX,
} from "@tabler/icons-react";
import { IconArrowLeft, IconServer } from "@tabler/icons-react";
import { ArrowUpCircle } from "lucide-react";
import type { AppView } from "@/app/AppShell";
import { PaneSurface } from "@/app/layout/panes/paneChrome";
Expand All @@ -25,9 +17,9 @@ import {
type SectionId,
} from "@/features/settings/ui/settingsSections";
import { cn } from "@/shared/lib/cn";
import { Button } from "@/shared/ui/button";
import {
SIDEBAR_PANEL_ELEVATED_SHADOW_CLASS,
SIDEBAR_PRIMARY_NAV_TOP_INSET_CLASS,
SIDEBAR_SECTION_DIVIDER_INSET_CLASS,
} from "@/shared/ui/sidebar-tokens";
import { SidebarNavItem } from "./SidebarNavItem";
Expand Down Expand Up @@ -62,7 +54,7 @@ interface PrimaryNavigationSurfaceProps {
onSettingsBack?: () => void;
onSettingsClick?: () => void;
onSettingsSectionChange?: (section: SectionId) => void;
renderInlineSessionList?: (searchQuery: string) => ReactNode;
renderInlineSessionList?: () => ReactNode;
secondaryNavRef: Ref<HTMLElement>;
settingsSections: readonly (typeof SETTINGS_SECTIONS)[number][];
showBottomMask: boolean;
Expand Down Expand Up @@ -107,30 +99,7 @@ export const PrimaryNavigationSurface = forwardRef<
},
ref,
) {
const { t } = useTranslation(["sidebar", "common", "settings"]);
const searchInputRef = useRef<HTMLInputElement>(null);
const [searchExpanded, setSearchExpanded] = useState(!navCollapsed);
const [searchQuery, setSearchQuery] = useState("");
const expandSearch = () => {
setSearchExpanded(true);
window.requestAnimationFrame(() => searchInputRef.current?.focus());
};
useEffect(() => {
if (navCollapsed) {
setSearchQuery("");
return;
}
setSearchExpanded(true);
}, [navCollapsed]);
useEffect(() => {
const focusSearch = () => {
setSearchExpanded(true);
window.requestAnimationFrame(() => searchInputRef.current?.focus());
};
window.addEventListener("goose:focus-nav-search", focusSearch);
return () =>
window.removeEventListener("goose:focus-nav-search", focusSearch);
}, []);
const { t } = useTranslation(["sidebar", "settings"]);
const mainNavItems: readonly {
id: AppView;
label: string;
Expand Down Expand Up @@ -169,7 +138,10 @@ export const PrimaryNavigationSurface = forwardRef<
fullHeight
width={width}
>
<div className="flex-shrink-0 pt-1.5" aria-hidden="true" />
<div
className={cn("flex-shrink-0", SIDEBAR_PRIMARY_NAV_TOP_INSET_CLASS)}
aria-hidden="true"
/>

<div className="relative min-h-0 flex-1 overflow-hidden">
<div
Expand All @@ -182,55 +154,6 @@ export const PrimaryNavigationSurface = forwardRef<
inert={isSecondarySurface ? true : undefined}
aria-hidden={isSecondarySurface}
>
<div className="mb-1 flex h-7 flex-shrink-0 items-center px-1.5">
{navCollapsed ? (
<Button
type="button"
variant="ghost"
size="icon-xs"
aria-label={t("search.jumpToChat")}
tooltip={t("search.jumpToChat")}
onClick={expandSearch}
>
<IconSearch aria-hidden="true" className="!size-4" />
</Button>
) : searchExpanded ? (
<div className="group relative block w-full overflow-hidden rounded-sm">
<IconSearch
aria-hidden="true"
className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground/50 transition-colors group-hover:text-muted-foreground group-focus-within:text-muted-foreground"
/>
<input
ref={searchInputRef}
type="search"
spellCheck={false}
autoCorrect="off"
autoCapitalize="none"
value={searchQuery}
onChange={(event) => setSearchQuery(event.target.value)}
placeholder={t("search.jumpToChat")}
aria-label={t("search.jumpToChat")}
className="h-7 w-full appearance-none rounded-sm border-0 bg-muted/40 pl-9 pr-8 text-sm font-normal text-muted-foreground/50 shadow-none outline-none ring-0 transition-colors placeholder:text-muted-foreground/50 hover:bg-muted/60 hover:text-muted-foreground focus:bg-muted/60 focus:text-muted-foreground focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 [&::-webkit-search-cancel-button]:appearance-none"
/>
{searchQuery && (
<Button
type="button"
variant="ghost"
size="icon-xs"
aria-label={t("common:actions.clear")}
title={t("common:actions.clear")}
className="absolute right-1 top-1/2 -translate-y-1/2"
onClick={() => {
setSearchQuery("");
searchInputRef.current?.focus();
}}
>
<IconX aria-hidden="true" />
</Button>
)}
</div>
) : null}
</div>
<nav
ref={mainNavRef}
onKeyDown={onKeyDown}
Expand All @@ -246,9 +169,7 @@ export const PrimaryNavigationSurface = forwardRef<
}
aria-label={t("navigation.main")}
>
<div
className={cn("relative z-10 space-y-0", searchQuery && "hidden")}
>
<div className="relative z-10 space-y-0">
<SidebarNavItem
testId="nav-home"
navId="home"
Expand Down Expand Up @@ -279,32 +200,30 @@ export const PrimaryNavigationSurface = forwardRef<
})}
</div>

{!navCollapsed && !searchQuery && <SidebarPinnedSection />}
{!navCollapsed && <SidebarPinnedSection />}

{renderInlineSessionList?.(searchQuery)}
{renderInlineSessionList?.()}
</nav>
{(!searchQuery || navCollapsed) && (
<div className="flex-shrink-0 px-1.5 py-1.5">
<div
aria-hidden="true"
className={cn(
"mb-1.5 h-px bg-border/70",
SIDEBAR_SECTION_DIVIDER_INSET_CLASS,
)}
/>
<SidebarNavItem
testId="nav-settings"
navId="settings"
icon={SidebarNavSettingsIcon}
label={t("settings:title")}
collapsed={navCollapsed}
labelTransition={labelTransition}
labelVisible={navLabelVisible}
isActive={activeView === "settings"}
onClick={() => onSettingsClick?.()}
/>
</div>
)}
<div className="flex-shrink-0 px-1.5 py-1.5">
<div
aria-hidden="true"
className={cn(
"mb-1.5 h-px bg-border/70",
SIDEBAR_SECTION_DIVIDER_INSET_CLASS,
)}
/>
<SidebarNavItem
testId="nav-settings"
navId="settings"
icon={SidebarNavSettingsIcon}
label={t("settings:title")}
collapsed={navCollapsed}
labelTransition={labelTransition}
labelVisible={navLabelVisible}
isActive={activeView === "settings"}
onClick={() => onSettingsClick?.()}
/>
</div>
</div>

<div
Expand Down
Loading