import ignore from 'ignore'; import { useGit } from '~/lib/hooks/useGit'; import type { Message } from 'ai'; import { detectProjectCommands, createCommandsMessage, escapeBoltTags } from '~/utils/projectCommands'; import { generateId } from '~/utils/fileUtils'; import { useState } from 'react'; import { toast } from 'react-toastify'; import { LoadingOverlay } from '~/components/ui/LoadingOverlay'; import { RepositorySelectionDialog } from '~/components/@settings/tabs/connections/components/RepositorySelectionDialog'; import { classNames } from '~/utils/classNames'; import { Button } from '~/components/ui/Button'; import type { IChatMetadata } from '~/lib/persistence/db'; const IGNORE_PATTERNS = [ 'node_modules/**', '.git/**', '.github/**', '.vscode/**', 'dist/**', 'build/**', '.next/**', 'coverage/**', '.cache/**', '.idea/**', '**/*.log', '**/.DS_Store', '**/npm-debug.log*', '**/yarn-debug.log*', '**/yarn-error.log*', '**/*lock.json', '**/*lock.yaml', ]; const ig = ignore().add(IGNORE_PATTERNS); const MAX_FILE_SIZE = 100 * 1024; // 100KB limit per file const MAX_TOTAL_SIZE = 500 * 1024; // 500KB total limit interface GitCloneButtonProps { className?: string; importChat?: (description: string, messages: Message[], metadata?: IChatMetadata) => Promise; } export default function GitCloneButton({ importChat, className }: GitCloneButtonProps) { const { ready, gitClone } = useGit(); const [loading, setLoading] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false); const handleClone = async (repoUrl: string) => { if (!ready) { return; } setLoading(true); try { const { workdir, data } = await gitClone(repoUrl); if (importChat) { const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath)); const textDecoder = new TextDecoder('utf-8'); let totalSize = 0; const skippedFiles: string[] = []; const fileContents = []; for (const filePath of filePaths) { const { data: content, encoding } = data[filePath]; // Skip binary files if ( content instanceof Uint8Array && !filePath.match(/\.(txt|md|js|jsx|ts|tsx|json|html|css|scss|less|yml|yaml|xml|svg)$/i) ) { skippedFiles.push(filePath); continue; } try { const textContent = encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : ''; if (!textContent) { continue; } // Check file size const fileSize = new TextEncoder().encode(textContent).length; if (fileSize > MAX_FILE_SIZE) { skippedFiles.push(`${filePath} (too large: ${Math.round(fileSize / 1024)}KB)`); continue; } // Check total size if (totalSize + fileSize > MAX_TOTAL_SIZE) { skippedFiles.push(`${filePath} (would exceed total size limit)`); continue; } totalSize += fileSize; fileContents.push({ path: filePath, content: textContent, }); } catch (e: any) { skippedFiles.push(`${filePath} (error: ${e.message})`); } } const commands = await detectProjectCommands(fileContents); const commandsMessage = createCommandsMessage(commands); const filesMessage: Message = { role: 'assistant', content: `Cloning the repo ${repoUrl} into ${workdir} ${ skippedFiles.length > 0 ? `\nSkipped files (${skippedFiles.length}): ${skippedFiles.map((f) => `- ${f}`).join('\n')}` : '' } ${fileContents .map( (file) => ` ${escapeBoltTags(file.content)} `, ) .join('\n')} `, id: generateId(), createdAt: new Date(), }; const messages = [filesMessage]; if (commandsMessage) { messages.push(commandsMessage); } await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages); } } catch (error) { console.error('Error during import:', error); toast.error('Failed to import repository'); } finally { setLoading(false); } }; return ( <> setIsDialogOpen(false)} onSelect={handleClone} /> {loading && } ); }