added fast apply

if isEdit === true & Morph API Key is in .env:
- Morph Fast Apply is activated.
This commit is contained in:
Bekbol Bolatov
2025-09-11 18:24:27 +05:00
parent 10409e95b9
commit 9fdc2fac20
+91 -91
View File
@@ -30,28 +30,28 @@ function parseAIResponse(response: string): ParsedResponse {
explanation: '',
template: ''
};
// Function to extract packages from import statements
function extractPackagesFromCode(content: string): string[] {
const packages: string[] = [];
// Match ES6 imports
const importRegex = /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)(?:\s*,\s*(?:\{[^}]*\}|\*\s+as\s+\w+|\w+))*\s+from\s+)?['"]([^'"]+)['"]/g;
let importMatch;
while ((importMatch = importRegex.exec(content)) !== null) {
const importPath = importMatch[1];
// Skip relative imports and built-in React
if (!importPath.startsWith('.') && !importPath.startsWith('/') &&
importPath !== 'react' && importPath !== 'react-dom' &&
!importPath.startsWith('@/')) {
if (!importPath.startsWith('.') && !importPath.startsWith('/') &&
importPath !== 'react' && importPath !== 'react-dom' &&
!importPath.startsWith('@/')) {
// Extract package name (handle scoped packages like @heroicons/react)
const packageName = importPath.startsWith('@')
const packageName = importPath.startsWith('@')
? importPath.split('/').slice(0, 2).join('/')
: importPath.split('/')[0];
if (!packages.includes(packageName)) {
packages.push(packageName);
// Log important packages for debugging
if (packageName === 'react-router-dom' || packageName.includes('router') || packageName.includes('icon')) {
console.log(`[apply-ai-code-stream] Detected package from imports: ${packageName}`);
@@ -59,13 +59,13 @@ function parseAIResponse(response: string): ParsedResponse {
}
}
}
return packages;
}
// Parse file sections - handle duplicates and prefer complete versions
const fileMap = new Map<string, { content: string; isComplete: boolean }>();
// First pass: Find all file declarations
const fileRegex = /<file path="([^"]+)">([\s\S]*?)(?:<\/file>|$)/g;
let match;
@@ -73,10 +73,10 @@ function parseAIResponse(response: string): ParsedResponse {
const filePath = match[1];
const content = match[2].trim();
const hasClosingTag = response.substring(match.index, match.index + match[0].length).includes('</file>');
// Check if this file already exists in our map
const existing = fileMap.get(filePath);
// Decide whether to keep this version
let shouldReplace = false;
if (!existing) {
@@ -90,7 +90,7 @@ function parseAIResponse(response: string): ParsedResponse {
} else if (!existing.isComplete && !hasClosingTag && content.length > existing.content.length) {
shouldReplace = true; // Both incomplete, keep longer one
}
if (shouldReplace) {
// Additional validation: reject obviously broken content
if (content.includes('...') && !content.includes('...props') && !content.includes('...rest')) {
@@ -104,18 +104,18 @@ function parseAIResponse(response: string): ParsedResponse {
}
}
}
// Convert map to array for sections.files
for (const [path, { content, isComplete }] of fileMap.entries()) {
if (!isComplete) {
console.log(`[apply-ai-code-stream] Warning: File ${path} appears to be truncated (no closing tag)`);
}
sections.files.push({
path,
content
});
// Extract packages from file content
const filePackages = extractPackagesFromCode(content);
for (const pkg of filePackages) {
@@ -125,7 +125,7 @@ function parseAIResponse(response: string): ParsedResponse {
}
}
}
// Also parse markdown code blocks with file paths
const markdownFileRegex = /```(?:file )?path="([^"]+)"\n([\s\S]*?)```/g;
while ((match = markdownFileRegex.exec(response)) !== null) {
@@ -135,7 +135,7 @@ function parseAIResponse(response: string): ParsedResponse {
path: filePath,
content: content
});
// Extract packages from file content
const filePackages = extractPackagesFromCode(content);
for (const pkg of filePackages) {
@@ -145,7 +145,7 @@ function parseAIResponse(response: string): ParsedResponse {
}
}
}
// Parse plain text format like "Generated Files: Header.jsx, index.css"
const generatedFilesMatch = response.match(/Generated Files?:\s*([^\n]+)/i);
if (generatedFilesMatch) {
@@ -155,7 +155,7 @@ function parseAIResponse(response: string): ParsedResponse {
.map(f => f.trim())
.filter(f => f.endsWith('.jsx') || f.endsWith('.js') || f.endsWith('.tsx') || f.endsWith('.ts') || f.endsWith('.css') || f.endsWith('.json') || f.endsWith('.html'));
console.log(`[apply-ai-code-stream] Detected generated files from plain text: ${filesList.join(', ')}`);
// Try to extract the actual file content if it follows
for (const fileName of filesList) {
// Look for the file content after the file name
@@ -171,7 +171,7 @@ function parseAIResponse(response: string): ParsedResponse {
content: codeMatch[1].trim()
});
console.log(`[apply-ai-code-stream] Extracted content for ${filePath}`);
// Extract packages from this file
const filePackages = extractPackagesFromCode(codeMatch[1]);
for (const pkg of filePackages) {
@@ -184,7 +184,7 @@ function parseAIResponse(response: string): ParsedResponse {
}
}
}
// Also try to parse if the response contains raw JSX/JS code blocks
const codeBlockRegex = /```(?:jsx?|tsx?|javascript|typescript)?\n([\s\S]*?)```/g;
while ((match = codeBlockRegex.exec(response)) !== null) {
@@ -194,14 +194,14 @@ function parseAIResponse(response: string): ParsedResponse {
if (fileNameMatch) {
const fileName = fileNameMatch[1].trim();
const filePath = fileName.includes('/') ? fileName : `src/components/${fileName}`;
// Don't add duplicate files
if (!sections.files.some(f => f.path === filePath)) {
sections.files.push({
path: filePath,
content: content
});
// Extract packages
const filePackages = extractPackagesFromCode(content);
for (const pkg of filePackages) {
@@ -224,7 +224,7 @@ function parseAIResponse(response: string): ParsedResponse {
while ((match = pkgRegex.exec(response)) !== null) {
sections.packages.push(match[1].trim());
}
// Also parse <packages> tag with multiple packages
const packagesRegex = /<packages>([\s\S]*?)<\/packages>/;
const packagesMatch = response.match(packagesRegex);
@@ -264,20 +264,20 @@ function parseAIResponse(response: string): ParsedResponse {
export async function POST(request: NextRequest) {
try {
const { response, isEdit = false, packages = [], sandboxId } = await request.json();
if (!response) {
return NextResponse.json({
error: 'response is required'
}, { status: 400 });
}
// Debug log the response
console.log('[apply-ai-code-stream] Received response to parse:');
console.log('[apply-ai-code-stream] Response length:', response.length);
console.log('[apply-ai-code-stream] Response preview:', response.substring(0, 500));
console.log('[apply-ai-code-stream] isEdit:', isEdit);
console.log('[apply-ai-code-stream] packages:', packages);
// Parse the AI response
const parsed = parseAIResponse(response);
const morphEnabled = Boolean(isEdit && process.env.MORPH_API_KEY);
@@ -296,15 +296,15 @@ export async function POST(request: NextRequest) {
});
}
console.log('[apply-ai-code-stream] Packages found:', parsed.packages);
// Initialize existingFiles if not already
if (!global.existingFiles) {
global.existingFiles = new Set<string>();
}
// Try to get provider from sandbox manager first
let provider = sandboxId ? sandboxManager.getProvider(sandboxId) : sandboxManager.getActiveProvider();
// Fall back to global state if not found in manager
if (!provider) {
provider = global.activeSandboxProvider;
@@ -313,10 +313,10 @@ export async function POST(request: NextRequest) {
// If we have a sandboxId but no provider, try to get or create one
if (!provider && sandboxId) {
console.log(`[apply-ai-code-stream] No provider found for sandbox ${sandboxId}, attempting to get or create...`);
try {
provider = await sandboxManager.getOrCreateProvider(sandboxId);
// If we got a new provider (not reconnected), we need to create a new sandbox
if (!provider.getSandboxInfo()) {
console.log(`[apply-ai-code-stream] Creating new sandbox since reconnection failed for ${sandboxId}`);
@@ -324,7 +324,7 @@ export async function POST(request: NextRequest) {
await provider.setupViteApp();
sandboxManager.registerSandbox(sandboxId, provider);
}
// Update legacy global state
global.activeSandboxProvider = provider;
console.log(`[apply-ai-code-stream] Successfully got provider for sandbox ${sandboxId}`);
@@ -346,7 +346,7 @@ export async function POST(request: NextRequest) {
}, { status: 500 });
}
}
// If we still don't have a provider, create a new one
if (!provider) {
console.log(`[apply-ai-code-stream] No active provider found, creating new sandbox...`);
@@ -358,7 +358,7 @@ export async function POST(request: NextRequest) {
// Register with sandbox manager
sandboxManager.registerSandbox(sandboxInfo.sandboxId, provider);
// Store in legacy global state
global.activeSandboxProvider = provider;
global.sandboxData = {
@@ -385,18 +385,18 @@ export async function POST(request: NextRequest) {
}, { status: 500 });
}
}
// Create a response stream for real-time updates
const encoder = new TextEncoder();
const stream = new TransformStream();
const writer = stream.writable.getWriter();
// Function to send progress updates
const sendProgress = async (data: any) => {
const message = `data: ${JSON.stringify(data)}\n\n`;
await writer.write(encoder.encode(message));
};
// Start processing in background (pass provider and request to the async function)
(async (providerInstance, req) => {
const results = {
@@ -408,10 +408,10 @@ export async function POST(request: NextRequest) {
commandsExecuted: [] as string[],
errors: [] as string[]
};
try {
await sendProgress({
type: 'start',
await sendProgress({
type: 'start',
message: 'Starting code application...',
totalSteps: 3
});
@@ -427,75 +427,75 @@ export async function POST(request: NextRequest) {
// Step 1: Install packages
const packagesArray = Array.isArray(packages) ? packages : [];
const parsedPackages = Array.isArray(parsed.packages) ? parsed.packages : [];
// Combine and deduplicate packages
const allPackages = [...packagesArray.filter(pkg => pkg && typeof pkg === 'string'), ...parsedPackages];
// Use Set to remove duplicates, then filter out pre-installed packages
const uniquePackages = [...new Set(allPackages)]
.filter(pkg => pkg && typeof pkg === 'string' && pkg.trim() !== '') // Remove empty strings
.filter(pkg => pkg !== 'react' && pkg !== 'react-dom'); // Filter pre-installed
// Log if we found duplicates
if (allPackages.length !== uniquePackages.length) {
console.log(`[apply-ai-code-stream] Removed ${allPackages.length - uniquePackages.length} duplicate packages`);
console.log(`[apply-ai-code-stream] Original packages:`, allPackages);
console.log(`[apply-ai-code-stream] Deduplicated packages:`, uniquePackages);
}
if (uniquePackages.length > 0) {
await sendProgress({
type: 'step',
await sendProgress({
type: 'step',
step: 1,
message: `Installing ${uniquePackages.length} packages...`,
packages: uniquePackages
});
// Use streaming package installation
try {
// Construct the API URL properly for both dev and production
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http';
const host = req.headers.get('host') || 'localhost:3000';
const apiUrl = `${protocol}://${host}/api/install-packages`;
const installResponse = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
body: JSON.stringify({
packages: uniquePackages,
sandboxId: sandboxId || providerInstance.getSandboxInfo()?.sandboxId
})
});
if (installResponse.ok && installResponse.body) {
const reader = installResponse.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
if (!chunk) continue;
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
// Forward package installation progress
await sendProgress({
type: 'package-progress',
...data
});
// Track results
if (data.type === 'success' && data.installedPackages) {
results.packagesInstalled = data.installedPackages;
}
} catch (parseError) {
console.debug('Error parsing terminal output:', parseError);
console.debug('Error parsing terminal output:', parseError);
}
}
}
@@ -510,21 +510,21 @@ export async function POST(request: NextRequest) {
results.errors.push(`Package installation failed: ${(error as Error).message}`);
}
} else {
await sendProgress({
type: 'step',
await sendProgress({
type: 'step',
step: 1,
message: 'No additional packages to install, skipping...'
});
}
// Step 2: Create/update files
const filesArray = Array.isArray(parsed.files) ? parsed.files : [];
await sendProgress({
type: 'step',
await sendProgress({
type: 'step',
step: 2,
message: `Creating ${filesArray.length} files...`
});
// Filter out config files that shouldn't be created
const configFiles = ['tailwind.config.js', 'vite.config.js', 'package.json', 'package-lock.json', 'tsconfig.json', 'postcss.config.js'];
let filteredFiles = filesArray.filter(file => {
@@ -592,27 +592,27 @@ export async function POST(request: NextRequest) {
fileName: file.path,
action: 'creating'
});
// Normalize the file path
let normalizedPath = file.path;
if (normalizedPath.startsWith('/')) {
normalizedPath = normalizedPath.substring(1);
}
if (!normalizedPath.startsWith('src/') &&
!normalizedPath.startsWith('public/') &&
normalizedPath !== 'index.html' &&
!configFiles.includes(normalizedPath.split('/').pop() || '')) {
if (!normalizedPath.startsWith('src/') &&
!normalizedPath.startsWith('public/') &&
normalizedPath !== 'index.html' &&
!configFiles.includes(normalizedPath.split('/').pop() || '')) {
normalizedPath = 'src/' + normalizedPath;
}
const isUpdate = global.existingFiles.has(normalizedPath);
// Remove any CSS imports from JSX/JS files (we're using Tailwind)
let fileContent = file.content;
if (file.path.endsWith('.jsx') || file.path.endsWith('.js') || file.path.endsWith('.tsx') || file.path.endsWith('.ts')) {
fileContent = fileContent.replace(/import\s+['"]\.\/[^'"]+\.css['"];?\s*\n?/g, '');
}
// Fix common Tailwind CSS errors in CSS files
if (file.path.endsWith('.css')) {
// Replace shadow-3xl with shadow-2xl (shadow-3xl doesn't exist)
@@ -621,7 +621,7 @@ export async function POST(request: NextRequest) {
fileContent = fileContent.replace(/shadow-4xl/g, 'shadow-2xl');
fileContent = fileContent.replace(/shadow-5xl/g, 'shadow-2xl');
}
// Create directory if needed
const dirPath = normalizedPath.includes('/') ? normalizedPath.substring(0, normalizedPath.lastIndexOf('/')) : '';
if (dirPath) {
@@ -630,7 +630,7 @@ export async function POST(request: NextRequest) {
// Write the file using provider
await providerInstance.writeFile(normalizedPath, fileContent);
// Update file cache
if (global.sandboxState?.fileCache) {
global.sandboxState.fileCache.files[normalizedPath] = {
@@ -638,14 +638,14 @@ export async function POST(request: NextRequest) {
lastModified: Date.now()
};
}
if (isUpdate) {
if (results.filesUpdated) results.filesUpdated.push(normalizedPath);
} else {
if (results.filesCreated) results.filesCreated.push(normalizedPath);
if (global.existingFiles) global.existingFiles.add(normalizedPath);
}
await sendProgress({
type: 'file-complete',
fileName: normalizedPath,
@@ -662,16 +662,16 @@ export async function POST(request: NextRequest) {
});
}
}
// Step 3: Execute commands
const commandsArray = Array.isArray(parsed.commands) ? parsed.commands : [];
if (commandsArray.length > 0) {
await sendProgress({
type: 'step',
await sendProgress({
type: 'step',
step: 3,
message: `Executing ${commandsArray.length} commands...`
});
for (const [index, cmd] of commandsArray.entries()) {
try {
await sendProgress({
@@ -681,14 +681,14 @@ export async function POST(request: NextRequest) {
command: cmd,
action: 'executing'
});
// Use provider runCommand
const result = await providerInstance.runCommand(cmd);
// Get command output from provider result
const stdout = result.stdout;
const stderr = result.stderr;
if (stdout) {
await sendProgress({
type: 'command-output',
@@ -697,7 +697,7 @@ export async function POST(request: NextRequest) {
stream: 'stdout'
});
}
if (stderr) {
await sendProgress({
type: 'command-output',
@@ -706,11 +706,11 @@ export async function POST(request: NextRequest) {
stream: 'stderr'
});
}
if (results.commandsExecuted) {
results.commandsExecuted.push(cmd);
}
await sendProgress({
type: 'command-complete',
command: cmd,
@@ -729,7 +729,7 @@ export async function POST(request: NextRequest) {
}
}
}
// Send final results
await sendProgress({
type: 'complete',
@@ -738,7 +738,7 @@ export async function POST(request: NextRequest) {
structure: parsed.structure,
message: `Successfully applied ${results.filesCreated.length} files`
});
// Track applied files in conversation state
if (global.conversationState && results.filesCreated.length > 0) {
const messages = global.conversationState.context.messages;
@@ -751,7 +751,7 @@ export async function POST(request: NextRequest) {
};
}
}
// Track applied code in project evolution
if (global.conversationState.context.projectEvolution) {
global.conversationState.context.projectEvolution.majorChanges.push({
@@ -760,10 +760,10 @@ export async function POST(request: NextRequest) {
filesAffected: results.filesCreated || []
});
}
global.conversationState.lastUpdated = Date.now();
}
} catch (error) {
await sendProgress({
type: 'error',
@@ -773,7 +773,7 @@ export async function POST(request: NextRequest) {
await writer.close();
}
})(provider, request);
// Return the stream
return new Response(stream.readable, {
headers: {