From 5df37c552de402c39e5f01fb361f0fe1ecd893d6 Mon Sep 17 00:00:00 2001 From: Bashamega Date: Wed, 11 Mar 2026 12:07:34 +0200 Subject: [PATCH 1/4] feat: Adds Search to the sidebar --- apps/web/src/components/Sidebar.tsx | 101 ++++++++++++++++++++++++---- 1 file changed, 88 insertions(+), 13 deletions(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 8b68c3b80..6909ba23a 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -9,6 +9,8 @@ import { SquarePenIcon, TerminalIcon, TriangleAlertIcon, + SearchIcon, + X, } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState, type MouseEvent } from "react"; import { @@ -84,6 +86,8 @@ import { useThreadSelectionStore } from "../threadSelectionStore"; import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "../worktreeCleanup"; import { isNonEmpty as isNonEmptyString } from "effect/String"; import { resolveThreadStatusPill, shouldClearThreadSelectionOnMouseDown } from "./Sidebar.logic"; +import { Project } from "~/types"; +import { Input } from "./ui/input"; const EMPTY_KEYBINDINGS: ResolvedKeybindingsConfig = []; const THREAD_PREVIEW_LIMIT = 6; @@ -310,6 +314,7 @@ export default function Sidebar() { const setSelectionAnchor = useThreadSelectionStore((s) => s.setAnchor); const shouldBrowseForProjectImmediately = isElectron; const shouldShowProjectPathEntry = addingProject && !shouldBrowseForProjectImmediately; + const [searchValue, setSearchValue] = useState(""); // state for search/filter const pendingApprovalByThreadId = useMemo(() => { const map = new Map(); for (const thread of threads) { @@ -1259,6 +1264,45 @@ export default function Sidebar() { ); + // Filtering logic for projects and threads + const filteredProjects = useMemo(() => { + if (!searchValue.trim()) return projects; + // Filter projects where the project name matches or any thread matches + const normalizedSearch = searchValue.toLowerCase(); + return projects.filter((project) => { + const nameMatch = project.name.toLowerCase().includes(normalizedSearch); + // Find threads for this project + const projectThreads = threads.filter((thread) => thread.projectId === project.id); + const threadMatch = projectThreads.some((thread) => + thread.title.toLowerCase().includes(normalizedSearch), + ); + return nameMatch || threadMatch; + }); + }, [projects, threads, searchValue]); + + // Filtering threads within a project if searching, otherwise normal + const getProjectThreads = useCallback( + (project: Project) => { + const projectThreads = threads + .filter((thread) => thread.projectId === project.id) + .toSorted((a, b) => { + const byDate = new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); + if (byDate !== 0) return byDate; + return b.id.localeCompare(a.id); + }); + + if (!searchValue.trim()) { + return projectThreads; + } + // Only threads matching search for this project + const normalizedSearch = searchValue.toLowerCase(); + return projectThreads.filter((thread) => + thread.title.toLowerCase().includes(normalizedSearch), + ); + }, + [threads, searchValue], + ); + return ( <> {isElectron ? ( @@ -1317,6 +1361,35 @@ export default function Sidebar() { ) : null} + {/* Search Input UI */} +
+ +
+ setSearchValue(e.target.value)} + placeholder="Search projects/threads" + aria-label="Search projects and threads" + className="pr-6" + autoComplete="off" + /> + {searchValue && ( + + )} +
+
Projects @@ -1418,20 +1491,14 @@ export default function Sidebar() { > project.id)} + items={filteredProjects.map((project) => project.id)} strategy={verticalListSortingStrategy} > - {projects.map((project) => { - const projectThreads = threads - .filter((thread) => thread.projectId === project.id) - .toSorted((a, b) => { - const byDate = - new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); - if (byDate !== 0) return byDate; - return b.id.localeCompare(a.id); - }); + {filteredProjects.map((project) => { + const projectThreads = getProjectThreads(project); const isThreadListExpanded = expandedThreadListsByProject.has(project.id); - const hasHiddenThreads = projectThreads.length > THREAD_PREVIEW_LIMIT; + const hasHiddenThreads = + projectThreads.length > THREAD_PREVIEW_LIMIT && !searchValue.trim(); const visibleThreads = hasHiddenThreads && !isThreadListExpanded ? projectThreads.slice(0, THREAD_PREVIEW_LIMIT) @@ -1717,6 +1784,14 @@ export default function Sidebar() { )} + {/* If search is active and no threads to show in this project, show "No threads found" */} + {searchValue.trim() && visibleThreads.length === 0 && ( + +
+ No threads found +
+
+ )} @@ -1728,9 +1803,9 @@ export default function Sidebar() {
- {projects.length === 0 && !shouldShowProjectPathEntry && ( + {filteredProjects.length === 0 && !shouldShowProjectPathEntry && (
- No projects yet + {searchValue.trim() ? "No projects or threads found" : "No projects yet"}
)} From da1124222f573d4590de23b51f708fdba22e1eb3 Mon Sep 17 00:00:00 2001 From: Bashamega Date: Wed, 11 Mar 2026 12:17:07 +0200 Subject: [PATCH 2/4] Allow the placeholder to be completely visible --- apps/web/src/components/Sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 6909ba23a..2158087f0 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -1374,7 +1374,7 @@ export default function Sidebar() { onChange={(e) => setSearchValue(e.target.value)} placeholder="Search projects/threads" aria-label="Search projects and threads" - className="pr-6" + className={searchValue && "pr-6"} autoComplete="off" /> {searchValue && ( From 85f91d5d2f64c2f8368b1be85a3d0d2cca701282 Mon Sep 17 00:00:00 2001 From: Bashamega Date: Wed, 11 Mar 2026 12:22:47 +0200 Subject: [PATCH 3/4] Remove unused search icon --- apps/web/src/components/Sidebar.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 2158087f0..37afdeb6b 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -9,7 +9,6 @@ import { SquarePenIcon, TerminalIcon, TriangleAlertIcon, - SearchIcon, X, } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState, type MouseEvent } from "react"; From 3d6fd97d8b2f3a3d71b59d063f00480f42a3d9e4 Mon Sep 17 00:00:00 2001 From: Bashamega Date: Thu, 12 Mar 2026 06:32:21 +0200 Subject: [PATCH 4/4] update --- apps/web/src/components/Sidebar.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 37afdeb6b..04cde4606 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -41,7 +41,7 @@ import { useLocation, useNavigate, useParams } from "@tanstack/react-router"; import { useAppSettings } from "../appSettings"; import { isElectron } from "../env"; import { APP_STAGE_LABEL, APP_VERSION } from "../branding"; -import { isMacPlatform, newCommandId, newProjectId, newThreadId } from "../lib/utils"; +import { cn, isMacPlatform, newCommandId, newProjectId, newThreadId } from "../lib/utils"; import { useStore } from "../store"; import { isChatNewLocalShortcut, isChatNewShortcut, shortcutLabelForCommand } from "../keybindings"; import { derivePendingApprovals, derivePendingUserInputs } from "../session-logic"; @@ -1366,14 +1366,14 @@ export default function Sidebar() { Search projects and threads
- setSearchValue(e.target.value)} placeholder="Search projects/threads" aria-label="Search projects and threads" - className={searchValue && "pr-6"} + className="border-0 border-b border-primary focus:ring-0 focus:border-accent focus:outline-none" autoComplete="off" /> {searchValue && (