Skip to content

Commit 46347f6

Browse files
ryaneggzclaude
andcommitted
feat: redesign sidebar to consolidate threads under projects (#918)
Replace flat Projects/Threads sidebar sections with a project-centric tree where threads are nested under their parent project. Key changes: - Add useProjectThreads hook for lazy per-project thread fetching with pagination - Create ProjectTreeGroup component with collapsible project nodes and nested threads - Projects section expanded by default; Threads section collapsed (orphan threads) - Empty state shows "No projects yet" with Create Project CTA for new users - Per-project "Create new thread" and "Show more" buttons - Thread move between projects syncs both sidebar sections Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: ryaneggz <kre8mymedia@gmail.com>
1 parent c88b4c9 commit 46347f6

3 files changed

Lines changed: 754 additions & 195 deletions

File tree

frontend/src/components/drawers/app-sidebar.tsx

Lines changed: 21 additions & 195 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import {
1010
MoreHorizontal,
1111
Trash2,
1212
FolderKanban,
13-
Plus,
14-
FileText,
1513
Loader2,
1614
Search,
1715
Calendar,
@@ -67,6 +65,8 @@ import { AxiosResponse } from "axios";
6765
import { useScheduleExecutions } from "@/hooks/useScheduleExecutions";
6866
import { useSchedules } from "@/hooks/useSchedules";
6967
import { ScheduleSidebarItem } from "@/components/sidebar/ScheduleSidebarItem";
68+
import { ProjectTreeGroup } from "@/components/sidebar/ProjectTreeGroup";
69+
import { useProjectThreads } from "@/hooks/useProjectThreads";
7070

7171
interface AssistantItemProps {
7272
agent: Agent;
@@ -361,197 +361,7 @@ function ThreadItem({ thread, projects }: ThreadItemProps) {
361361
);
362362
}
363363

364-
interface ProjectItemProps {
365-
project: Project;
366-
onAddSource: (project: Project) => void;
367-
}
368-
369-
function ProjectItem({ project, onAddSource }: ProjectItemProps) {
370-
const { selectedProject, selectProject, handleDeleteProject } =
371-
useProjectContext();
372-
const { setMetadata } = useChatContext();
373-
const { isMobile, setOpenMobile } = useSidebar();
374-
const navigate = useNavigate();
375-
376-
const isSelected = selectedProject?.id === project.id;
377-
const sourceCount = project.sources?.length || 0;
378-
379-
const handleProjectClick = () => {
380-
selectProject(project);
381-
setMetadata((prev: any) => ({
382-
...prev,
383-
project_id: project.id,
384-
}));
385-
if (isMobile) {
386-
setOpenMobile(false);
387-
}
388-
// Navigate to project page
389-
navigate(`/p/${project.id}`);
390-
};
391-
392-
const handleDeleteClick = async () => {
393-
if (window.confirm("Are you sure you want to delete this project?")) {
394-
const deleted = await handleDeleteProject(project.id!);
395-
if (deleted) {
396-
setMetadata((prev: any) => {
397-
const { project_id: _project_id, ...rest } = prev;
398-
return rest;
399-
});
400-
}
401-
}
402-
};
403-
404-
const relativeTime = project.updated_at
405-
? formatDistanceToNow(new Date(project.updated_at), { addSuffix: true })
406-
: "";
407-
408-
return (
409-
<SidebarMenuItem className="mb-1 group/project relative">
410-
<SidebarMenuButton
411-
asChild
412-
isActive={isSelected}
413-
className={`h-auto px-3 py-3 rounded-lg border transition-all ${
414-
isSelected
415-
? "bg-sidebar-accent border-sidebar-accent shadow-sm"
416-
: "bg-transparent border-sidebar-border hover:bg-sidebar-accent/50 hover:border-sidebar-accent/50"
417-
}`}
418-
>
419-
<button
420-
onClick={handleProjectClick}
421-
className="flex items-start gap-2.5 w-full"
422-
>
423-
<div className="flex flex-col min-w-0 flex-1 gap-1.5">
424-
<div className="flex items-start justify-between gap-2 w-full">
425-
<span
426-
className={`text-sm leading-tight line-clamp-2 ${
427-
isSelected
428-
? "font-semibold text-sidebar-accent-foreground"
429-
: "font-medium text-sidebar-foreground"
430-
}`}
431-
>
432-
{project.name}
433-
</span>
434-
{relativeTime && (
435-
<span className="text-[10px] text-sidebar-foreground/40 shrink-0 font-normal mt-0.5 whitespace-nowrap">
436-
{relativeTime}
437-
</span>
438-
)}
439-
</div>
440-
{project.description && (
441-
<span className="text-xs text-sidebar-foreground/60 truncate">
442-
{project.description}
443-
</span>
444-
)}
445-
<div className="flex items-center gap-2.5 text-[11px] text-sidebar-foreground/50">
446-
<div className="flex items-center gap-1">
447-
<FileText className="w-3 h-3" />
448-
<span>
449-
{sourceCount} source{sourceCount !== 1 ? "s" : ""}
450-
</span>
451-
</div>
452-
</div>
453-
</div>
454-
</button>
455-
</SidebarMenuButton>
456-
<DropdownMenu>
457-
<DropdownMenuTrigger asChild>
458-
<Button
459-
variant="ghost"
460-
size="icon"
461-
className="absolute right-2 bottom-2 opacity-0 group-hover/project:opacity-100 transition-opacity h-6 w-6"
462-
onClick={(e) => e.stopPropagation()}
463-
>
464-
<MoreHorizontal className="h-3.5 w-3.5 text-sidebar-foreground/60" />
465-
</Button>
466-
</DropdownMenuTrigger>
467-
<DropdownMenuContent align="end" className="w-48">
468-
<DropdownMenuItem
469-
onClick={() => onAddSource(project)}
470-
className="cursor-pointer"
471-
>
472-
<Plus className="mr-2 h-4 w-4" />
473-
Add Source
474-
</DropdownMenuItem>
475-
<DropdownMenuItem
476-
onClick={handleDeleteClick}
477-
className="text-red-300 focus:text-red-400 hover:text-red-300 cursor-pointer"
478-
>
479-
<Trash2 className="mr-2 h-4 w-4" />
480-
Delete
481-
</DropdownMenuItem>
482-
</DropdownMenuContent>
483-
</DropdownMenu>
484-
</SidebarMenuItem>
485-
);
486-
}
487-
488-
interface ProjectsCollapsibleGroupProps {
489-
projects: Project[];
490-
onCreateProject: () => void;
491-
onAddSource: (project: Project) => void;
492-
}
493-
494-
function ProjectsCollapsibleGroup({
495-
projects,
496-
onCreateProject,
497-
onAddSource,
498-
}: ProjectsCollapsibleGroupProps) {
499-
return (
500-
<Collapsible
501-
key="projects"
502-
title={`Projects (${projects.length} items)`}
503-
defaultOpen={false}
504-
className="group/collapsible"
505-
data-tour="projects-section"
506-
>
507-
<SidebarGroup className="border-b border-sidebar-border">
508-
<SidebarGroupLabel
509-
asChild
510-
className={`
511-
group/label text-sidebar-foreground hover:bg-sidebar-accent
512-
hover:text-sidebar-accent-foreground text-sm
513-
`}
514-
>
515-
<CollapsibleTrigger>
516-
<FolderKanban className="w-4 h-4 mr-2" />
517-
Projects
518-
<ChevronRight className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-90" />
519-
</CollapsibleTrigger>
520-
</SidebarGroupLabel>
521-
<CollapsibleContent>
522-
<SidebarGroupContent className="px-1 pt-2">
523-
<div className="px-2 pb-2">
524-
<Button
525-
variant="outline"
526-
size="sm"
527-
className="w-full justify-start gap-2"
528-
onClick={onCreateProject}
529-
>
530-
<Plus className="h-4 w-4" />
531-
Create Project
532-
</Button>
533-
</div>
534-
<SidebarMenu className="gap-0">
535-
{projects.length > 0 ? (
536-
projects.map((project) => (
537-
<ProjectItem
538-
key={project.id}
539-
project={project}
540-
onAddSource={onAddSource}
541-
/>
542-
))
543-
) : (
544-
<div className="px-3 py-4 text-center text-sm text-sidebar-foreground/50">
545-
No projects yet
546-
</div>
547-
)}
548-
</SidebarMenu>
549-
</SidebarGroupContent>
550-
</CollapsibleContent>
551-
</SidebarGroup>
552-
</Collapsible>
553-
);
554-
}
364+
// ProjectItem and ProjectsCollapsibleGroup replaced by ProjectTreeGroup
555365

556366
interface CollapsibleGroupProps {
557367
title: string;
@@ -617,7 +427,7 @@ function CollapsibleGroup({
617427
<Collapsible
618428
key={title}
619429
title={`${title} (${items.length} items)`}
620-
defaultOpen={type === "threads"}
430+
defaultOpen={false}
621431
className="group/collapsible"
622432
{...(type === "threads" ? { "data-tour": "threads-section" } : {})}
623433
>
@@ -813,6 +623,15 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
813623
} = useChatContext();
814624
const { projects, useEffectGetProjects } = useProjectContext();
815625
const onLogoLinkClick = useLinkClick("/");
626+
const {
627+
projectThreadsMap,
628+
fetchProjectThreads,
629+
loadMoreProjectThreads,
630+
toggleProjectExpanded,
631+
isProjectExpanded,
632+
addThreadToProject,
633+
removeThreadFromProject,
634+
} = useProjectThreads();
816635
// Modal state
817636
const [isCreateProjectModalOpen, setIsCreateProjectModalOpen] =
818637
useState(false);
@@ -901,10 +720,17 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
901720
</SidebarGroupLabel>
902721
</SidebarGroup>
903722

904-
<ProjectsCollapsibleGroup
723+
<ProjectTreeGroup
905724
projects={projects}
906725
onCreateProject={() => setIsCreateProjectModalOpen(true)}
907726
onAddSource={handleAddSource}
727+
projectThreadsMap={projectThreadsMap}
728+
fetchProjectThreads={fetchProjectThreads}
729+
loadMoreProjectThreads={loadMoreProjectThreads}
730+
toggleProjectExpanded={toggleProjectExpanded}
731+
isProjectExpanded={isProjectExpanded}
732+
addThreadToProject={addThreadToProject}
733+
removeThreadFromProject={removeThreadFromProject}
908734
/>
909735
<SchedulesCollapsibleGroup />
910736
<CollapsibleGroup

0 commit comments

Comments
 (0)