diff --git a/apps/x/apps/renderer/src/components/sidebar-content.tsx b/apps/x/apps/renderer/src/components/sidebar-content.tsx index 1bfc98c4..7e99049b 100644 --- a/apps/x/apps/renderer/src/components/sidebar-content.tsx +++ b/apps/x/apps/renderer/src/components/sidebar-content.tsx @@ -16,10 +16,12 @@ import { Pencil, Plug, LoaderIcon, + Search, Settings, Square, SquarePen, Trash2, + X, } from "lucide-react" import { @@ -354,6 +356,36 @@ function SyncStatusBar() { ) } +function filterTree(nodes: TreeNode[], query: string): TreeNode[] { + const lowerQuery = query.toLowerCase() + return nodes.reduce((acc, node) => { + if (node.kind === "dir") { + const filteredChildren = filterTree(node.children || [], query) + if (filteredChildren.length > 0) { + acc.push({ ...node, children: filteredChildren }) + } + } else if (node.name.toLowerCase().includes(lowerQuery)) { + acc.push(node) + } + return acc + }, []) +} + +function collectDirPaths(nodes: TreeNode[]): Set { + const paths = new Set() + for (const node of nodes) { + if (node.kind === "dir") { + paths.add(node.path) + if (node.children) { + for (const p of collectDirPaths(node.children)) { + paths.add(p) + } + } + } + } + return paths +} + export function SidebarContentPanel({ tree, selectedPath, @@ -370,6 +402,35 @@ export function SidebarContentPanel({ ...props }: SidebarContentPanelProps) { const { activeSection, setActiveSection } = useSidebarSection() + const [searchOpen, setSearchOpen] = useState(false) + const [searchQuery, setSearchQuery] = useState("") + const searchInputRef = useRef(null) + + // Auto-focus when search opens + useEffect(() => { + if (searchOpen) { + // Small delay so the input is rendered before focusing + requestAnimationFrame(() => searchInputRef.current?.focus()) + } + }, [searchOpen]) + + // Clear query when search closes or section changes + useEffect(() => { + if (!searchOpen) setSearchQuery("") + }, [searchOpen]) + useEffect(() => { + setSearchQuery("") + }, [activeSection]) + + // Compute filtered data + const filteredRuns = searchQuery + ? runs.filter((r) => (r.title || "").toLowerCase().includes(searchQuery.toLowerCase())) + : runs + const filteredBackgroundTasks = searchQuery + ? backgroundTasks.filter((t) => t.name.toLowerCase().includes(searchQuery.toLowerCase())) + : backgroundTasks + const filteredTree = searchQuery ? filterTree(tree, searchQuery) : tree + const searchExpandedPaths = searchQuery ? collectDirPaths(filteredTree) : expandedPaths return ( @@ -399,22 +460,32 @@ export function SidebarContentPanel({ {activeSection === "knowledge" && ( )} {activeSection === "tasks" && ( )} @@ -691,6 +762,11 @@ function KnowledgeSection({ onSelectFile, actions, onVoiceNoteCreated, + searchOpen, + setSearchOpen, + searchQuery, + setSearchQuery, + searchInputRef, }: { tree: TreeNode[] selectedPath: string | null @@ -698,6 +774,11 @@ function KnowledgeSection({ onSelectFile: (path: string, kind: "file" | "dir") => void actions: KnowledgeActions onVoiceNoteCreated?: (path: string) => void + searchOpen: boolean + setSearchOpen: (open: boolean) => void + searchQuery: string + setSearchQuery: (query: string) => void + searchInputRef: React.RefObject }) { const isExpanded = expandedPaths.size > 0 @@ -711,38 +792,79 @@ function KnowledgeSection({ -
- {quickActions.map((action) => ( - +
+
+ {quickActions.map((action) => ( + + + + + {action.label} + + ))} + + - {action.label} + + {isExpanded ? "Collapse All" : "Expand All"} + - ))} - - - - - - - {isExpanded ? "Collapse All" : "Expand All"} - - + + + + + Search + +
+ {searchOpen && ( +
+ + 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 && ( + + )} +
+ )}
@@ -996,6 +1118,11 @@ function TasksSection({ actions, backgroundTasks = [], selectedBackgroundTask, + searchOpen, + setSearchOpen, + searchQuery, + setSearchQuery, + searchInputRef, }: { runs: RunListItem[] currentRunId?: string | null @@ -1003,19 +1130,65 @@ function TasksSection({ actions?: TasksActions backgroundTasks?: BackgroundTaskItem[] selectedBackgroundTask?: string | null + searchOpen: boolean + setSearchOpen: (open: boolean) => void + searchQuery: string + setSearchQuery: (query: string) => void + searchInputRef: React.RefObject }) { return ( - {/* Sticky New Chat button - matches Knowledge section height */} + {/* Sticky New Chat button + search */}
- - - - - New chat - - - +
+ + + + + New chat + + + + + + + + Search + +
+ {searchOpen && ( +
+ + 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 && ( + + )} +
+ )}
{/* Background Tasks Section */}