feat: implement default Git identity management and local identity check
This commit is contained in:
@@ -51,6 +51,7 @@ interface GitIdentitiesSidebarProps {
|
||||
export const GitIdentitiesSidebar: React.FC<GitIdentitiesSidebarProps> = ({ onItemSelect }) => {
|
||||
const {
|
||||
selectedProfileId,
|
||||
defaultGitIdentityId,
|
||||
profiles,
|
||||
globalIdentity,
|
||||
setSelectedProfile,
|
||||
@@ -58,6 +59,8 @@ export const GitIdentitiesSidebar: React.FC<GitIdentitiesSidebarProps> = ({ onIt
|
||||
loadProfiles,
|
||||
loadGlobalIdentity,
|
||||
loadDiscoveredCredentials,
|
||||
loadDefaultGitIdentityId,
|
||||
setDefaultGitIdentityId,
|
||||
getUnimportedCredentials,
|
||||
} = useGitIdentitiesStore();
|
||||
|
||||
@@ -82,7 +85,8 @@ export const GitIdentitiesSidebar: React.FC<GitIdentitiesSidebarProps> = ({ onIt
|
||||
loadProfiles();
|
||||
loadGlobalIdentity();
|
||||
loadDiscoveredCredentials();
|
||||
}, [loadProfiles, loadGlobalIdentity, loadDiscoveredCredentials]);
|
||||
loadDefaultGitIdentityId();
|
||||
}, [loadProfiles, loadGlobalIdentity, loadDiscoveredCredentials, loadDefaultGitIdentityId]);
|
||||
|
||||
const handleImportCredential = (credential: DiscoveredGitCredential) => {
|
||||
// Set a special "import" selection that carries the credential data
|
||||
@@ -119,6 +123,16 @@ export const GitIdentitiesSidebar: React.FC<GitIdentitiesSidebarProps> = ({ onIt
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleDefault = async (profileId: string) => {
|
||||
const next = defaultGitIdentityId === profileId ? null : profileId;
|
||||
const ok = await setDefaultGitIdentityId(next);
|
||||
if (!ok) {
|
||||
toast.error('Failed to update default identity');
|
||||
return;
|
||||
}
|
||||
toast.success(next ? 'Default identity updated' : 'Default identity unset');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('flex h-full flex-col', bgClass)}>
|
||||
<div className={cn('border-b px-3', isMobile ? 'mt-2 py-3' : 'py-3')}>
|
||||
@@ -146,6 +160,7 @@ export const GitIdentitiesSidebar: React.FC<GitIdentitiesSidebarProps> = ({ onIt
|
||||
<ProfileListItem
|
||||
profile={globalIdentity}
|
||||
isSelected={selectedProfileId === 'global'}
|
||||
isDefault={defaultGitIdentityId === 'global'}
|
||||
onSelect={() => {
|
||||
setSelectedProfile('global');
|
||||
onItemSelect?.();
|
||||
@@ -153,6 +168,7 @@ export const GitIdentitiesSidebar: React.FC<GitIdentitiesSidebarProps> = ({ onIt
|
||||
setSidebarOpen(false);
|
||||
}
|
||||
}}
|
||||
onToggleDefault={() => handleToggleDefault('global')}
|
||||
onDelete={undefined}
|
||||
isReadOnly
|
||||
/>
|
||||
@@ -179,6 +195,7 @@ export const GitIdentitiesSidebar: React.FC<GitIdentitiesSidebarProps> = ({ onIt
|
||||
key={profile.id}
|
||||
profile={profile}
|
||||
isSelected={selectedProfileId === profile.id}
|
||||
isDefault={defaultGitIdentityId === profile.id}
|
||||
onSelect={() => {
|
||||
setSelectedProfile(profile.id);
|
||||
onItemSelect?.();
|
||||
@@ -186,6 +203,7 @@ export const GitIdentitiesSidebar: React.FC<GitIdentitiesSidebarProps> = ({ onIt
|
||||
setSidebarOpen(false);
|
||||
}
|
||||
}}
|
||||
onToggleDefault={() => handleToggleDefault(profile.id)}
|
||||
onDelete={() => handleDeleteProfile(profile)}
|
||||
/>
|
||||
))}
|
||||
@@ -218,7 +236,9 @@ export const GitIdentitiesSidebar: React.FC<GitIdentitiesSidebarProps> = ({ onIt
|
||||
interface ProfileListItemProps {
|
||||
profile: GitIdentityProfile;
|
||||
isSelected: boolean;
|
||||
isDefault?: boolean;
|
||||
onSelect: () => void;
|
||||
onToggleDefault?: () => void | Promise<void>;
|
||||
onDelete?: () => void;
|
||||
isReadOnly?: boolean;
|
||||
}
|
||||
@@ -226,7 +246,9 @@ interface ProfileListItemProps {
|
||||
const ProfileListItem: React.FC<ProfileListItemProps> = ({
|
||||
profile,
|
||||
isSelected,
|
||||
isDefault = false,
|
||||
onSelect,
|
||||
onToggleDefault,
|
||||
onDelete,
|
||||
isReadOnly = false,
|
||||
}) => {
|
||||
@@ -258,6 +280,11 @@ const ProfileListItem: React.FC<ProfileListItemProps> = ({
|
||||
<span className="typography-micro text-muted-foreground bg-muted px-1 rounded flex-shrink-0 leading-none pb-px border border-border/50">
|
||||
{authType}
|
||||
</span>
|
||||
{isDefault && (
|
||||
<span className="typography-micro text-primary bg-primary/12 px-1 rounded flex-shrink-0 leading-none pb-px border border-primary/25">
|
||||
default
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="typography-micro text-muted-foreground/60 truncate leading-tight">
|
||||
@@ -265,7 +292,7 @@ const ProfileListItem: React.FC<ProfileListItemProps> = ({
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{!isReadOnly && onDelete && (
|
||||
{(onToggleDefault || (!isReadOnly && onDelete)) && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
@@ -276,17 +303,29 @@ const ProfileListItem: React.FC<ProfileListItemProps> = ({
|
||||
<RiMore2Line className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-fit min-w-20">
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
}}
|
||||
className="text-destructive focus:text-destructive"
|
||||
>
|
||||
<RiDeleteBinLine className="h-4 w-4 mr-px" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuContent align="end" className="w-fit min-w-28">
|
||||
{onToggleDefault && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
void onToggleDefault();
|
||||
}}
|
||||
>
|
||||
{isDefault ? 'Unset default' : 'Set as default'}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{!isReadOnly && onDelete && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
}}
|
||||
className="text-destructive focus:text-destructive"
|
||||
>
|
||||
<RiDeleteBinLine className="h-4 w-4 mr-px" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
@@ -25,6 +25,7 @@ import { getDesktopSettings, isDesktopRuntime, isVSCodeRuntime } from '@/lib/des
|
||||
import { updateDesktopSettings } from '@/lib/persistence';
|
||||
import type { DesktopSettings, SkillCatalogConfig } from '@/lib/desktop';
|
||||
import { useSkillsCatalogStore } from '@/stores/useSkillsCatalogStore';
|
||||
import { useGitIdentitiesStore } from '@/stores/useGitIdentitiesStore';
|
||||
|
||||
const generateCatalogId = () => `custom:${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
|
||||
@@ -81,6 +82,8 @@ interface AddCatalogDialogProps {
|
||||
|
||||
export const AddCatalogDialog: React.FC<AddCatalogDialogProps> = ({ open, onOpenChange }) => {
|
||||
const { scanRepo, loadCatalog, isScanning } = useSkillsCatalogStore();
|
||||
const defaultGitIdentityId = useGitIdentitiesStore((s) => s.defaultGitIdentityId);
|
||||
const loadDefaultGitIdentityId = useGitIdentitiesStore((s) => s.loadDefaultGitIdentityId);
|
||||
|
||||
const [label, setLabel] = React.useState('');
|
||||
const [source, setSource] = React.useState('');
|
||||
@@ -104,13 +107,14 @@ export const AddCatalogDialog: React.FC<AddCatalogDialogProps> = ({ open, onOpen
|
||||
setScanOk(false);
|
||||
setIdentityOptions([]);
|
||||
setGitIdentityId(null);
|
||||
void loadDefaultGitIdentityId();
|
||||
|
||||
void (async () => {
|
||||
const settings = await loadSettings();
|
||||
const catalogs = Array.isArray(settings?.skillCatalogs) ? settings?.skillCatalogs : [];
|
||||
setExistingCatalogs(catalogs || []);
|
||||
})();
|
||||
}, [open]);
|
||||
}, [open, loadDefaultGitIdentityId]);
|
||||
|
||||
const isDuplicate = React.useMemo(() => {
|
||||
const normalizedSource = source.trim();
|
||||
@@ -153,7 +157,13 @@ export const AddCatalogDialog: React.FC<AddCatalogDialogProps> = ({ open, onOpen
|
||||
const ids = (result.error.identities || []) as IdentityOption[];
|
||||
setIdentityOptions(ids);
|
||||
if (!gitIdentityId && ids.length > 0) {
|
||||
setGitIdentityId(ids[0].id);
|
||||
const preferred =
|
||||
defaultGitIdentityId &&
|
||||
defaultGitIdentityId !== 'global' &&
|
||||
ids.some((i) => i.id === defaultGitIdentityId)
|
||||
? defaultGitIdentityId
|
||||
: ids[0].id;
|
||||
setGitIdentityId(preferred);
|
||||
}
|
||||
toast.error('Authentication required. Select a Git identity and scan again.');
|
||||
return;
|
||||
|
||||
@@ -25,6 +25,7 @@ import { isVSCodeRuntime } from '@/lib/desktop';
|
||||
import type { SkillsCatalogItem } from '@/lib/api/types';
|
||||
import { useSkillsCatalogStore } from '@/stores/useSkillsCatalogStore';
|
||||
import { useSkillsStore } from '@/stores/useSkillsStore';
|
||||
import { useGitIdentitiesStore } from '@/stores/useGitIdentitiesStore';
|
||||
import { InstallConflictsDialog, type ConflictDecision, type SkillConflict } from './InstallConflictsDialog';
|
||||
|
||||
interface InstallFromRepoDialogProps {
|
||||
@@ -37,6 +38,8 @@ type IdentityOption = { id: string; name: string };
|
||||
export const InstallFromRepoDialog: React.FC<InstallFromRepoDialogProps> = ({ open, onOpenChange }) => {
|
||||
const { scanRepo, installSkills, isScanning, isInstalling } = useSkillsCatalogStore();
|
||||
const installedSkills = useSkillsStore((s) => s.skills);
|
||||
const defaultGitIdentityId = useGitIdentitiesStore((s) => s.defaultGitIdentityId);
|
||||
const loadDefaultGitIdentityId = useGitIdentitiesStore((s) => s.loadDefaultGitIdentityId);
|
||||
|
||||
const [source, setSource] = React.useState('');
|
||||
const [subpath, setSubpath] = React.useState('');
|
||||
@@ -69,10 +72,13 @@ export const InstallFromRepoDialog: React.FC<InstallFromRepoDialogProps> = ({ op
|
||||
setSearch('');
|
||||
setIdentities([]);
|
||||
setGitIdentityId(null);
|
||||
void loadDefaultGitIdentityId();
|
||||
|
||||
setConflictsOpen(false);
|
||||
|
||||
setConflicts([]);
|
||||
setBaseInstallRequest(null);
|
||||
}, [open]);
|
||||
}, [open, loadDefaultGitIdentityId]);
|
||||
|
||||
const installedByName = React.useMemo(() => {
|
||||
const map = new Map<string, { scope: 'user' | 'project' }>();
|
||||
@@ -127,7 +133,13 @@ export const InstallFromRepoDialog: React.FC<InstallFromRepoDialogProps> = ({ op
|
||||
const ids = (result.error.identities || []) as IdentityOption[];
|
||||
setIdentities(ids);
|
||||
if (!gitIdentityId && ids.length > 0) {
|
||||
setGitIdentityId(ids[0].id);
|
||||
const preferred =
|
||||
defaultGitIdentityId &&
|
||||
defaultGitIdentityId !== 'global' &&
|
||||
ids.some((i) => i.id === defaultGitIdentityId)
|
||||
? defaultGitIdentityId
|
||||
: ids[0].id;
|
||||
setGitIdentityId(preferred);
|
||||
}
|
||||
toast.error('Authentication required. Select a Git identity and try scanning again.');
|
||||
return;
|
||||
@@ -195,7 +207,13 @@ export const InstallFromRepoDialog: React.FC<InstallFromRepoDialogProps> = ({ op
|
||||
const ids = (result.error.identities || []) as IdentityOption[];
|
||||
setIdentities(ids);
|
||||
if (!gitIdentityId && ids.length > 0) {
|
||||
setGitIdentityId(ids[0].id);
|
||||
const preferred =
|
||||
defaultGitIdentityId &&
|
||||
defaultGitIdentityId !== 'global' &&
|
||||
ids.some((i) => i.id === defaultGitIdentityId)
|
||||
? defaultGitIdentityId
|
||||
: ids[0].id;
|
||||
setGitIdentityId(preferred);
|
||||
}
|
||||
toast.error('Authentication required. Select a Git identity and try installing again.');
|
||||
return;
|
||||
|
||||
@@ -60,7 +60,7 @@ export const GitView: React.FC = () => {
|
||||
? worktreeMap.get(currentSessionId) ?? undefined
|
||||
: undefined;
|
||||
|
||||
const { profiles, globalIdentity, loadProfiles, loadGlobalIdentity } =
|
||||
const { profiles, globalIdentity, defaultGitIdentityId, loadProfiles, loadGlobalIdentity, loadDefaultGitIdentityId } =
|
||||
useGitIdentitiesStore();
|
||||
|
||||
const isGitRepo = useIsGitRepo(currentDirectory ?? null);
|
||||
@@ -95,6 +95,21 @@ export const GitView: React.FC = () => {
|
||||
const [isSettingIdentity, setIsSettingIdentity] = React.useState(false);
|
||||
const { triggerFireworks } = useFireworksCelebration();
|
||||
|
||||
const autoAppliedDefaultRef = React.useRef<Map<string, string>>(new Map());
|
||||
const identityApplyCountRef = React.useRef(0);
|
||||
|
||||
const beginIdentityApply = React.useCallback(() => {
|
||||
identityApplyCountRef.current += 1;
|
||||
setIsSettingIdentity(true);
|
||||
}, []);
|
||||
|
||||
const endIdentityApply = React.useCallback(() => {
|
||||
identityApplyCountRef.current = Math.max(0, identityApplyCountRef.current - 1);
|
||||
if (identityApplyCountRef.current === 0) {
|
||||
setIsSettingIdentity(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const [selectedPaths, setSelectedPaths] = React.useState<Set<string>>(
|
||||
() => new Set(initialSnapshot?.selectedPaths ?? [])
|
||||
);
|
||||
@@ -187,7 +202,8 @@ export const GitView: React.FC = () => {
|
||||
React.useEffect(() => {
|
||||
loadProfiles();
|
||||
loadGlobalIdentity();
|
||||
}, [loadProfiles, loadGlobalIdentity]);
|
||||
loadDefaultGitIdentityId();
|
||||
}, [loadProfiles, loadGlobalIdentity, loadDefaultGitIdentityId]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!currentDirectory || !git?.getRemoteUrl) {
|
||||
@@ -238,6 +254,45 @@ export const GitView: React.FC = () => {
|
||||
await fetchIdentity(currentDirectory, git);
|
||||
}, [currentDirectory, git, fetchIdentity]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!currentDirectory) return;
|
||||
if (!git?.hasLocalIdentity) return;
|
||||
if (isGitRepo !== true) return;
|
||||
|
||||
const defaultId = typeof defaultGitIdentityId === 'string' ? defaultGitIdentityId.trim() : '';
|
||||
if (!defaultId || defaultId === 'global') return;
|
||||
|
||||
const previousAttempt = autoAppliedDefaultRef.current.get(currentDirectory);
|
||||
if (previousAttempt === defaultId) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
const hasLocal = await git.hasLocalIdentity!(currentDirectory);
|
||||
if (cancelled) return;
|
||||
if (hasLocal) return;
|
||||
|
||||
beginIdentityApply();
|
||||
await git.setGitIdentity(currentDirectory, defaultId);
|
||||
autoAppliedDefaultRef.current.set(currentDirectory, defaultId);
|
||||
await refreshIdentity();
|
||||
} catch (error) {
|
||||
console.warn('Failed to auto-apply default git identity:', error);
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
endIdentityApply();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void run();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [beginIdentityApply, currentDirectory, defaultGitIdentityId, endIdentityApply, git, isGitRepo, refreshIdentity]);
|
||||
|
||||
const changeEntries = React.useMemo(() => {
|
||||
if (!status) return [];
|
||||
const files = status.files ?? [];
|
||||
@@ -470,7 +525,7 @@ export const GitView: React.FC = () => {
|
||||
|
||||
const handleApplyIdentity = async (profile: GitIdentityProfile) => {
|
||||
if (!currentDirectory) return;
|
||||
setIsSettingIdentity(true);
|
||||
beginIdentityApply();
|
||||
|
||||
try {
|
||||
await git.setGitIdentity(currentDirectory, profile.id);
|
||||
@@ -480,7 +535,7 @@ export const GitView: React.FC = () => {
|
||||
const message = err instanceof Error ? err.message : 'Failed to apply git identity';
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setIsSettingIdentity(false);
|
||||
endIdentityApply();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user