|
1 | 1 | import { autoAnimate } from "@formkit/auto-animate"; |
2 | 2 | import { useAtomValue } from "@effect/atom-react"; |
3 | 3 | import { effectiveSettled } from "@t3tools/client-runtime/state/thread-settled"; |
4 | | -import type { |
5 | | - EnvironmentProject, |
6 | | - EnvironmentThreadShell, |
7 | | -} from "@t3tools/client-runtime/state/models"; |
| 4 | +import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/models"; |
8 | 5 | import { |
9 | 6 | scopeProjectRef, |
10 | 7 | scopeThreadRef, |
@@ -62,7 +59,10 @@ import { isMacPlatform } from "~/lib/utils"; |
62 | 59 | import { useOpenPrLink } from "../lib/openPullRequestLink"; |
63 | 60 | import { readLocalApi } from "../localApi"; |
64 | 61 | import { getProjectOrderKey, selectProjectGroupingSettings } from "../logicalProject"; |
65 | | -import { buildSidebarProjectSnapshots } from "../sidebarProjectGrouping"; |
| 62 | +import { |
| 63 | + buildSidebarProjectSnapshots, |
| 64 | + type SidebarProjectSnapshot, |
| 65 | +} from "../sidebarProjectGrouping"; |
66 | 66 | import { legacyProjectCwdPreferenceKey, useUiStateStore } from "../uiStateStore"; |
67 | 67 | import { useThreadSelectionStore } from "../threadSelectionStore"; |
68 | 68 | import { useThreadActions } from "../hooks/useThreadActions"; |
@@ -894,66 +894,86 @@ export default function SidebarV2() { |
894 | 894 | }, [clearSelection, projectScopeKey]); |
895 | 895 |
|
896 | 896 | const handleRemoveProject = useCallback( |
897 | | - async (project: EnvironmentProject) => { |
| 897 | + async (projectGroup: SidebarProjectSnapshot) => { |
898 | 898 | const api = readLocalApi(); |
899 | 899 | if (!api) return; |
900 | 900 |
|
901 | | - const projectThreads = threads.filter( |
902 | | - (thread) => |
903 | | - thread.environmentId === project.environmentId && thread.projectId === project.id, |
| 901 | + const memberKeys = new Set( |
| 902 | + projectGroup.memberProjectRefs.map( |
| 903 | + (projectRef) => `${projectRef.environmentId}:${projectRef.projectId}`, |
| 904 | + ), |
| 905 | + ); |
| 906 | + const projectThreads = threads.filter((thread) => |
| 907 | + memberKeys.has(`${thread.environmentId}:${thread.projectId}`), |
904 | 908 | ); |
905 | 909 | const confirmed = await settlePromise(() => |
906 | 910 | api.dialogs.confirm( |
907 | 911 | projectThreads.length > 0 |
908 | 912 | ? [ |
909 | | - `Remove project "${project.title}" and delete its ${projectThreads.length} thread${projectThreads.length === 1 ? "" : "s"}?`, |
910 | | - `Path: ${project.workspaceRoot}`, |
| 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 | + ]), |
911 | 919 | "This permanently clears conversation history for those threads.", |
912 | | - "This removes only the project entry, not the files on disk.", |
| 920 | + "This removes only the project entries, not the files on disk.", |
913 | 921 | "This action cannot be undone.", |
914 | 922 | ].join("\n") |
915 | 923 | : [ |
916 | | - `Remove project "${project.title}"?`, |
917 | | - `Path: ${project.workspaceRoot}`, |
918 | | - "This removes only the project entry, not the files on disk.", |
| 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.", |
919 | 931 | ].join("\n"), |
920 | 932 | ), |
921 | 933 | ); |
922 | 934 | if (confirmed._tag === "Failure" || !confirmed.value) return; |
923 | 935 |
|
924 | | - const projectRef = scopeProjectRef(project.environmentId, project.id); |
925 | | - const result = await deleteProject({ |
926 | | - environmentId: project.environmentId, |
927 | | - input: { |
928 | | - projectId: project.id, |
929 | | - ...(projectThreads.length > 0 ? { force: true } : {}), |
930 | | - }, |
931 | | - }); |
932 | | - if (result._tag === "Failure") { |
933 | | - if (!isAtomCommandInterrupted(result)) { |
934 | | - const error = squashAtomCommandFailure(result); |
935 | | - toastManager.add( |
936 | | - stackedThreadToast({ |
937 | | - type: "error", |
938 | | - title: `Failed to remove "${project.title}"`, |
939 | | - description: error instanceof Error ? error.message : "An error occurred.", |
940 | | - }), |
941 | | - ); |
| 936 | + const draftStore = useComposerDraftStore.getState(); |
| 937 | + let shouldNavigate = false; |
| 938 | + for (const project of projectGroup.memberProjects) { |
| 939 | + const memberThreads = projectThreads.filter( |
| 940 | + (thread) => |
| 941 | + thread.environmentId === project.environmentId && thread.projectId === project.id, |
| 942 | + ); |
| 943 | + const projectRef = scopeProjectRef(project.environmentId, project.id); |
| 944 | + const projectDraftThread = draftStore.getDraftThreadByProjectRef(projectRef); |
| 945 | + shouldNavigate ||= shouldNavigateAfterProjectRemoval({ |
| 946 | + routeTarget: routeTargetRef.current, |
| 947 | + projectThreads: memberThreads, |
| 948 | + projectDraftId: projectDraftThread?.draftId ?? null, |
| 949 | + }); |
| 950 | + |
| 951 | + const result = await deleteProject({ |
| 952 | + environmentId: project.environmentId, |
| 953 | + input: { |
| 954 | + projectId: project.id, |
| 955 | + ...(memberThreads.length > 0 ? { force: true } : {}), |
| 956 | + }, |
| 957 | + }); |
| 958 | + if (result._tag === "Failure") { |
| 959 | + if (!isAtomCommandInterrupted(result)) { |
| 960 | + const error = squashAtomCommandFailure(result); |
| 961 | + toastManager.add( |
| 962 | + stackedThreadToast({ |
| 963 | + type: "error", |
| 964 | + title: `Failed to remove "${project.title}"`, |
| 965 | + description: error instanceof Error ? error.message : "An error occurred.", |
| 966 | + }), |
| 967 | + ); |
| 968 | + } |
| 969 | + return; |
942 | 970 | } |
943 | | - return; |
944 | | - } |
945 | 971 |
|
946 | | - const draftStore = useComposerDraftStore.getState(); |
947 | | - const projectDraftThread = draftStore.getDraftThreadByProjectRef(projectRef); |
948 | | - const shouldNavigate = shouldNavigateAfterProjectRemoval({ |
949 | | - routeTarget: routeTargetRef.current, |
950 | | - projectThreads, |
951 | | - projectDraftId: projectDraftThread?.draftId ?? null, |
952 | | - }); |
953 | | - if (projectDraftThread) { |
954 | | - draftStore.clearDraftThread(projectDraftThread.draftId); |
| 972 | + if (projectDraftThread) { |
| 973 | + draftStore.clearDraftThread(projectDraftThread.draftId); |
| 974 | + } |
| 975 | + draftStore.clearProjectDraftThreadId(projectRef); |
955 | 976 | } |
956 | | - draftStore.clearProjectDraftThreadId(projectRef); |
957 | 977 |
|
958 | 978 | if (shouldNavigate) { |
959 | 979 | void router.navigate({ to: "/" }); |
@@ -1650,11 +1670,11 @@ export default function SidebarV2() { |
1650 | 1670 | cwd={project.workspaceRoot} |
1651 | 1671 | className="size-4 shrink-0" |
1652 | 1672 | /> |
1653 | | - <span className="min-w-0 truncate text-sm">{project.title}</span> |
| 1673 | + <span className="min-w-0 truncate text-sm">{project.displayName}</span> |
1654 | 1674 | <button |
1655 | 1675 | type="button" |
1656 | | - aria-label={`Remove project ${project.title}`} |
1657 | | - title={`Remove ${project.title}`} |
| 1676 | + aria-label={`Remove project ${project.displayName}`} |
| 1677 | + title={`Remove ${project.displayName}`} |
1658 | 1678 | 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" |
1659 | 1679 | onPointerDown={(event) => event.stopPropagation()} |
1660 | 1680 | onClick={(event) => { |
|
0 commit comments