| ... | @@ -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"; |
| 10 | import type { ScopedThreadRef } from "@t3tools/contracts"; | 10 | import type { ScopedThreadRef, SidebarProjectGroupingMode } from "@t3tools/contracts"; |
| 11 | import { | 11 | import { |
| 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 |
| 58 | import { isMacPlatform } from "~/lib/utils"; | 60 | import { isMacPlatform } from "~/lib/utils"; |
| 59 | import { useOpenPrLink } from "../lib/openPullRequestLink"; | 61 | import { useOpenPrLink } from "../lib/openPullRequestLink"; |
| 60 | import { readLocalApi } from "../localApi"; | 62 | import { readLocalApi } from "../localApi"; |
| 61 | import { getProjectOrderKey, selectProjectGroupingSettings } from "../logicalProject"; | 63 | import { |
| | 64 | deriveProjectGroupingOverrideKey, |
| | 65 | getProjectOrderKey, |
| | 66 | selectProjectGroupingSettings, |
| | 67 | } from "../logicalProject"; |
| 62 | import { | 68 | import { |
| 63 | buildSidebarProjectSnapshots, | 69 | buildSidebarProjectSnapshots, |
| | 70 | type SidebarProjectGroupMember, |
| 64 | type SidebarProjectSnapshot, | 71 | type SidebarProjectSnapshot, |
| 65 | } from "../sidebarProjectGrouping"; | 72 | } from "../sidebarProjectGrouping"; |
| 66 | import { legacyProjectCwdPreferenceKey, useUiStateStore } from "../uiStateStore"; | 73 | import { legacyProjectCwdPreferenceKey, useUiStateStore } from "../uiStateStore"; |
| ... | @@ -69,7 +76,8 @@ import { useThreadActions } from "../hooks/useThreadActions"; | ... | @@ -69,7 +76,8 @@ import { useThreadActions } from "../hooks/useThreadActions"; |
| 69 | import { useHandleNewThread } from "../hooks/useHandleNewThread"; | 76 | import { useHandleNewThread } from "../hooks/useHandleNewThread"; |
| 70 | import { openCommandPalette } from "../commandPaletteBus"; | 77 | import { openCommandPalette } from "../commandPaletteBus"; |
| 71 | import { startNewThreadFromContext } from "../lib/chatThreadActions"; | 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 | import { useEnvironments, usePrimaryEnvironmentId } from "../state/environments"; | 81 | import { useEnvironments, usePrimaryEnvironmentId } from "../state/environments"; |
| 74 | import { useProjects, useThreadShells } from "../state/entities"; | 82 | import { useProjects, useThreadShells } from "../state/entities"; |
| 75 | import { environmentServerConfigsAtom, primaryServerKeybindingsAtom } from "../state/server"; | 83 | import { 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 |
| 102 | import { primaryServerProvidersAtom } from "../state/server"; | 110 | import { primaryServerProvidersAtom } from "../state/server"; |
| 103 | import { stackedThreadToast, toastManager } from "./ui/toast"; | 111 | import { stackedThreadToast, toastManager } from "./ui/toast"; |
| 104 | import { CommandDialogTrigger } from "./ui/command"; | 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 | import { Kbd } from "./ui/kbd"; | 124 | import { Kbd } from "./ui/kbd"; |
| 106 | import { Menu, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuTrigger } from "./ui/menu"; | 125 | import { Menu, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuTrigger } from "./ui/menu"; |
| | 126 | import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "./ui/select"; |
| 107 | import { SidebarContent, SidebarGroup, SidebarMenuButton, useSidebar } from "./ui/sidebar"; | 127 | import { SidebarContent, SidebarGroup, SidebarMenuButton, useSidebar } from "./ui/sidebar"; |
| 108 | import { SidebarChromeFooter, SidebarChromeHeader } from "./sidebar/SidebarChrome"; | 128 | import { SidebarChromeFooter, SidebarChromeHeader } from "./sidebar/SidebarChrome"; |
| 109 | import { Tooltip, TooltipPopup, TooltipProvider, TooltipTrigger } from "./ui/tooltip"; | 129 | import { 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. |
| 114 | const SETTLED_TAIL_INITIAL_COUNT = 10; | 134 | const SETTLED_TAIL_INITIAL_COUNT = 10; |
| 115 | const SETTLED_TAIL_PAGE_COUNT = 25; | 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 | function compactSidebarTimeLabel(label: string): string { | 142 | function 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]); |
| 895 | | 946 | |
| 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; |
| 900 | | 951 | |
| 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 > 0 | 961 | 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 === 1 | 964 | ...(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 === 1 | 980 | ...(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() { |
| 935 | | 995 | |
| 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 | ); |
| 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 | // Settled threads stay in the live shell stream (settled ≠ archived), so | 1099 | // Settled threads stay in the live shell stream (settled ≠ archived), so |
| 990 | // the partition works directly off live shells: no archived-snapshot | 1100 | // 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 | <MenuTrigger | 1740 | <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 | <button | 1788 | <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 | ); |