| ... | ... | @@ -7,16 +7,18 @@ import { |
| 7 | 7 | scopeThreadRef, |
| 8 | 8 | scopedThreadKey, |
| 9 | 9 | } from "@t3tools/client-runtime/environment"; |
| 10 | | import type { ScopedThreadRef } from "@t3tools/contracts"; |
| 10 | import type { ScopedThreadRef, SidebarProjectGroupingMode } from "@t3tools/contracts"; |
| 11 | 11 | import { |
| 12 | 12 | CheckIcon, |
| 13 | 13 | ChevronDownIcon, |
| 14 | 14 | CircleAlertIcon, |
| 15 | 15 | CircleCheckIcon, |
| 16 | 16 | CircleDashedIcon, |
| 17 | CopyIcon, |
| 17 | 18 | FolderIcon, |
| 18 | 19 | FolderPlusIcon, |
| 19 | 20 | GitBranchIcon, |
| 21 | EllipsisIcon, |
| 20 | 22 | MessageSquareIcon, |
| 21 | 23 | PlusIcon, |
| 22 | 24 | SearchIcon, |
| ... | ... | @@ -58,9 +60,14 @@ import { selectThreadTerminalUiState, useTerminalUiStateStore } from "../termina |
| 58 | 60 | import { isMacPlatform } from "~/lib/utils"; |
| 59 | 61 | import { useOpenPrLink } from "../lib/openPullRequestLink"; |
| 60 | 62 | import { readLocalApi } from "../localApi"; |
| 61 | | import { getProjectOrderKey, selectProjectGroupingSettings } from "../logicalProject"; |
| 63 | import { |
| 64 | deriveProjectGroupingOverrideKey, |
| 65 | getProjectOrderKey, |
| 66 | selectProjectGroupingSettings, |
| 67 | } from "../logicalProject"; |
| 62 | 68 | import { |
| 63 | 69 | buildSidebarProjectSnapshots, |
| 70 | type SidebarProjectGroupMember, |
| 64 | 71 | type SidebarProjectSnapshot, |
| 65 | 72 | } from "../sidebarProjectGrouping"; |
| 66 | 73 | import { legacyProjectCwdPreferenceKey, useUiStateStore } from "../uiStateStore"; |
| ... | ... | @@ -69,7 +76,8 @@ import { useThreadActions } from "../hooks/useThreadActions"; |
| 69 | 76 | import { useHandleNewThread } from "../hooks/useHandleNewThread"; |
| 70 | 77 | import { openCommandPalette } from "../commandPaletteBus"; |
| 71 | 78 | import { startNewThreadFromContext } from "../lib/chatThreadActions"; |
| 72 | | import { useClientSettings } from "../hooks/useSettings"; |
| 79 | import { useClientSettings, useUpdateClientSettings } from "../hooks/useSettings"; |
| 80 | import { useCopyToClipboard } from "../hooks/useCopyToClipboard"; |
| 73 | 81 | import { useEnvironments, usePrimaryEnvironmentId } from "../state/environments"; |
| 74 | 82 | import { useProjects, useThreadShells } from "../state/entities"; |
| 75 | 83 | import { environmentServerConfigsAtom, primaryServerKeybindingsAtom } from "../state/server"; |
| ... | ... | @@ -102,8 +110,20 @@ import { deriveProviderInstanceEntries, type ProviderInstanceEntry } from "../pr |
| 102 | 110 | import { primaryServerProvidersAtom } from "../state/server"; |
| 103 | 111 | import { stackedThreadToast, toastManager } from "./ui/toast"; |
| 104 | 112 | import { CommandDialogTrigger } from "./ui/command"; |
| 113 | import { Button } from "./ui/button"; |
| 114 | import { |
| 115 | Dialog, |
| 116 | DialogDescription, |
| 117 | DialogFooter, |
| 118 | DialogHeader, |
| 119 | DialogPanel, |
| 120 | DialogPopup, |
| 121 | DialogTitle, |
| 122 | } from "./ui/dialog"; |
| 123 | import { Input } from "./ui/input"; |
| 105 | 124 | import { Kbd } from "./ui/kbd"; |
| 106 | 125 | import { Menu, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuTrigger } from "./ui/menu"; |
| 126 | import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "./ui/select"; |
| 107 | 127 | import { SidebarContent, SidebarGroup, SidebarMenuButton, useSidebar } from "./ui/sidebar"; |
| 108 | 128 | import { SidebarChromeFooter, SidebarChromeHeader } from "./sidebar/SidebarChrome"; |
| 109 | 129 | import { Tooltip, TooltipPopup, TooltipProvider, TooltipTrigger } from "./ui/tooltip"; |
| ... | ... | @@ -113,6 +133,11 @@ import { useComposerDraftStore } from "../composerDraftStore"; |
| 113 | 133 | // stays behind an explicit Show more. |
| 114 | 134 | const SETTLED_TAIL_INITIAL_COUNT = 10; |
| 115 | 135 | const SETTLED_TAIL_PAGE_COUNT = 25; |
| 136 | const PROJECT_GROUPING_MODE_LABELS: Record<SidebarProjectGroupingMode, string> = { |
| 137 | repository: "Group by repository", |
| 138 | repository_path: "Group by repository path", |
| 139 | separate: "Keep separate", |
| 140 | }; |
| 116 | 141 | |
| 117 | 142 | function compactSidebarTimeLabel(label: string): string { |
| 118 | 143 | if (label === "just now") return "now"; |
| ... | ... | @@ -736,6 +761,32 @@ export default function SidebarV2() { |
| 736 | 761 | const deleteProject = useAtomCommand(projectEnvironment.delete, { |
| 737 | 762 | reportFailure: false, |
| 738 | 763 | }); |
| 764 | const updateProject = useAtomCommand(projectEnvironment.update, { |
| 765 | reportFailure: false, |
| 766 | }); |
| 767 | const updateSettings = useUpdateClientSettings(); |
| 768 | const { copyToClipboard: copyProjectPath } = useCopyToClipboard<{ path: string }>({ |
| 769 | onCopy: ({ path }) => { |
| 770 | toastManager.add({ |
| 771 | type: "success", |
| 772 | title: "Path copied", |
| 773 | description: path, |
| 774 | }); |
| 775 | }, |
| 776 | onError: (error) => { |
| 777 | toastManager.add( |
| 778 | stackedThreadToast({ |
| 779 | type: "error", |
| 780 | title: "Failed to copy path", |
| 781 | description: error instanceof Error ? error.message : "An error occurred.", |
| 782 | }), |
| 783 | ); |
| 784 | }, |
| 785 | }); |
| 786 | const [projectActionsTarget, setProjectActionsTarget] = useState<SidebarProjectSnapshot | null>( |
| 787 | null, |
| 788 | ); |
| 789 | const [projectScopeMenuOpen, setProjectScopeMenuOpen] = useState(false); |
| 739 | 790 | const newThreadContext = useHandleNewThread(); |
| 740 | 791 | const openAddProjectCommandPalette = useCallback( |
| 741 | 792 | () => openCommandPalette({ open: "add-project" }), |
| ... | ... | @@ -893,41 +944,50 @@ export default function SidebarV2() { |
| 893 | 944 | clearSelection(); |
| 894 | 945 | }, [clearSelection, projectScopeKey]); |
| 895 | 946 | |
| 896 | | const handleRemoveProject = useCallback( |
| 897 | | async (projectGroup: SidebarProjectSnapshot) => { |
| 947 | const handleRemoveProjectMembers = useCallback( |
| 948 | async (projectGroup: SidebarProjectSnapshot, members: readonly SidebarProjectGroupMember[]) => { |
| 898 | 949 | const api = readLocalApi(); |
| 899 | 950 | if (!api) return; |
| 900 | 951 | |
| 901 | | const memberKeys = new Set( |
| 902 | | projectGroup.memberProjectRefs.map( |
| 903 | | (projectRef) => `${projectRef.environmentId}:${projectRef.projectId}`, |
| 904 | | ), |
| 905 | | ); |
| 952 | const memberKeys = new Set(members.map((member) => `${member.environmentId}:${member.id}`)); |
| 906 | 953 | const projectThreads = threads.filter((thread) => |
| 907 | 954 | memberKeys.has(`${thread.environmentId}:${thread.projectId}`), |
| 908 | 955 | ); |
| 956 | const isWholeGroup = members.length === projectGroup.memberProjects.length; |
| 957 | const singleMember = members.length === 1 ? members[0]! : null; |
| 958 | const targetLabel = singleMember?.title ?? projectGroup.displayName; |
| 909 | 959 | const confirmed = await settlePromise(() => |
| 910 | 960 | api.dialogs.confirm( |
| 911 | 961 | projectThreads.length > 0 |
| 912 | 962 | ? [ |
| 913 | | `Remove project "${projectGroup.displayName}" and delete its ${projectThreads.length} thread${projectThreads.length === 1 ? "" : "s"}?`, |
| 914 | | ...(projectGroup.memberProjects.length === 1 |
| 915 | | ? [`Path: ${projectGroup.workspaceRoot}`] |
| 916 | | : [ |
| 917 | | `This removes ${projectGroup.memberProjects.length} grouped project entries.`, |
| 918 | | ]), |
| 963 | `Remove project "${targetLabel}" and delete its ${projectThreads.length} thread${projectThreads.length === 1 ? "" : "s"}?`, |
| 964 | ...(singleMember |
| 965 | ? [ |
| 966 | `Path: ${singleMember.workspaceRoot}`, |
| 967 | ...(singleMember.environmentLabel |
| 968 | ? [`Environment: ${singleMember.environmentLabel}`] |
| 969 | : []), |
| 970 | ] |
| 971 | : [`This removes ${members.length} grouped project entries.`]), |
| 919 | 972 | "This permanently clears conversation history for those threads.", |
| 920 | | "This removes only the project entries, not the files on disk.", |
| 973 | isWholeGroup |
| 974 | ? "This removes only the project entries, not the files on disk." |
| 975 | : "Other entries in this grouped project are unaffected.", |
| 921 | 976 | "This action cannot be undone.", |
| 922 | 977 | ].join("\n") |
| 923 | 978 | : [ |
| 924 | | `Remove project "${projectGroup.displayName}"?`, |
| 925 | | ...(projectGroup.memberProjects.length === 1 |
| 926 | | ? [`Path: ${projectGroup.workspaceRoot}`] |
| 927 | | : [ |
| 928 | | `This removes ${projectGroup.memberProjects.length} grouped project entries.`, |
| 929 | | ]), |
| 930 | | "This removes only the project entries, not the files on disk.", |
| 979 | `Remove project "${targetLabel}"?`, |
| 980 | ...(singleMember |
| 981 | ? [ |
| 982 | `Path: ${singleMember.workspaceRoot}`, |
| 983 | ...(singleMember.environmentLabel |
| 984 | ? [`Environment: ${singleMember.environmentLabel}`] |
| 985 | : []), |
| 986 | ] |
| 987 | : [`This removes ${members.length} grouped project entries.`]), |
| 988 | isWholeGroup |
| 989 | ? "This removes only the project entries, not the files on disk." |
| 990 | : "Other entries in this grouped project are unaffected.", |
| 931 | 991 | ].join("\n"), |
| 932 | 992 | ), |
| 933 | 993 | ); |
| ... | ... | @@ -935,7 +995,7 @@ export default function SidebarV2() { |
| 935 | 995 | |
| 936 | 996 | const draftStore = useComposerDraftStore.getState(); |
| 937 | 997 | let shouldNavigate = false; |
| 938 | | for (const project of projectGroup.memberProjects) { |
| 998 | for (const project of members) { |
| 939 | 999 | const memberThreads = projectThreads.filter( |
| 940 | 1000 | (thread) => |
| 941 | 1001 | thread.environmentId === project.environmentId && thread.projectId === project.id, |
| ... | ... | @@ -986,6 +1046,56 @@ export default function SidebarV2() { |
| 986 | 1046 | [deleteProject, router, threads], |
| 987 | 1047 | ); |
| 988 | 1048 | |
| 1049 | const renameProjectMember = useCallback( |
| 1050 | async (member: SidebarProjectGroupMember, nextTitle: string) => { |
| 1051 | const title = nextTitle.trim(); |
| 1052 | if (!title) { |
| 1053 | toastManager.add({ type: "warning", title: "Project title cannot be empty" }); |
| 1054 | return; |
| 1055 | } |
| 1056 | if (title === member.title) return; |
| 1057 | const result = await updateProject({ |
| 1058 | environmentId: member.environmentId, |
| 1059 | input: { projectId: member.id, title }, |
| 1060 | }); |
| 1061 | if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { |
| 1062 | const error = squashAtomCommandFailure(result); |
| 1063 | toastManager.add( |
| 1064 | stackedThreadToast({ |
| 1065 | type: "error", |
| 1066 | title: "Failed to rename project", |
| 1067 | description: error instanceof Error ? error.message : "An error occurred.", |
| 1068 | }), |
| 1069 | ); |
| 1070 | } |
| 1071 | }, |
| 1072 | [updateProject], |
| 1073 | ); |
| 1074 | |
| 1075 | const updateProjectGroupingPreference = useCallback( |
| 1076 | (member: SidebarProjectGroupMember, selection: SidebarProjectGroupingMode | "inherit") => { |
| 1077 | const overrideKey = deriveProjectGroupingOverrideKey(member); |
| 1078 | const nextOverrides = { ...projectGroupingSettings.sidebarProjectGroupingOverrides }; |
| 1079 | if (selection === "inherit") { |
| 1080 | delete nextOverrides[overrideKey]; |
| 1081 | } else { |
| 1082 | nextOverrides[overrideKey] = selection; |
| 1083 | } |
| 1084 | updateSettings({ sidebarProjectGroupingOverrides: nextOverrides }); |
| 1085 | }, |
| 1086 | [projectGroupingSettings.sidebarProjectGroupingOverrides, updateSettings], |
| 1087 | ); |
| 1088 | |
| 1089 | const handleProjectActions = useCallback( |
| 1090 | (event: ReactMouseEvent<HTMLButtonElement>, projectGroup: SidebarProjectSnapshot) => { |
| 1091 | event.preventDefault(); |
| 1092 | event.stopPropagation(); |
| 1093 | setProjectScopeMenuOpen(false); |
| 1094 | window.requestAnimationFrame(() => setProjectActionsTarget(projectGroup)); |
| 1095 | }, |
| 1096 | [], |
| 1097 | ); |
| 1098 | |
| 989 | 1099 | // Settled threads stay in the live shell stream (settled ≠ archived), so |
| 990 | 1100 | // the partition works directly off live shells: no archived-snapshot |
| 991 | 1101 | // merging, no optimistic holds. Archived threads remain hidden here — |
| ... | ... | @@ -1626,7 +1736,7 @@ export default function SidebarV2() { |
| 1626 | 1736 | {projectGroups.length > 0 ? ( |
| 1627 | 1737 | <SidebarGroup className="px-2 pb-2 pt-0"> |
| 1628 | 1738 | <div className="flex items-center gap-1"> |
| 1629 | | <Menu> |
| 1739 | <Menu open={projectScopeMenuOpen} onOpenChange={setProjectScopeMenuOpen}> |
| 1630 | 1740 | <MenuTrigger |
| 1631 | 1741 | aria-label="Filter threads by project" |
| 1632 | 1742 | className="flex h-8 min-w-0 flex-1 cursor-pointer items-center gap-2 rounded-md px-2 text-left text-sm font-medium text-sidebar-muted-foreground outline-none hover:bg-sidebar-row-hover hover:text-sidebar-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-sidebar" |
| ... | ... | @@ -1677,17 +1787,15 @@ export default function SidebarV2() { |
| 1677 | 1787 | <span className="min-w-0 truncate text-sm">{project.displayName}</span> |
| 1678 | 1788 | <button |
| 1679 | 1789 | type="button" |
| 1680 | | aria-label={`Remove project ${project.displayName}`} |
| 1681 | | title={`Remove ${project.displayName}`} |
| 1682 | | className="ml-auto inline-flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground/55 outline-none transition-colors hover:bg-destructive/12 hover:text-destructive focus-visible:bg-destructive/12 focus-visible:text-destructive focus-visible:ring-2 focus-visible:ring-destructive/40" |
| 1790 | aria-label={`Project actions for ${project.displayName}`} |
| 1791 | title={`Project actions for ${project.displayName}`} |
| 1792 | className="ml-auto inline-flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground/55 outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:bg-accent focus-visible:text-foreground focus-visible:ring-2 focus-visible:ring-ring" |
| 1683 | 1793 | onPointerDown={(event) => event.stopPropagation()} |
| 1684 | 1794 | onClick={(event) => { |
| 1685 | | event.preventDefault(); |
| 1686 | | event.stopPropagation(); |
| 1687 | | void handleRemoveProject(project); |
| 1795 | void handleProjectActions(event, project); |
| 1688 | 1796 | }} |
| 1689 | 1797 | > |
| 1690 | | <Trash2Icon className="size-3.5" /> |
| 1798 | <EllipsisIcon className="size-3.5" /> |
| 1691 | 1799 | </button> |
| 1692 | 1800 | </MenuRadioItem> |
| 1693 | 1801 | ); |
| ... | ... | @@ -1845,6 +1953,178 @@ export default function SidebarV2() { |
| 1845 | 1953 | ) : null} |
| 1846 | 1954 | </SidebarGroup> |
| 1847 | 1955 | </SidebarContent> |
| 1956 | <Dialog |
| 1957 | open={projectActionsTarget !== null} |
| 1958 | onOpenChange={(open) => { |
| 1959 | if (!open) setProjectActionsTarget(null); |
| 1960 | }} |
| 1961 | > |
| 1962 | <DialogPopup className="max-w-xl"> |
| 1963 | <DialogHeader className="gap-1.5"> |
| 1964 | <DialogTitle className="text-balance">Project settings</DialogTitle> |
| 1965 | <DialogDescription> |
| 1966 | {projectActionsTarget && projectActionsTarget.memberProjects.length > 1 |
| 1967 | ? `${projectActionsTarget.displayName} has an entry in each environment. Changes apply only to the entry you choose.` |
| 1968 | : `Manage ${projectActionsTarget?.displayName ?? "this project"} in this environment.`} |
| 1969 | </DialogDescription> |
| 1970 | </DialogHeader> |
| 1971 | <DialogPanel className="p-0"> |
| 1972 | <div className="divide-y divide-border/60"> |
| 1973 | {projectActionsTarget?.memberProjects.map((member) => ( |
| 1974 | <section |
| 1975 | key={member.physicalProjectKey} |
| 1976 | className="flex min-w-0 flex-col gap-4 px-6 py-5 sm:gap-3 sm:py-4" |
| 1977 | > |
| 1978 | <div className="flex min-w-0 items-start gap-3"> |
| 1979 | <ProjectFavicon |
| 1980 | environmentId={member.environmentId} |
| 1981 | cwd={member.workspaceRoot} |
| 1982 | className="size-5 shrink-0 sm:size-4" |
| 1983 | /> |
| 1984 | <div className="min-w-0 flex-1"> |
| 1985 | <div className="flex min-w-0 items-center gap-1.5 text-base text-muted-foreground sm:text-sm"> |
| 1986 | <ServerIcon className="size-4 shrink-0 stroke-muted-foreground" /> |
| 1987 | <p className="min-w-0 truncate"> |
| 1988 | {member.environmentLabel ?? "Current environment"} |
| 1989 | </p> |
| 1990 | </div> |
| 1991 | <p |
| 1992 | className="truncate font-mono text-base text-muted-foreground/72 sm:text-sm" |
| 1993 | title={member.workspaceRoot} |
| 1994 | > |
| 1995 | {member.workspaceRoot} |
| 1996 | </p> |
| 1997 | </div> |
| 1998 | </div> |
| 1999 | <div className="grid gap-4 sm:grid-cols-2 sm:gap-3 sm:pl-7"> |
| 2000 | <label className="grid min-w-0 gap-1.5"> |
| 2001 | <span className="font-medium text-foreground">Project name</span> |
| 2002 | <Input |
| 2003 | key={`${member.physicalProjectKey}:${member.title}`} |
| 2004 | size="sm" |
| 2005 | aria-label={`Project name in ${member.environmentLabel ?? "current environment"}`} |
| 2006 | defaultValue={member.title} |
| 2007 | onBlur={(event) => { |
| 2008 | void renameProjectMember(member, event.currentTarget.value); |
| 2009 | }} |
| 2010 | onKeyDown={(event) => { |
| 2011 | if (event.key === "Enter") event.currentTarget.blur(); |
| 2012 | }} |
| 2013 | /> |
| 2014 | </label> |
| 2015 | <label className="grid min-w-0 gap-1.5"> |
| 2016 | <span className="font-medium text-foreground">Grouping rule</span> |
| 2017 | <Select |
| 2018 | value={ |
| 2019 | projectGroupingSettings.sidebarProjectGroupingOverrides?.[ |
| 2020 | deriveProjectGroupingOverrideKey(member) |
| 2021 | ] ?? "inherit" |
| 2022 | } |
| 2023 | onValueChange={(value) => { |
| 2024 | if ( |
| 2025 | value === "inherit" || |
| 2026 | value === "repository" || |
| 2027 | value === "repository_path" || |
| 2028 | value === "separate" |
| 2029 | ) { |
| 2030 | updateProjectGroupingPreference(member, value); |
| 2031 | } |
| 2032 | }} |
| 2033 | > |
| 2034 | <SelectTrigger |
| 2035 | size="sm" |
| 2036 | className="w-full" |
| 2037 | aria-label={`Grouping rule for ${member.environmentLabel ?? "current environment"}`} |
| 2038 | > |
| 2039 | <SelectValue> |
| 2040 | {(() => { |
| 2041 | const selection = |
| 2042 | projectGroupingSettings.sidebarProjectGroupingOverrides?.[ |
| 2043 | deriveProjectGroupingOverrideKey(member) |
| 2044 | ] ?? "inherit"; |
| 2045 | return selection === "inherit" |
| 2046 | ? `Default (${PROJECT_GROUPING_MODE_LABELS[projectGroupingSettings.sidebarProjectGroupingMode]})` |
| 2047 | : PROJECT_GROUPING_MODE_LABELS[selection]; |
| 2048 | })()} |
| 2049 | </SelectValue> |
| 2050 | </SelectTrigger> |
| 2051 | <SelectPopup align="start" alignItemWithTrigger={false}> |
| 2052 | <SelectItem hideIndicator value="inherit"> |
| 2053 | Use global default |
| 2054 | </SelectItem> |
| 2055 | <SelectItem hideIndicator value="repository"> |
| 2056 | {PROJECT_GROUPING_MODE_LABELS.repository} |
| 2057 | </SelectItem> |
| 2058 | <SelectItem hideIndicator value="repository_path"> |
| 2059 | {PROJECT_GROUPING_MODE_LABELS.repository_path} |
| 2060 | </SelectItem> |
| 2061 | <SelectItem hideIndicator value="separate"> |
| 2062 | {PROJECT_GROUPING_MODE_LABELS.separate} |
| 2063 | </SelectItem> |
| 2064 | </SelectPopup> |
| 2065 | </Select> |
| 2066 | </label> |
| 2067 | </div> |
| 2068 | <div className="flex flex-wrap items-center gap-2 sm:pl-7"> |
| 2069 | <Button |
| 2070 | size="sm" |
| 2071 | variant="ghost" |
| 2072 | onClick={() => |
| 2073 | copyProjectPath(member.workspaceRoot, { path: member.workspaceRoot }) |
| 2074 | } |
| 2075 | > |
| 2076 | <CopyIcon /> |
| 2077 | Copy path |
| 2078 | </Button> |
| 2079 | <Button |
| 2080 | size="sm" |
| 2081 | variant="ghost" |
| 2082 | className="text-destructive-foreground hover:bg-destructive/8 hover:text-destructive-foreground sm:ml-auto" |
| 2083 | onClick={() => { |
| 2084 | const projectGroup = projectActionsTarget; |
| 2085 | if (!projectGroup) return; |
| 2086 | setProjectActionsTarget(null); |
| 2087 | void handleRemoveProjectMembers(projectGroup, [member]); |
| 2088 | }} |
| 2089 | > |
| 2090 | <Trash2Icon /> |
| 2091 | Remove |
| 2092 | </Button> |
| 2093 | </div> |
| 2094 | </section> |
| 2095 | ))} |
| 2096 | </div> |
| 2097 | {projectActionsTarget && projectActionsTarget.memberProjects.length > 1 ? ( |
| 2098 | <div className="flex flex-col gap-3 border-t border-border/60 bg-muted/32 px-6 py-4 sm:flex-row sm:items-center sm:justify-between"> |
| 2099 | <div className="min-w-0"> |
| 2100 | <p className="text-base font-medium text-foreground sm:text-sm"> |
| 2101 | Remove this project everywhere |
| 2102 | </p> |
| 2103 | <p className="text-base text-pretty text-muted-foreground sm:text-sm"> |
| 2104 | Deletes all grouped entries and their conversation history. |
| 2105 | </p> |
| 2106 | </div> |
| 2107 | <Button |
| 2108 | size="sm" |
| 2109 | variant="destructive-outline" |
| 2110 | className="shrink-0" |
| 2111 | onClick={() => { |
| 2112 | const projectGroup = projectActionsTarget; |
| 2113 | setProjectActionsTarget(null); |
| 2114 | void handleRemoveProjectMembers(projectGroup, projectGroup.memberProjects); |
| 2115 | }} |
| 2116 | > |
| 2117 | <Trash2Icon /> |
| 2118 | Remove all entries |
| 2119 | </Button> |
| 2120 | </div> |
| 2121 | ) : null} |
| 2122 | </DialogPanel> |
| 2123 | <DialogFooter variant="bare"> |
| 2124 | <Button onClick={() => setProjectActionsTarget(null)}>Done</Button> |
| 2125 | </DialogFooter> |
| 2126 | </DialogPopup> |
| 2127 | </Dialog> |
| 1848 | 2128 | <SidebarChromeFooter /> |
| 1849 | 2129 | </> |
| 1850 | 2130 | ); |