added search bar to top bar

This commit is contained in:
Arjun 2026-02-17 23:26:24 +05:30
parent ac30af4d10
commit 5f56a842e2
3 changed files with 84 additions and 154 deletions

View file

@ -6,7 +6,7 @@ import type { LanguageModelUsage, ToolUIPart } from 'ai';
import './App.css' import './App.css'
import z from 'zod'; import z from 'zod';
import { Button } from './components/ui/button'; import { Button } from './components/ui/button';
import { CheckIcon, LoaderIcon, ArrowUp, PanelLeftIcon, PanelRightIcon, Square, X, ChevronLeftIcon, ChevronRightIcon, SquarePen } from 'lucide-react'; import { CheckIcon, LoaderIcon, ArrowUp, PanelLeftIcon, PanelRightIcon, Search, Square, X, ChevronLeftIcon, ChevronRightIcon, SquarePen } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { MarkdownEditor } from './components/markdown-editor'; import { MarkdownEditor } from './components/markdown-editor';
import { ChatInputBar } from './components/chat-button'; import { ChatInputBar } from './components/chat-button';
@ -14,7 +14,7 @@ import { ChatSidebar } from './components/chat-sidebar';
import { GraphView, type GraphEdge, type GraphNode } from '@/components/graph-view'; import { GraphView, type GraphEdge, type GraphNode } from '@/components/graph-view';
import { useDebounce } from './hooks/use-debounce'; import { useDebounce } from './hooks/use-debounce';
import { SidebarContentPanel } from '@/components/sidebar-content'; import { SidebarContentPanel } from '@/components/sidebar-content';
import { SidebarSectionProvider, type ActiveSection } from '@/contexts/sidebar-context'; import { SidebarSectionProvider, useSidebarSection, type ActiveSection } from '@/contexts/sidebar-context';
import { import {
Conversation, Conversation,
ConversationContent, ConversationContent,
@ -131,8 +131,8 @@ const TITLEBAR_BUTTON_PX = 32
const TITLEBAR_BUTTON_GAP_PX = 4 const TITLEBAR_BUTTON_GAP_PX = 4
const TITLEBAR_HEADER_GAP_PX = 8 const TITLEBAR_HEADER_GAP_PX = 8
const TITLEBAR_TOGGLE_MARGIN_LEFT_PX = 12 const TITLEBAR_TOGGLE_MARGIN_LEFT_PX = 12
const TITLEBAR_BUTTONS_COLLAPSED = 4 const TITLEBAR_BUTTONS_COLLAPSED = 5
const TITLEBAR_BUTTON_GAPS_COLLAPSED = 3 const TITLEBAR_BUTTON_GAPS_COLLAPSED = 4
const clampNumber = (value: number, min: number, max: number) => const clampNumber = (value: number, min: number, max: number) =>
Math.min(max, Math.max(min, value)) Math.min(max, Math.max(min, value))
@ -487,6 +487,7 @@ function FixedSidebarToggle({
leftInsetPx: number leftInsetPx: number
}) { }) {
const { toggleSidebar, state } = useSidebar() const { toggleSidebar, state } = useSidebar()
const { searchOpen, setSearchOpen } = useSidebarSection()
const isCollapsed = state === "collapsed" const isCollapsed = state === "collapsed"
return ( return (
<div className="fixed left-0 top-0 z-50 flex h-10 items-center" style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}> <div className="fixed left-0 top-0 z-50 flex h-10 items-center" style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}>
@ -510,6 +511,20 @@ function FixedSidebarToggle({
> >
<SquarePen className="size-5" /> <SquarePen className="size-5" />
</button> </button>
<button
type="button"
onClick={() => setSearchOpen(!searchOpen)}
className={cn(
"flex h-8 w-8 items-center justify-center rounded-md transition-colors",
searchOpen
? "text-foreground bg-accent"
: "text-muted-foreground hover:bg-accent hover:text-foreground"
)}
style={{ marginLeft: TITLEBAR_BUTTON_GAP_PX }}
aria-label="Search"
>
<Search className="size-5" />
</button>
{/* Back / Forward navigation */} {/* Back / Forward navigation */}
{isCollapsed && ( {isCollapsed && (
<> <>

View file

@ -401,8 +401,7 @@ export function SidebarContentPanel({
selectedBackgroundTask, selectedBackgroundTask,
...props ...props
}: SidebarContentPanelProps) { }: SidebarContentPanelProps) {
const { activeSection, setActiveSection } = useSidebarSection() const { activeSection, setActiveSection, searchOpen, setSearchOpen } = useSidebarSection()
const [searchOpen, setSearchOpen] = useState(false)
const [searchQuery, setSearchQuery] = useState("") const [searchQuery, setSearchQuery] = useState("")
const searchInputRef = useRef<HTMLInputElement>(null) const searchInputRef = useRef<HTMLInputElement>(null)
@ -437,7 +436,7 @@ export function SidebarContentPanel({
<SidebarHeader className="titlebar-drag-region"> <SidebarHeader className="titlebar-drag-region">
{/* Top spacer to clear the traffic lights + fixed toggle row */} {/* Top spacer to clear the traffic lights + fixed toggle row */}
<div className="h-8" /> <div className="h-8" />
{/* Tab switcher - centered below the traffic lights row */} {/* Tab switcher */}
<div className="flex items-center px-2 py-1.5"> <div className="flex items-center px-2 py-1.5">
<div className="titlebar-no-drag flex w-full rounded-lg bg-sidebar-accent/50 p-0.5"> <div className="titlebar-no-drag flex w-full rounded-lg bg-sidebar-accent/50 p-0.5">
{sectionTabs.map((tab) => ( {sectionTabs.map((tab) => (
@ -456,6 +455,29 @@ export function SidebarContentPanel({
))} ))}
</div> </div>
</div> </div>
{searchOpen && (
<div className="titlebar-no-drag flex items-center gap-1 rounded-md border border-sidebar-border bg-sidebar-accent/30 mx-2 mb-1.5 px-2 py-1">
<Search className="size-3.5 shrink-0 text-muted-foreground" />
<input
ref={searchInputRef}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Escape") setSearchOpen(false)
}}
placeholder={activeSection === "tasks" ? "Search chats..." : "Search files..."}
className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground/60"
/>
{searchQuery && (
<button
onClick={() => setSearchQuery("")}
className="rounded p-0.5 text-muted-foreground hover:text-sidebar-foreground transition-colors"
>
<X className="size-3.5" />
</button>
)}
</div>
)}
</SidebarHeader> </SidebarHeader>
<SidebarContent> <SidebarContent>
{activeSection === "knowledge" && ( {activeSection === "knowledge" && (
@ -466,11 +488,6 @@ export function SidebarContentPanel({
onSelectFile={onSelectFile} onSelectFile={onSelectFile}
actions={knowledgeActions} actions={knowledgeActions}
onVoiceNoteCreated={onVoiceNoteCreated} onVoiceNoteCreated={onVoiceNoteCreated}
searchOpen={searchOpen}
setSearchOpen={setSearchOpen}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
searchInputRef={searchInputRef}
/> />
)} )}
{activeSection === "tasks" && ( {activeSection === "tasks" && (
@ -481,11 +498,6 @@ export function SidebarContentPanel({
actions={tasksActions} actions={tasksActions}
backgroundTasks={filteredBackgroundTasks} backgroundTasks={filteredBackgroundTasks}
selectedBackgroundTask={selectedBackgroundTask} selectedBackgroundTask={selectedBackgroundTask}
searchOpen={searchOpen}
setSearchOpen={setSearchOpen}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
searchInputRef={searchInputRef}
/> />
)} )}
</SidebarContent> </SidebarContent>
@ -762,11 +774,6 @@ function KnowledgeSection({
onSelectFile, onSelectFile,
actions, actions,
onVoiceNoteCreated, onVoiceNoteCreated,
searchOpen,
setSearchOpen,
searchQuery,
setSearchQuery,
searchInputRef,
}: { }: {
tree: TreeNode[] tree: TreeNode[]
selectedPath: string | null selectedPath: string | null
@ -774,11 +781,6 @@ function KnowledgeSection({
onSelectFile: (path: string, kind: "file" | "dir") => void onSelectFile: (path: string, kind: "file" | "dir") => void
actions: KnowledgeActions actions: KnowledgeActions
onVoiceNoteCreated?: (path: string) => void onVoiceNoteCreated?: (path: string) => void
searchOpen: boolean
setSearchOpen: (open: boolean) => void
searchQuery: string
setSearchQuery: (query: string) => void
searchInputRef: React.RefObject<HTMLInputElement | null>
}) { }) {
const isExpanded = expandedPaths.size > 0 const isExpanded = expandedPaths.size > 0
@ -792,79 +794,38 @@ function KnowledgeSection({
<ContextMenu> <ContextMenu>
<ContextMenuTrigger asChild> <ContextMenuTrigger asChild>
<SidebarGroup className="flex-1 flex flex-col overflow-hidden"> <SidebarGroup className="flex-1 flex flex-col overflow-hidden">
<div className="sticky top-0 z-10 bg-sidebar border-b border-sidebar-border"> <div className="flex items-center justify-center gap-1 py-1 sticky top-0 z-10 bg-sidebar border-b border-sidebar-border">
<div className="flex items-center justify-center gap-1 py-1"> {quickActions.map((action) => (
{quickActions.map((action) => ( <Tooltip key={action.label}>
<Tooltip key={action.label}>
<TooltipTrigger asChild>
<button
onClick={action.action}
className="text-sidebar-foreground/70 hover:text-sidebar-foreground hover:bg-sidebar-accent rounded p-1.5 transition-colors"
>
<action.icon className="size-4" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom">{action.label}</TooltipContent>
</Tooltip>
))}
<VoiceNoteButton onNoteCreated={onVoiceNoteCreated} />
<Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<button <button
onClick={isExpanded ? actions.collapseAll : actions.expandAll} onClick={action.action}
className="text-sidebar-foreground/70 hover:text-sidebar-foreground hover:bg-sidebar-accent rounded p-1.5 transition-colors" className="text-sidebar-foreground/70 hover:text-sidebar-foreground hover:bg-sidebar-accent rounded p-1.5 transition-colors"
> >
{isExpanded ? ( <action.icon className="size-4" />
<ChevronsDownUp className="size-4" />
) : (
<ChevronsUpDown className="size-4" />
)}
</button> </button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="bottom"> <TooltipContent side="bottom">{action.label}</TooltipContent>
{isExpanded ? "Collapse All" : "Expand All"}
</TooltipContent>
</Tooltip> </Tooltip>
<Tooltip> ))}
<TooltipTrigger asChild> <VoiceNoteButton onNoteCreated={onVoiceNoteCreated} />
<button <Tooltip>
onClick={() => setSearchOpen(!searchOpen)} <TooltipTrigger asChild>
className={cn( <button
"rounded p-1.5 transition-colors", onClick={isExpanded ? actions.collapseAll : actions.expandAll}
searchOpen className="text-sidebar-foreground/70 hover:text-sidebar-foreground hover:bg-sidebar-accent rounded p-1.5 transition-colors"
? "text-sidebar-foreground bg-sidebar-accent" >
: "text-sidebar-foreground/70 hover:text-sidebar-foreground hover:bg-sidebar-accent" {isExpanded ? (
)} <ChevronsDownUp className="size-4" />
> ) : (
<Search className="size-4" /> <ChevronsUpDown className="size-4" />
</button> )}
</TooltipTrigger> </button>
<TooltipContent side="bottom">Search</TooltipContent> </TooltipTrigger>
</Tooltip> <TooltipContent side="bottom">
</div> {isExpanded ? "Collapse All" : "Expand All"}
{searchOpen && ( </TooltipContent>
<div className="flex items-center gap-1 rounded-md border border-sidebar-border bg-sidebar-accent/30 mx-2 mb-1 px-2 py-1"> </Tooltip>
<Search className="size-3.5 shrink-0 text-muted-foreground" />
<input
ref={searchInputRef}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Escape") setSearchOpen(false)
}}
placeholder="Search files..."
className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground/60"
/>
{searchQuery && (
<button
onClick={() => setSearchQuery("")}
className="rounded p-0.5 text-muted-foreground hover:text-sidebar-foreground transition-colors"
>
<X className="size-3.5" />
</button>
)}
</div>
)}
</div> </div>
<SidebarGroupContent className="flex-1 overflow-y-auto"> <SidebarGroupContent className="flex-1 overflow-y-auto">
<SidebarMenu> <SidebarMenu>
@ -1118,11 +1079,6 @@ function TasksSection({
actions, actions,
backgroundTasks = [], backgroundTasks = [],
selectedBackgroundTask, selectedBackgroundTask,
searchOpen,
setSearchOpen,
searchQuery,
setSearchQuery,
searchInputRef,
}: { }: {
runs: RunListItem[] runs: RunListItem[]
currentRunId?: string | null currentRunId?: string | null
@ -1130,65 +1086,19 @@ function TasksSection({
actions?: TasksActions actions?: TasksActions
backgroundTasks?: BackgroundTaskItem[] backgroundTasks?: BackgroundTaskItem[]
selectedBackgroundTask?: string | null selectedBackgroundTask?: string | null
searchOpen: boolean
setSearchOpen: (open: boolean) => void
searchQuery: string
setSearchQuery: (query: string) => void
searchInputRef: React.RefObject<HTMLInputElement | null>
}) { }) {
return ( return (
<SidebarGroup className="flex-1 flex flex-col overflow-hidden"> <SidebarGroup className="flex-1 flex flex-col overflow-hidden">
{/* Sticky New Chat button + search */} {/* Sticky New Chat button - matches Knowledge section height */}
<div className="sticky top-0 z-10 bg-sidebar border-b border-sidebar-border py-0.5"> <div className="sticky top-0 z-10 bg-sidebar border-b border-sidebar-border py-0.5">
<div className="flex items-center gap-1 px-1"> <SidebarMenu>
<SidebarMenu className="flex-1"> <SidebarMenuItem>
<SidebarMenuItem> <SidebarMenuButton onClick={actions?.onNewChat} className="gap-2">
<SidebarMenuButton onClick={actions?.onNewChat} className="gap-2"> <SquarePen className="size-4 shrink-0" />
<SquarePen className="size-4 shrink-0" /> <span className="text-sm">New chat</span>
<span className="text-sm">New chat</span> </SidebarMenuButton>
</SidebarMenuButton> </SidebarMenuItem>
</SidebarMenuItem> </SidebarMenu>
</SidebarMenu>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => setSearchOpen(!searchOpen)}
className={cn(
"rounded p-1.5 transition-colors shrink-0",
searchOpen
? "text-sidebar-foreground bg-sidebar-accent"
: "text-sidebar-foreground/70 hover:text-sidebar-foreground hover:bg-sidebar-accent"
)}
>
<Search className="size-4" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom">Search</TooltipContent>
</Tooltip>
</div>
{searchOpen && (
<div className="flex items-center gap-1 rounded-md border border-sidebar-border bg-sidebar-accent/30 mx-2 mb-1 mt-0.5 px-2 py-1">
<Search className="size-3.5 shrink-0 text-muted-foreground" />
<input
ref={searchInputRef}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Escape") setSearchOpen(false)
}}
placeholder="Search chats..."
className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground/60"
/>
{searchQuery && (
<button
onClick={() => setSearchQuery("")}
className="rounded p-0.5 text-muted-foreground hover:text-sidebar-foreground transition-colors"
>
<X className="size-3.5" />
</button>
)}
</div>
)}
</div> </div>
<SidebarGroupContent className="flex-1 overflow-y-auto"> <SidebarGroupContent className="flex-1 overflow-y-auto">
{/* Background Tasks Section */} {/* Background Tasks Section */}

View file

@ -7,6 +7,8 @@ export type ActiveSection = "knowledge" | "tasks"
type SidebarSectionContextProps = { type SidebarSectionContextProps = {
activeSection: ActiveSection activeSection: ActiveSection
setActiveSection: (section: ActiveSection) => void setActiveSection: (section: ActiveSection) => void
searchOpen: boolean
setSearchOpen: (open: boolean) => void
} }
const SidebarSectionContext = React.createContext<SidebarSectionContextProps | null>(null) const SidebarSectionContext = React.createContext<SidebarSectionContextProps | null>(null)
@ -29,6 +31,7 @@ export function SidebarSectionProvider({
children: React.ReactNode children: React.ReactNode
}) { }) {
const [activeSection, setActiveSectionState] = React.useState<ActiveSection>(defaultSection) const [activeSection, setActiveSectionState] = React.useState<ActiveSection>(defaultSection)
const [searchOpen, setSearchOpen] = React.useState(false)
const setActiveSection = React.useCallback((section: ActiveSection) => { const setActiveSection = React.useCallback((section: ActiveSection) => {
setActiveSectionState(section) setActiveSectionState(section)
@ -39,8 +42,10 @@ export function SidebarSectionProvider({
() => ({ () => ({
activeSection, activeSection,
setActiveSection, setActiveSection,
searchOpen,
setSearchOpen,
}), }),
[activeSection, setActiveSection] [activeSection, setActiveSection, searchOpen]
) )
return ( return (