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 280c177619
commit 2327442b89
5 changed files with 351 additions and 7 deletions
+66 -2
View File
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
import { parseMorphEdits, applyMorphEditToFile } from '@/lib/morph-fast-apply';
import { Sandbox } from '@e2b/code-interpreter';
import type { SandboxState } from '@/types/sandbox';
import type { ConversationState } from '@/types/conversation';
@@ -278,6 +279,12 @@ export async function POST(request: NextRequest) {
// Parse the AI response
const parsed = parseAIResponse(response);
const morphEnabled = Boolean(isEdit && process.env.MORPH_API_KEY);
const morphEdits = morphEnabled ? parseMorphEdits(response) : [];
console.log('[apply-ai-code-stream] Morph Fast Apply mode:', morphEnabled);
if (morphEnabled) {
console.log('[apply-ai-code-stream] Morph edits found:', morphEdits.length);
}
// Log what was parsed
console.log('[apply-ai-code-stream] Parsed result:');
@@ -392,6 +399,14 @@ export async function POST(request: NextRequest) {
message: 'Starting code application...',
totalSteps: 3
});
if (morphEnabled) {
await sendProgress({ type: 'info', message: 'Morph Fast Apply enabled' });
await sendProgress({ type: 'info', message: `Parsed ${morphEdits.length} Morph edits` });
if (morphEdits.length === 0) {
console.warn('[apply-ai-code-stream] Morph enabled but no <edit> blocks found; falling back to full-file flow');
await sendProgress({ type: 'warning', message: 'Morph enabled but no <edit> blocks found; falling back to full-file flow' });
}
}
// Step 1: Install packages
const packagesArray = Array.isArray(packages) ? packages : [];
@@ -463,7 +478,7 @@ export async function POST(request: NextRequest) {
if (data.type === 'success' && data.installedPackages) {
results.packagesInstalled = data.installedPackages;
}
} catch (e) {
} catch {
// Ignore parse errors
}
}
@@ -496,11 +511,60 @@ export async function POST(request: NextRequest) {
// 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'];
const filteredFiles = filesArray.filter(file => {
let filteredFiles = filesArray.filter(file => {
if (!file || typeof file !== 'object') return false;
const fileName = (file.path || '').split('/').pop() || '';
return !configFiles.includes(fileName);
});
// If Morph is enabled and we have edits, apply them before file writes
const morphUpdatedPaths = new Set<string>();
if (morphEnabled && morphEdits.length > 0 && sandboxInstance) {
await sendProgress({ type: 'info', message: `Applying ${morphEdits.length} fast edits via Morph...` });
for (const [idx, edit] of morphEdits.entries()) {
try {
await sendProgress({ type: 'file-progress', current: idx + 1, total: morphEdits.length, fileName: edit.targetFile, action: 'morph-applying' });
const result = await applyMorphEditToFile({
sandbox: sandboxInstance,
targetPath: edit.targetFile,
instructions: edit.instructions,
updateSnippet: edit.update
});
if (result.success && result.normalizedPath) {
console.log('[apply-ai-code-stream] Morph updated', result.normalizedPath);
morphUpdatedPaths.add(result.normalizedPath);
if (results.filesUpdated) results.filesUpdated.push(result.normalizedPath);
await sendProgress({ type: 'file-complete', fileName: result.normalizedPath, action: 'morph-updated' });
} else {
const msg = result.error || 'Unknown Morph error';
console.error('[apply-ai-code-stream] Morph apply failed for', edit.targetFile, msg);
if (results.errors) results.errors.push(`Morph apply failed for ${edit.targetFile}: ${msg}`);
await sendProgress({ type: 'file-error', fileName: edit.targetFile, error: msg });
}
} catch (err) {
const msg = (err as Error).message;
console.error('[apply-ai-code-stream] Morph apply exception for', edit.targetFile, msg);
if (results.errors) results.errors.push(`Morph apply exception for ${edit.targetFile}: ${msg}`);
await sendProgress({ type: 'file-error', fileName: edit.targetFile, error: msg });
}
}
}
// Avoid overwriting Morph-updated files in the file write loop
if (morphUpdatedPaths.size > 0) {
filteredFiles = filteredFiles.filter(file => {
if (!file?.path) return true;
let normalizedPath = file.path.startsWith('/') ? file.path.slice(1) : file.path;
const fileName = normalizedPath.split('/').pop() || '';
if (!normalizedPath.startsWith('src/') &&
!normalizedPath.startsWith('public/') &&
normalizedPath !== 'index.html' &&
!configFiles.includes(fileName)) {
normalizedPath = 'src/' + normalizedPath;
}
return !morphUpdatedPaths.has(normalizedPath);
});
}
for (const [index, file] of filteredFiles.entries()) {
try {
+70 -3
View File
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
import { parseMorphEdits, applyMorphEditToFile } from '@/lib/morph-fast-apply';
import type { SandboxState } from '@/types/sandbox';
import type { ConversationState } from '@/types/conversation';
@@ -144,6 +145,12 @@ export async function POST(request: NextRequest) {
// Parse the AI response
const parsed = parseAIResponse(response);
const morphEnabled = Boolean(isEdit && process.env.MORPH_API_KEY);
const morphEdits = morphEnabled ? parseMorphEdits(response) : [];
console.log('[apply-ai-code] Morph Fast Apply mode:', morphEnabled);
if (morphEnabled) {
console.log('[apply-ai-code] Morph edits found:', morphEdits.length);
}
// Initialize existingFiles if not already
if (!global.existingFiles) {
@@ -172,6 +179,14 @@ export async function POST(request: NextRequest) {
console.log('[apply-ai-code] Is edit mode:', isEdit);
console.log('[apply-ai-code] Files to write:', parsed.files.map(f => f.path));
console.log('[apply-ai-code] Existing files:', Array.from(global.existingFiles));
if (morphEnabled) {
console.log('[apply-ai-code] Morph Fast Apply enabled');
if (morphEdits.length > 0) {
console.log('[apply-ai-code] Parsed Morph edits:', morphEdits.map(e => e.targetFile));
} else {
console.log('[apply-ai-code] No <edit> blocks found in response');
}
}
const results = {
filesCreated: [] as string[],
@@ -296,9 +311,46 @@ export async function POST(request: NextRequest) {
}
}
// Attempt Morph Fast Apply for edits before file creation
const morphUpdatedPaths = new Set<string>();
if (morphEnabled && morphEdits.length > 0) {
if (!global.activeSandbox) {
console.warn('[apply-ai-code] Morph edits found but no active sandbox; skipping Morph application');
} else {
console.log(`[apply-ai-code] Applying ${morphEdits.length} fast edits via Morph...`);
for (const edit of morphEdits) {
try {
const result = await applyMorphEditToFile({
sandbox: global.activeSandbox,
targetPath: edit.targetFile,
instructions: edit.instructions,
updateSnippet: edit.update
});
if (result.success && result.normalizedPath) {
morphUpdatedPaths.add(result.normalizedPath);
results.filesUpdated.push(result.normalizedPath);
console.log('[apply-ai-code] Morph applied to', result.normalizedPath);
} else {
const msg = result.error || 'Unknown Morph error';
console.error('[apply-ai-code] Morph apply failed:', msg);
results.errors.push(`Morph apply failed for ${edit.targetFile}: ${msg}`);
}
} catch (e) {
console.error('[apply-ai-code] Morph apply exception:', e);
results.errors.push(`Morph apply exception for ${edit.targetFile}: ${(e as Error).message}`);
}
}
}
}
if (morphEnabled && morphEdits.length === 0) {
console.warn('[apply-ai-code] Morph enabled but no <edit> blocks found; falling back to full-file flow');
}
// 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'];
const filteredFiles = parsed.files.filter(file => {
let filteredFiles = parsed.files.filter(file => {
const fileName = file.path.split('/').pop() || '';
if (configFiles.includes(fileName)) {
console.warn(`[apply-ai-code] Skipping config file: ${file.path} - already exists in template`);
@@ -306,6 +358,21 @@ export async function POST(request: NextRequest) {
}
return true;
});
// Avoid overwriting files already updated by Morph
if (morphUpdatedPaths.size > 0) {
filteredFiles = filteredFiles.filter(file => {
let normalizedPath = file.path.startsWith('/') ? file.path.slice(1) : file.path;
const fileName = normalizedPath.split('/').pop() || '';
if (!normalizedPath.startsWith('src/') &&
!normalizedPath.startsWith('public/') &&
normalizedPath !== 'index.html' &&
!configFiles.includes(fileName)) {
normalizedPath = 'src/' + normalizedPath;
}
return !morphUpdatedPaths.has(normalizedPath);
});
}
// Create or update files AFTER package installation
for (const file of filteredFiles) {
@@ -354,7 +421,7 @@ export async function POST(request: NextRequest) {
} catch (writeError) {
console.error(`[apply-ai-code] E2B file write error:`, writeError);
throw writeError;
throw writeError as Error;
}
@@ -491,7 +558,7 @@ with open(file_path, 'w') as f:
print(f"Auto-generated: {file_path}")
`);
results.filesCreated.push('src/index.css (with Tailwind)');
} catch (error) {
} catch {
results.errors.push('Failed to create index.css with Tailwind');
}
}
+30 -1
View File
@@ -551,7 +551,7 @@ Remember: You are a SURGEON making a precise incision, not an artist repainting
}
// Build system prompt with conversation awareness
const systemPrompt = `You are an expert React developer with perfect memory of the conversation. You maintain context across messages and remember scraped websites, generated components, and applied code. Generate clean, modern React code for Vite applications.
let systemPrompt = `You are an expert React developer with perfect memory of the conversation. You maintain context across messages and remember scraped websites, generated components, and applied code. Generate clean, modern React code for Vite applications.
${conversationContext}
🚨 CRITICAL RULES - YOUR MOST IMPORTANT INSTRUCTIONS:
@@ -897,6 +897,24 @@ CRITICAL: When files are provided in the context:
4. Do NOT ask to see files - they are already provided in the context above
5. Make the requested change immediately`;
// If Morph Fast Apply is enabled (edit mode + MORPH_API_KEY), force <edit> block output
const morphFastApplyEnabled = Boolean(isEdit && process.env.MORPH_API_KEY);
if (morphFastApplyEnabled) {
systemPrompt += `
MORPH FAST APPLY MODE (EDIT-ONLY):
- Output edits as <edit> blocks, not full <file> blocks, for files that already exist.
- Format for each edit:
<edit target_file="src/components/Header.jsx">
<instructions>Describe the minimal change, single sentence.</instructions>
<update>Provide the SMALLEST code snippet necessary to perform the change.</update>
</edit>
- Only use <file> blocks when you must CREATE a brand-new file.
- Prefer ONE edit block for a simple change; multiple edits only if absolutely needed for separate files.
- Keep updates minimal and precise; do not rewrite entire files.
`;
}
// Build full prompt with context
let fullPrompt = prompt;
if (context) {
@@ -1140,6 +1158,17 @@ CRITICAL: When files are provided in the context:
}
if (contextParts.length > 0) {
if (morphFastApplyEnabled) {
contextParts.push('\nOUTPUT FORMAT (REQUIRED IN MORPH MODE):');
contextParts.push('<edit target_file="src/components/Component.jsx">');
contextParts.push('<instructions>Minimal, precise instruction.</instructions>');
contextParts.push('<update>// Smallest necessary snippet</update>');
contextParts.push('</edit>');
contextParts.push('\nIf you need to create a NEW file, then and only then output a full file:');
contextParts.push('<file path="src/components/NewComponent.jsx">');
contextParts.push('// Full file content when creating new files');
contextParts.push('</file>');
}
fullPrompt = `CONTEXT:\n${contextParts.join('\n')}\n\nUSER REQUEST:\n${prompt}`;
}
}