Unified · Split
| @@ -1,10 +1,7 @@ | ||
| 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"; · 51 unmodified lines | ||
| 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() { · 825 unmodified lines | ||
| 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() { · 690 unmodified lines | ||
| 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) => { |