Skip to content
Closed
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
18 changes: 2 additions & 16 deletions src/features/app/components/Sidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,25 +70,11 @@ const baseProps = {
};

describe("Sidebar", () => {
it("toggles the search bar from the header icon", () => {
it("does not render the removed sidebar search controls", () => {
render(<Sidebar {...baseProps} />);

const toggleButton = screen.getByRole("button", { name: "Toggle search" });
expect(screen.queryByRole("button", { name: "Toggle search" })).toBeNull();
expect(screen.queryByLabelText("Search projects")).toBeNull();

fireEvent.click(toggleButton);
const input = screen.getByLabelText("Search projects") as HTMLInputElement;
expect(input).toBeTruthy();

fireEvent.change(input, { target: { value: "alpha" } });
expect(input.value).toBe("alpha");

fireEvent.click(toggleButton);
expect(screen.queryByLabelText("Search projects")).toBeNull();

fireEvent.click(toggleButton);
const reopened = screen.getByLabelText("Search projects") as HTMLInputElement;
expect(reopened.value).toBe("");
});

it("opens thread sort menu from the header filter button", () => {
Expand Down
162 changes: 14 additions & 148 deletions src/features/app/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { FolderOpen } from "lucide-react";
import Copy from "lucide-react/dist/esm/icons/copy";
import GitBranch from "lucide-react/dist/esm/icons/git-branch";
import Plus from "lucide-react/dist/esm/icons/plus";
import X from "lucide-react/dist/esm/icons/x";
import {
PopoverMenuItem,
PopoverSurface,
Expand All @@ -33,7 +32,6 @@ import { useMenuController } from "../hooks/useMenuController";
import { useSidebarMenus } from "../hooks/useSidebarMenus";
import { useSidebarScrollFade } from "../hooks/useSidebarScrollFade";
import { useThreadRows } from "../hooks/useThreadRows";
import { useDebouncedValue } from "../../../hooks/useDebouncedValue";
import { getUsageLabels } from "../utils/usageLabels";
import { formatRelativeTimeShort } from "../../../utils/time";
import type { ThreadStatusById } from "../../../utils/threadStatus";
Expand Down Expand Up @@ -180,8 +178,6 @@ export const Sidebar = memo(function Sidebar({
const [expandedWorkspaces, setExpandedWorkspaces] = useState(
new Set<string>(),
);
const [searchQuery, setSearchQuery] = useState("");
const [isSearchOpen, setIsSearchOpen] = useState(false);
const [addMenuAnchor, setAddMenuAnchor] = useState<{
workspaceId: string;
top: number;
Expand Down Expand Up @@ -228,8 +224,6 @@ export const Sidebar = memo(function Sidebar({
creditsLabel,
showWeekly,
} = getUsageLabels(accountRateLimits, usageShowRemaining);
const debouncedQuery = useDebouncedValue(searchQuery, 150);
const normalizedQuery = debouncedQuery.trim().toLowerCase();
const pendingUserInputKeys = useMemo(
() =>
new Set(
Expand All @@ -244,48 +238,6 @@ export const Sidebar = memo(function Sidebar({
[userInputRequests],
);

const isWorkspaceMatch = useCallback(
(workspace: WorkspaceInfo) => {
if (!normalizedQuery) {
return true;
}
return workspace.name.toLowerCase().includes(normalizedQuery);
},
[normalizedQuery],
);

const renderHighlightedName = useCallback(
(name: string) => {
if (!normalizedQuery) {
return name;
}
const lower = name.toLowerCase();
const parts: React.ReactNode[] = [];
let cursor = 0;
let matchIndex = lower.indexOf(normalizedQuery, cursor);

while (matchIndex !== -1) {
if (matchIndex > cursor) {
parts.push(name.slice(cursor, matchIndex));
}
parts.push(
<span key={`${matchIndex}-${cursor}`} className="workspace-name-match">
{name.slice(matchIndex, matchIndex + normalizedQuery.length)}
</span>,
);
cursor = matchIndex + normalizedQuery.length;
matchIndex = lower.indexOf(normalizedQuery, cursor);
}

if (cursor < name.length) {
parts.push(name.slice(cursor));
}

return parts.length ? parts : name;
},
[normalizedQuery],
);

const accountEmail = accountInfo?.email?.trim() ?? "";
const accountButtonLabel = accountEmail
? accountEmail
Expand All @@ -310,9 +262,6 @@ export const Sidebar = memo(function Sidebar({
}> = [];

workspaces.forEach((workspace) => {
if (!isWorkspaceMatch(workspace)) {
return;
}
const threads = threadsByWorkspace[workspace.id] ?? [];
if (!threads.length) {
return;
Expand Down Expand Up @@ -369,41 +318,8 @@ export const Sidebar = memo(function Sidebar({
getThreadRows,
getPinTimestamp,
pinnedThreadsVersion,
isWorkspaceMatch,
]);

const cloneSourceIdsMatchingQuery = useMemo(() => {
if (!normalizedQuery) {
return new Set<string>();
}
const ids = new Set<string>();
workspaces.forEach((workspace) => {
const sourceId = workspace.settings.cloneSourceWorkspaceId?.trim();
if (!sourceId) {
return;
}
if (isWorkspaceMatch(workspace)) {
ids.add(sourceId);
}
});
return ids;
}, [isWorkspaceMatch, normalizedQuery, workspaces]);

const filteredGroupedWorkspaces = useMemo(
() =>
groupedWorkspaces
.map((group) => ({
...group,
workspaces: group.workspaces.filter(
(workspace) =>
isWorkspaceMatch(workspace) ||
cloneSourceIdsMatchingQuery.has(workspace.id),
),
}))
.filter((group) => group.workspaces.length > 0),
[cloneSourceIdsMatchingQuery, groupedWorkspaces, isWorkspaceMatch],
);

const getSortTimestamp = useCallback(
(thread: ThreadSummary | undefined) => {
if (!thread) {
Expand Down Expand Up @@ -443,15 +359,10 @@ export const Sidebar = memo(function Sidebar({
cloneWorkspacesBySourceId.set(sourceId, list);
});

filteredGroupedWorkspaces.forEach((group) => {
groupedWorkspaces.forEach((group) => {
group.workspaces.forEach((workspace) => {
const rootThreads = threadsByWorkspace[workspace.id] ?? [];
const visibleClones =
normalizedQuery && !isWorkspaceMatch(workspace)
? (cloneWorkspacesBySourceId.get(workspace.id) ?? []).filter((clone) =>
isWorkspaceMatch(clone),
)
: (cloneWorkspacesBySourceId.get(workspace.id) ?? []);
const visibleClones = cloneWorkspacesBySourceId.get(workspace.id) ?? [];
let hasThreads = rootThreads.length > 0;
let timestamp = getSortTimestamp(rootThreads[0]);

Expand All @@ -472,19 +383,17 @@ export const Sidebar = memo(function Sidebar({
});
return activityById;
}, [
filteredGroupedWorkspaces,
getSortTimestamp,
isWorkspaceMatch,
normalizedQuery,
groupedWorkspaces,
threadsByWorkspace,
workspaces,
]);

const sortedGroupedWorkspaces = useMemo(() => {
if (threadListOrganizeMode !== "by_project_activity") {
return filteredGroupedWorkspaces;
return groupedWorkspaces;
}
return filteredGroupedWorkspaces.map((group) => ({
return groupedWorkspaces.map((group) => ({
...group,
workspaces: group.workspaces.slice().sort((a, b) => {
const aActivity = workspaceActivityById.get(a.id) ?? {
Expand All @@ -505,7 +414,7 @@ export const Sidebar = memo(function Sidebar({
return a.name.localeCompare(b.name);
}),
}));
}, [filteredGroupedWorkspaces, threadListOrganizeMode, workspaceActivityById]);
}, [groupedWorkspaces, threadListOrganizeMode, workspaceActivityById]);

const flatThreadRows = useMemo(() => {
if (threadListOrganizeMode !== "threads_only") {
Expand All @@ -519,7 +428,7 @@ export const Sidebar = memo(function Sidebar({
rows: FlatThreadRow[];
}> = [];

filteredGroupedWorkspaces.forEach((group) => {
groupedWorkspaces.forEach((group) => {
group.workspaces.forEach((workspace) => {
const threads = threadsByWorkspace[workspace.id] ?? [];
if (!threads.length) {
Expand Down Expand Up @@ -591,10 +500,10 @@ export const Sidebar = memo(function Sidebar({
})
.flatMap((group) => group.rows);
}, [
filteredGroupedWorkspaces,
getPinTimestamp,
getSortTimestamp,
getThreadRows,
groupedWorkspaces,
pinnedThreadsVersion,
threadListOrganizeMode,
threadsByWorkspace,
Expand All @@ -606,15 +515,13 @@ export const Sidebar = memo(function Sidebar({
flatThreadRows,
threadsByWorkspace,
expandedWorkspaces,
normalizedQuery,
threadListOrganizeMode,
],
[
sortedGroupedWorkspaces,
flatThreadRows,
threadsByWorkspace,
expandedWorkspaces,
normalizedQuery,
threadListOrganizeMode,
],
);
Expand All @@ -636,7 +543,7 @@ export const Sidebar = memo(function Sidebar({
const groupedWorkspacesForRender =
threadListOrganizeMode === "by_project_activity"
? sortedGroupedWorkspaces
: filteredGroupedWorkspaces;
: groupedWorkspaces;
const isThreadsOnlyMode = threadListOrganizeMode === "threads_only";

const handleAllThreadsAddMenuToggle = useCallback(
Expand Down Expand Up @@ -669,7 +576,6 @@ export const Sidebar = memo(function Sidebar({
},
[onAddAgent],
);
const isSearchActive = Boolean(normalizedQuery);

const worktreesByParent = useMemo(() => {
const worktrees = new Map<string, WorkspaceInfo[]>();
Expand Down Expand Up @@ -779,15 +685,9 @@ export const Sidebar = memo(function Sidebar({
};
}, [allThreadsAddMenuAnchor]);

useEffect(() => {
if (!isSearchOpen && searchQuery) {
setSearchQuery("");
}
}, [isSearchOpen, searchQuery]);

return (
<aside
className={`sidebar${isSearchOpen ? " search-open" : ""}`}
className="sidebar"
ref={workspaceDropTargetRef}
onDragOver={onWorkspaceDragOver}
onDragEnter={onWorkspaceDragEnter}
Expand All @@ -798,8 +698,6 @@ export const Sidebar = memo(function Sidebar({
<SidebarHeader
onSelectHome={onSelectHome}
onAddWorkspace={onAddWorkspace}
onToggleSearch={() => setIsSearchOpen((prev) => !prev)}
isSearchOpen={isSearchOpen}
threadListSortKey={threadListSortKey}
onSetThreadListSortKey={onSetThreadListSortKey}
threadListOrganizeMode={threadListOrganizeMode}
Expand All @@ -808,30 +706,6 @@ export const Sidebar = memo(function Sidebar({
refreshDisabled={refreshDisabled || refreshInProgress}
refreshInProgress={refreshInProgress}
/>
<div className={`sidebar-search${isSearchOpen ? " is-open" : ""}`}>
{isSearchOpen && (
<input
className="sidebar-search-input"
value={searchQuery}
onChange={(event) => setSearchQuery(event.target.value)}
placeholder="Search projects"
aria-label="Search projects"
data-tauri-drag-region="false"
autoFocus
/>
)}
{isSearchOpen && searchQuery.length > 0 && (
<button
type="button"
className="sidebar-search-clear"
onClick={() => setSearchQuery("")}
aria-label="Clear search"
data-tauri-drag-region="false"
>
<X size={12} aria-hidden />
</button>
)}
</div>
<div
className={`workspace-drop-overlay${
isWorkspaceDropActive ? " is-active" : ""
Expand Down Expand Up @@ -981,10 +855,6 @@ export const Sidebar = memo(function Sidebar({
isLoadingThreads && threads.length === 0;
const isPaging = threadListPagingByWorkspace[entry.id] ?? false;
const clones = clonesBySource.get(entry.id) ?? [];
const visibleClones =
isSearchActive && !isWorkspaceMatch(entry)
? clones.filter((clone) => isWorkspaceMatch(clone))
: clones;
const worktrees = worktreesByParent.get(entry.id) ?? [];
const addMenuOpen = addMenuAnchor?.workspaceId === entry.id;
const isDraftNewAgent = newAgentDraftWorkspaceId === entry.id;
Expand All @@ -1001,7 +871,7 @@ export const Sidebar = memo(function Sidebar({
<WorkspaceCard
key={entry.id}
workspace={entry}
workspaceName={renderHighlightedName(entry.name)}
workspaceName={entry.name}
isActive={entry.id === activeWorkspaceId}
isCollapsed={isCollapsed}
addMenuOpen={addMenuOpen}
Expand Down Expand Up @@ -1078,9 +948,9 @@ export const Sidebar = memo(function Sidebar({
<span className="thread-name">New Agent</span>
</div>
)}
{visibleClones.length > 0 && (
{clones.length > 0 && (
<WorktreeSection
worktrees={visibleClones}
worktrees={clones}
deletingWorktreeIds={deletingWorktreeIds}
threadsByWorkspace={threadsByWorkspace}
threadStatusById={threadStatusById}
Expand Down Expand Up @@ -1171,11 +1041,7 @@ export const Sidebar = memo(function Sidebar({
);
})}
{!groupedWorkspacesForRender.length && (
<div className="empty">
{isSearchActive
? "No projects match your search."
: "Add a workspace to start."}
</div>
<div className="empty">Add a workspace to start.</div>
)}
{isThreadsOnlyMode &&
groupedWorkspacesForRender.length > 0 &&
Expand Down
Loading
Loading