import { SessionOptions } from "./session-options"; const EMPTY_REPOS: SessionSidebarRepoToggle[] = []; import type { ComponentPropsWithRef, MouseEvent, ReactNode } from "react"; import { useId, useMemo } from "react"; import * as format from "../../lib/format"; import { AnimatePresence, m as motion, useReducedMotion } from "motion/react"; import { SessionItem } from "../session-item/session-item"; import { SessionItemLoading } from "../session-item/session-item-loading"; import type { SidebarRow } from "../session-item/session-item.types"; import { cn } from "../../lib/utils"; import { Avatar, AvatarFallback, AvatarImage } from "../../ui/avatar"; import { Button } from "../../ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "../../ui/dropdown-menu"; import { Skeleton } from "../../ui/skeleton"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../../ui/tooltip"; import { isSidebarRowSelected } from "./selection"; import type { SessionSidebarRepo } from "./session-sidebar"; import { IconArchive, IconChevronDown, IconEye, IconPlus, IconX } from "../../icons"; export type SessionSidebarUnifiedGroupId = "pinned" | "mine" | "team" | "archived"; export type SessionSidebarUnifiedRepoSection = { repo: SessionSidebarRepo; rows: SidebarRow[]; }; export type SessionSidebarUnifiedGroup = { id: SessionSidebarUnifiedGroupId; label: string; collapsed?: boolean; repos: SessionSidebarUnifiedRepoSection[]; }; export type SessionSidebarRepoToggle = { repo: SessionSidebarRepo; visible: boolean; }; export type SessionSidebarUnifiedProps = & Omit, "children" | "onSelect"> & { groups: SessionSidebarUnifiedGroup[]; repos?: SessionSidebarRepoToggle[]; loading?: boolean; selectedUid?: SidebarRow["uid"]; onSelect?: (row: SidebarRow) => void; onPin?: (row: SidebarRow) => void; onUnpin?: (row: SidebarRow) => void; onArchive?: (row: SidebarRow) => void; onFork?: (row: SidebarRow) => void; onInfo?: (row: SidebarRow) => void; onLeave?: (row: SidebarRow) => void; onDelete?: (row: SidebarRow) => void; onRename?: (row: SidebarRow) => void; onToggleGroup?: (id: SessionSidebarUnifiedGroupId) => void; onToggleRepo?: (repo: SessionSidebarRepo) => void; onRemoveRepo?: (repo: SessionSidebarRepo) => void; onAddRepo?: () => void; /** Per-repo "New session" (only offered on My Sessions repo headers). */ onNewSession?: (repo: SessionSidebarRepo) => void; /** When provided, a "…" menu button offers "Continue a pull request…". */ onContinuePr?: (repo: SessionSidebarRepo) => void; /** When provided, the "…" menu offers "Start from an issue…". */ onStartIssue?: (repo: SessionSidebarRepo) => void; empty?: ReactNode; }; const CHEVRON_TRANSITION = { duration: 0.15, ease: "easeOut" as const }; const CHEVRON_TRANSITION_REDUCED = { duration: 0, ease: "easeOut" as const }; const GROUP_TRANSITION = { duration: 0.18, ease: "easeOut" as const }; const GROUP_TRANSITION_REDUCED = { duration: 0, ease: "easeOut" as const }; const ACTION_BUTTON_CLASS = "size-5.5 border border-transparent bg-clip-padding text-muted-foreground shadow-none hover:border-[var(--edge)] hover:bg-background/50 hover:text-sidebar-foreground hover:shadow-[0_1px_2px_rgb(0_0_0/0.08)] hover:backdrop-blur-[1px] focus-visible:border-[var(--edge)] focus-visible:bg-background/50 focus-visible:text-sidebar-foreground focus-visible:shadow-[0_1px_2px_rgb(0_0_0/0.08)] focus-visible:backdrop-blur-[1px] dark:hover:border-black/20 dark:hover:bg-background/50 dark:focus-visible:border-black/20 dark:focus-visible:bg-background/50"; function count(groups: SessionSidebarUnifiedGroup[]) { let total = 0; for (let group of groups) { for (let section of group.repos) total += section.rows.length; } return total; } function byCreation(a: SidebarRow, b: SidebarRow) { return b.createdAt - a.createdAt || b.uid.localeCompare(a.uid); } function rowKey(row: SidebarRow) { return row.renderKey ?? row.uid; } function fallback(name: string) { return name.slice(0, 2).toUpperCase(); } function byOrg(repos: SessionSidebarRepoToggle[]) { let groups = new Map(); for (let toggle of repos) { let list = groups.get(toggle.repo.org); if (list) list.push(toggle); else groups.set(toggle.repo.org, [toggle]); } return [...groups]; } function RepoAvatar({ repo, className }: { repo: SessionSidebarRepo; className?: string }) { return ( {fallback(repo.org)} ); } function RepoToggleRow({ toggle, onToggleRepo, onRemoveRepo }: { toggle: SessionSidebarRepoToggle; onToggleRepo?: (repo: SessionSidebarRepo) => void; onRemoveRepo?: (repo: SessionSidebarRepo) => void; }) { function toggleVisible() { onToggleRepo?.(toggle.repo); } function remove(event: MouseEvent) { event.preventDefault(); event.stopPropagation(); onRemoveRepo?.(toggle.repo); } return (
{onRemoveRepo && ( } > Remove )}
); } function RepositoriesPicker( { repos = EMPTY_REPOS, onToggleRepo, onRemoveRepo, onAddRepo }: { repos?: SessionSidebarRepoToggle[]; onToggleRepo?: (repo: SessionSidebarRepo) => void; onRemoveRepo?: (repo: SessionSidebarRepo) => void; onAddRepo?: () => void; }, ) { let orgGroups = useMemo(() => byOrg(repos), [repos]); return ( } > Repositories {onAddRepo && ( <> Add repository {orgGroups.length > 0 && } )} {orgGroups.map(([org, list], index) => ( {index > 0 && } {org} {list.map((toggle) => ( ))} ))} ); } function RepoHeader( { repo, onNewSession, onContinuePr, onStartIssue }: { repo: SessionSidebarRepo; onNewSession?: (repo: SessionSidebarRepo) => void; onContinuePr?: (repo: SessionSidebarRepo) => void; onStartIssue?: (repo: SessionSidebarRepo) => void; }, ) { return (
{repo.org}/ {repo.name} {onNewSession && (
onNewSession(repo)} > } /> New session onContinuePr(repo) : undefined} onStartIssue={onStartIssue ? () => onStartIssue(repo) : undefined} />
)}
); } function RepoSection({ section, pinned, archived, showActions, selectedUid, onSelect, onPin, onUnpin, onArchive, onFork, onInfo, onLeave, onDelete, onRename, onNewSession, onContinuePr, onStartIssue, }: { section: SessionSidebarUnifiedRepoSection; pinned: boolean; archived: boolean; showActions: boolean; selectedUid?: SidebarRow["uid"]; onSelect?: (row: SidebarRow) => void; onPin?: (row: SidebarRow) => void; onUnpin?: (row: SidebarRow) => void; onArchive?: (row: SidebarRow) => void; onFork?: (row: SidebarRow) => void; onInfo?: (row: SidebarRow) => void; onLeave?: (row: SidebarRow) => void; onDelete?: (row: SidebarRow) => void; onRename?: (row: SidebarRow) => void; onNewSession?: (repo: SessionSidebarRepo) => void; onContinuePr?: (repo: SessionSidebarRepo) => void; onStartIssue?: (repo: SessionSidebarRepo) => void; }) { let rows = useMemo(() => pinned ? section.rows : [...section.rows].sort(byCreation), [ section.rows, pinned, ]); return (
{rows.map((row) => row.lifecycle === "creating" ? ( ) : ( onSelect?.(row)} /> ) )}
); } function Group({ group, selectedUid, onSelect, onPin, onUnpin, onArchive, onFork, onInfo, onLeave, onDelete, onRename, onToggleGroup, onNewSession, onContinuePr, onStartIssue, }: & Pick< SessionSidebarUnifiedProps, | "selectedUid" | "onSelect" | "onPin" | "onUnpin" | "onArchive" | "onFork" | "onInfo" | "onLeave" | "onDelete" | "onRename" | "onToggleGroup" | "onNewSession" | "onContinuePr" | "onStartIssue" > & { group: SessionSidebarUnifiedGroup }) { let pinned = group.id === "pinned"; let archived = group.id === "archived"; let showActions = group.id === "mine"; let collapsed = Boolean(group.collapsed); let content = useId(); let reduced = useReducedMotion(); return (
{!collapsed && (
{group.repos.map((section) => ( ))}
)}
); } function LoadingRows() { let placeholder = "bg-sidebar-accent/80 dark:bg-sidebar-accent"; return (
{[0, 1].map((group) => (
{[0, 1].map((row) => (
))}
))}
); } function DefaultEmpty({ onAddRepo }: { onAddRepo?: () => void }) { return (
No repositories yet
{onAddRepo && }
); } export function SessionSidebarUnified( { groups, repos, loading, selectedUid, onSelect, onPin, onUnpin, onArchive, onFork, onInfo, onLeave, onDelete, onRename, onToggleGroup, onToggleRepo, onRemoveRepo, onAddRepo, onNewSession, onContinuePr, onStartIssue, empty, className, ref, ...props }: SessionSidebarUnifiedProps, ) { let total = count(groups); let visible = groups.filter((group) => group.repos.length > 0); return ( ); }