authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-07-23 18:11:27+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-23 16:11:27+00:00
log57100fba854fb1214ae7a413111efae14443d445
treeae102b34f25d6a35935ebbefed0cb1a608771981
parent3afb4a9ef9e71ddf44035797cd224301bc5645a1
signature Signed by PGP key B5690EEEBB952194

[codex] restore Sidebar V2 project actions (#4373)

Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 314 insertions(+), 33 deletions(-)

apps/web/src/components/Sidebar.logic.test.ts+1
...@@ -35,6 +35,7 @@ import {...@@ -35,6 +35,7 @@ import {
35 ProviderInstanceId,35 ProviderInstanceId,
36 ThreadId,36 ThreadId,
37} from "@t3tools/contracts";37} from "@t3tools/contracts";
38
38import {39import {
39 DEFAULT_INTERACTION_MODE,40 DEFAULT_INTERACTION_MODE,
40 DEFAULT_RUNTIME_MODE,41 DEFAULT_RUNTIME_MODE,
apps/web/src/components/SidebarV2.tsx+313-33
...@@ -7,16 +7,18 @@ import {...@@ -7,16 +7,18 @@ import {
7 scopeThreadRef,7 scopeThreadRef,
8 scopedThreadKey,8 scopedThreadKey,
9} from "@t3tools/client-runtime/environment";9} from "@t3tools/client-runtime/environment";
10import type { ScopedThreadRef } from "@t3tools/contracts";10import type { ScopedThreadRef, SidebarProjectGroupingMode } from "@t3tools/contracts";
11import {11import {
12 CheckIcon,12 CheckIcon,
13 ChevronDownIcon,13 ChevronDownIcon,
14 CircleAlertIcon,14 CircleAlertIcon,
15 CircleCheckIcon,15 CircleCheckIcon,
16 CircleDashedIcon,16 CircleDashedIcon,
17 CopyIcon,
17 FolderIcon,18 FolderIcon,
18 FolderPlusIcon,19 FolderPlusIcon,
19 GitBranchIcon,20 GitBranchIcon,
21 EllipsisIcon,
20 MessageSquareIcon,22 MessageSquareIcon,
21 PlusIcon,23 PlusIcon,
22 SearchIcon,24 SearchIcon,
...@@ -58,9 +60,14 @@ import { selectThreadTerminalUiState, useTerminalUiStateStore } from "../termina...@@ -58,9 +60,14 @@ import { selectThreadTerminalUiState, useTerminalUiStateStore } from "../termina
58import { isMacPlatform } from "~/lib/utils";60import { isMacPlatform } from "~/lib/utils";
59import { useOpenPrLink } from "../lib/openPullRequestLink";61import { useOpenPrLink } from "../lib/openPullRequestLink";
60import { readLocalApi } from "../localApi";62import { readLocalApi } from "../localApi";
61import { getProjectOrderKey, selectProjectGroupingSettings } from "../logicalProject";63import {
64 deriveProjectGroupingOverrideKey,
65 getProjectOrderKey,
66 selectProjectGroupingSettings,
67} from "../logicalProject";
62import {68import {
63 buildSidebarProjectSnapshots,69 buildSidebarProjectSnapshots,
70 type SidebarProjectGroupMember,
64 type SidebarProjectSnapshot,71 type SidebarProjectSnapshot,
65} from "../sidebarProjectGrouping";72} from "../sidebarProjectGrouping";
66import { legacyProjectCwdPreferenceKey, useUiStateStore } from "../uiStateStore";73import { legacyProjectCwdPreferenceKey, useUiStateStore } from "../uiStateStore";
...@@ -69,7 +76,8 @@ import { useThreadActions } from "../hooks/useThreadActions";...@@ -69,7 +76,8 @@ import { useThreadActions } from "../hooks/useThreadActions";
69import { useHandleNewThread } from "../hooks/useHandleNewThread";76import { useHandleNewThread } from "../hooks/useHandleNewThread";
70import { openCommandPalette } from "../commandPaletteBus";77import { openCommandPalette } from "../commandPaletteBus";
71import { startNewThreadFromContext } from "../lib/chatThreadActions";78import { startNewThreadFromContext } from "../lib/chatThreadActions";
72import { useClientSettings } from "../hooks/useSettings";79import { useClientSettings, useUpdateClientSettings } from "../hooks/useSettings";
80import { useCopyToClipboard } from "../hooks/useCopyToClipboard";
73import { useEnvironments, usePrimaryEnvironmentId } from "../state/environments";81import { useEnvironments, usePrimaryEnvironmentId } from "../state/environments";
74import { useProjects, useThreadShells } from "../state/entities";82import { useProjects, useThreadShells } from "../state/entities";
75import { environmentServerConfigsAtom, primaryServerKeybindingsAtom } from "../state/server";83import { environmentServerConfigsAtom, primaryServerKeybindingsAtom } from "../state/server";
...@@ -102,8 +110,20 @@ import { deriveProviderInstanceEntries, type ProviderInstanceEntry } from "../pr...@@ -102,8 +110,20 @@ import { deriveProviderInstanceEntries, type ProviderInstanceEntry } from "../pr
102import { primaryServerProvidersAtom } from "../state/server";110import { primaryServerProvidersAtom } from "../state/server";
103import { stackedThreadToast, toastManager } from "./ui/toast";111import { stackedThreadToast, toastManager } from "./ui/toast";
104import { CommandDialogTrigger } from "./ui/command";112import { CommandDialogTrigger } from "./ui/command";
113import { Button } from "./ui/button";
114import {
115 Dialog,
116 DialogDescription,
117 DialogFooter,
118 DialogHeader,
119 DialogPanel,
120 DialogPopup,
121 DialogTitle,
122} from "./ui/dialog";
123import { Input } from "./ui/input";
105import { Kbd } from "./ui/kbd";124import { Kbd } from "./ui/kbd";
106import { Menu, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuTrigger } from "./ui/menu";125import { Menu, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuTrigger } from "./ui/menu";
126import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "./ui/select";
107import { SidebarContent, SidebarGroup, SidebarMenuButton, useSidebar } from "./ui/sidebar";127import { SidebarContent, SidebarGroup, SidebarMenuButton, useSidebar } from "./ui/sidebar";
108import { SidebarChromeFooter, SidebarChromeHeader } from "./sidebar/SidebarChrome";128import { SidebarChromeFooter, SidebarChromeHeader } from "./sidebar/SidebarChrome";
109import { Tooltip, TooltipPopup, TooltipProvider, TooltipTrigger } from "./ui/tooltip";129import { Tooltip, TooltipPopup, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
...@@ -113,6 +133,11 @@ import { useComposerDraftStore } from "../composerDraftStore";...@@ -113,6 +133,11 @@ import { useComposerDraftStore } from "../composerDraftStore";
113// stays behind an explicit Show more.133// stays behind an explicit Show more.
114const SETTLED_TAIL_INITIAL_COUNT = 10;134const SETTLED_TAIL_INITIAL_COUNT = 10;
115const SETTLED_TAIL_PAGE_COUNT = 25;135const SETTLED_TAIL_PAGE_COUNT = 25;
136const PROJECT_GROUPING_MODE_LABELS: Record<SidebarProjectGroupingMode, string> = {
137 repository: "Group by repository",
138 repository_path: "Group by repository path",
139 separate: "Keep separate",
140};
116141
117function compactSidebarTimeLabel(label: string): string {142function compactSidebarTimeLabel(label: string): string {
118 if (label === "just now") return "now";143 if (label === "just now") return "now";
...@@ -736,6 +761,32 @@ export default function SidebarV2() {...@@ -736,6 +761,32 @@ export default function SidebarV2() {
736 const deleteProject = useAtomCommand(projectEnvironment.delete, {761 const deleteProject = useAtomCommand(projectEnvironment.delete, {
737 reportFailure: false,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 const newThreadContext = useHandleNewThread();790 const newThreadContext = useHandleNewThread();
740 const openAddProjectCommandPalette = useCallback(791 const openAddProjectCommandPalette = useCallback(
741 () => openCommandPalette({ open: "add-project" }),792 () => openCommandPalette({ open: "add-project" }),
...@@ -893,41 +944,50 @@ export default function SidebarV2() {...@@ -893,41 +944,50 @@ export default function SidebarV2() {
893 clearSelection();944 clearSelection();
894 }, [clearSelection, projectScopeKey]);945 }, [clearSelection, projectScopeKey]);
895946
896 const handleRemoveProject = useCallback(947 const handleRemoveProjectMembers = useCallback(
897 async (projectGroup: SidebarProjectSnapshot) => {948 async (projectGroup: SidebarProjectSnapshot, members: readonly SidebarProjectGroupMember[]) => {
898 const api = readLocalApi();949 const api = readLocalApi();
899 if (!api) return;950 if (!api) return;
900951
901 const memberKeys = new Set(952 const memberKeys = new Set(members.map((member) => `${member.environmentId}:${member.id}`));
902 projectGroup.memberProjectRefs.map(
903 (projectRef) => `${projectRef.environmentId}:${projectRef.projectId}`,
904 ),
905 );
906 const projectThreads = threads.filter((thread) =>953 const projectThreads = threads.filter((thread) =>
907 memberKeys.has(`${thread.environmentId}:${thread.projectId}`),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 const confirmed = await settlePromise(() =>959 const confirmed = await settlePromise(() =>
910 api.dialogs.confirm(960 api.dialogs.confirm(
911 projectThreads.length > 0961 projectThreads.length > 0
912 ? [962 ? [
913 `Remove project "${projectGroup.displayName}" and delete its ${projectThreads.length} thread${projectThreads.length === 1 ? "" : "s"}?`,963 `Remove project "${targetLabel}" and delete its ${projectThreads.length} thread${projectThreads.length === 1 ? "" : "s"}?`,
914 ...(projectGroup.memberProjects.length === 1964 ...(singleMember
915 ? [`Path: ${projectGroup.workspaceRoot}`]965 ? [
916 : [966 `Path: ${singleMember.workspaceRoot}`,
917 `This removes ${projectGroup.memberProjects.length} grouped project entries.`,967 ...(singleMember.environmentLabel
918 ]),968 ? [`Environment: ${singleMember.environmentLabel}`]
969 : []),
970 ]
971 : [`This removes ${members.length} grouped project entries.`]),
919 "This permanently clears conversation history for those threads.",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 "This action cannot be undone.",976 "This action cannot be undone.",
922 ].join("\n")977 ].join("\n")
923 : [978 : [
924 `Remove project "${projectGroup.displayName}"?`,979 `Remove project "${targetLabel}"?`,
925 ...(projectGroup.memberProjects.length === 1980 ...(singleMember
926 ? [`Path: ${projectGroup.workspaceRoot}`]981 ? [
927 : [982 `Path: ${singleMember.workspaceRoot}`,
928 `This removes ${projectGroup.memberProjects.length} grouped project entries.`,983 ...(singleMember.environmentLabel
929 ]),984 ? [`Environment: ${singleMember.environmentLabel}`]
930 "This removes only the project entries, not the files on disk.",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 ].join("\n"),991 ].join("\n"),
932 ),992 ),
933 );993 );
...@@ -935,7 +995,7 @@ export default function SidebarV2() {...@@ -935,7 +995,7 @@ export default function SidebarV2() {
935995
936 const draftStore = useComposerDraftStore.getState();996 const draftStore = useComposerDraftStore.getState();
937 let shouldNavigate = false;997 let shouldNavigate = false;
938 for (const project of projectGroup.memberProjects) {998 for (const project of members) {
939 const memberThreads = projectThreads.filter(999 const memberThreads = projectThreads.filter(
940 (thread) =>1000 (thread) =>
941 thread.environmentId === project.environmentId && thread.projectId === project.id,1001 thread.environmentId === project.environmentId && thread.projectId === project.id,
...@@ -986,6 +1046,56 @@ export default function SidebarV2() {...@@ -986,6 +1046,56 @@ export default function SidebarV2() {
986 [deleteProject, router, threads],1046 [deleteProject, router, threads],
987 );1047 );
9881048
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 // Settled threads stay in the live shell stream (settled ≠ archived), so1099 // Settled threads stay in the live shell stream (settled ≠ archived), so
990 // the partition works directly off live shells: no archived-snapshot1100 // the partition works directly off live shells: no archived-snapshot
991 // merging, no optimistic holds. Archived threads remain hidden here —1101 // merging, no optimistic holds. Archived threads remain hidden here —
...@@ -1626,7 +1736,7 @@ export default function SidebarV2() {...@@ -1626,7 +1736,7 @@ export default function SidebarV2() {
1626 {projectGroups.length > 0 ? (1736 {projectGroups.length > 0 ? (
1627 <SidebarGroup className="px-2 pb-2 pt-0">1737 <SidebarGroup className="px-2 pb-2 pt-0">
1628 <div className="flex items-center gap-1">1738 <div className="flex items-center gap-1">
1629 <Menu>1739 <Menu open={projectScopeMenuOpen} onOpenChange={setProjectScopeMenuOpen}>
1630 <MenuTrigger1740 <MenuTrigger
1631 aria-label="Filter threads by project"1741 aria-label="Filter threads by project"
1632 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"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,17 +1787,15 @@ export default function SidebarV2() {
1677 <span className="min-w-0 truncate text-sm">{project.displayName}</span>1787 <span className="min-w-0 truncate text-sm">{project.displayName}</span>
1678 <button1788 <button
1679 type="button"1789 type="button"
1680 aria-label={`Remove project ${project.displayName}`}1790 aria-label={`Project actions for ${project.displayName}`}
1681 title={`Remove ${project.displayName}`}1791 title={`Project actions for ${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"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 onPointerDown={(event) => event.stopPropagation()}1793 onPointerDown={(event) => event.stopPropagation()}
1684 onClick={(event) => {1794 onClick={(event) => {
1685 event.preventDefault();1795 void handleProjectActions(event, project);
1686 event.stopPropagation();
1687 void handleRemoveProject(project);
1688 }}1796 }}
1689 >1797 >
1690 <Trash2Icon className="size-3.5" />1798 <EllipsisIcon className="size-3.5" />
1691 </button>1799 </button>
1692 </MenuRadioItem>1800 </MenuRadioItem>
1693 );1801 );
...@@ -1845,6 +1953,178 @@ export default function SidebarV2() {...@@ -1845,6 +1953,178 @@ export default function SidebarV2() {
1845 ) : null}1953 ) : null}
1846 </SidebarGroup>1954 </SidebarGroup>
1847 </SidebarContent>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 <SidebarChromeFooter />2128 <SidebarChromeFooter />
1849 </>2129 </>
1850 );2130 );