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
+4
View File
@@ -18,3 +18,7 @@ GEMINI_API_KEY=your_gemini_api_key_here
# Get yours at https://console.groq.com
GROQ_API_KEY=your_groq_api_key_here
# Optional Morph Fast Apply
# Get yours at https://morphllm.com/
MORPH_API_KEY=your_fast_apply_key
+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,12 +511,61 @@ 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 {
// Send progress for each file
+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`);
@@ -307,6 +359,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) {
try {
@@ -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}`;
}
}
+180
View File
@@ -0,0 +1,180 @@
// Using direct fetch to Morph's OpenAI-compatible API to avoid SDK type issues
export interface MorphEditBlock {
targetFile: string;
instructions: string;
update: string;
}
export interface MorphApplyResult {
success: boolean;
normalizedPath?: string;
mergedCode?: string;
error?: string;
}
// Normalize project-relative paths to sandbox layout
export function normalizeProjectPath(inputPath: string): { normalizedPath: string; fullPath: string } {
let normalizedPath = inputPath.trim();
if (normalizedPath.startsWith('/')) normalizedPath = normalizedPath.slice(1);
const configFiles = new Set([
'tailwind.config.js',
'vite.config.js',
'package.json',
'package-lock.json',
'tsconfig.json',
'postcss.config.js'
]);
const fileName = normalizedPath.split('/').pop() || '';
if (!normalizedPath.startsWith('src/') &&
!normalizedPath.startsWith('public/') &&
normalizedPath !== 'index.html' &&
!configFiles.has(fileName)) {
normalizedPath = 'src/' + normalizedPath;
}
const fullPath = `/home/user/app/${normalizedPath}`;
return { normalizedPath, fullPath };
}
async function morphChatCompletionsCreate(payload: any) {
if (!process.env.MORPH_API_KEY) throw new Error('MORPH_API_KEY is not set');
const res = await fetch('https://api.morphllm.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.MORPH_API_KEY}`
},
body: JSON.stringify(payload)
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Morph API error ${res.status}: ${text}`);
}
return res.json();
}
// Parse <edit> blocks from LLM output
export function parseMorphEdits(text: string): MorphEditBlock[] {
const edits: MorphEditBlock[] = [];
const editRegex = /<edit\s+target_file="([^"]+)">([\s\S]*?)<\/edit>/g;
let match: RegExpExecArray | null;
while ((match = editRegex.exec(text)) !== null) {
const targetFile = match[1].trim();
const inner = match[2];
const instrMatch = inner.match(/<instructions>([\s\S]*?)<\/instructions>/);
const updateMatch = inner.match(/<update>([\s\S]*?)<\/update>/);
const instructions = instrMatch ? instrMatch[1].trim() : '';
const update = updateMatch ? updateMatch[1].trim() : '';
if (targetFile && update) {
edits.push({ targetFile, instructions, update });
}
}
return edits;
}
// Read a file from sandbox: prefers cache, then sandbox.files, then commands.run("cat ...")
async function readFileFromSandbox(sandbox: any, normalizedPath: string, fullPath: string): Promise<string> {
// Try backend cache first
if ((global as any).sandboxState?.fileCache?.files?.[normalizedPath]?.content) {
return (global as any).sandboxState.fileCache.files[normalizedPath].content as string;
}
// Try E2B files API
if (sandbox?.files?.read) {
return await sandbox.files.read(fullPath);
}
// Try shell cat via commands.run
if (sandbox?.commands?.run) {
const result = await sandbox.commands.run(`cat ${fullPath}`, { cwd: '/home/user/app', timeout: 30 });
if (result?.exitCode === 0 && typeof result?.stdout === 'string') {
return result.stdout as string;
}
}
throw new Error(`Unable to read file: ${normalizedPath}`);
}
// Write a file to sandbox and update cache
async function writeFileToSandbox(sandbox: any, normalizedPath: string, fullPath: string, content: string): Promise<void> {
// Prefer E2B files API
if (sandbox?.files?.write) {
await sandbox.files.write(fullPath, content);
} else if (sandbox?.runCode) {
// Use Python to write safely
const escaped = content
.replace(/\\/g, '\\\\')
.replace(/"""/g, '\"\"\"');
await sandbox.runCode(`
import os
os.makedirs(os.path.dirname("${fullPath}"), exist_ok=True)
with open("${fullPath}", 'w') as f:
f.write("""${escaped}""")
print("WROTE:${fullPath}")
`);
} else if (sandbox?.commands?.run) {
// Shell redirection (fallback)
// Note: beware of special chars; this is a last-resort path
const result = await sandbox.commands.run(`bash -lc 'mkdir -p "$(dirname "${fullPath}")" && cat > "${fullPath}" << \EOF\n${content}\nEOF'`, { cwd: '/home/user/app', timeout: 60 });
if (result?.exitCode !== 0) {
throw new Error(`Failed to write file via shell: ${normalizedPath}`);
}
} else {
throw new Error('No available method to write files to sandbox');
}
// Update backend cache if available
if ((global as any).sandboxState?.fileCache) {
(global as any).sandboxState.fileCache.files[normalizedPath] = {
content,
lastModified: Date.now()
};
}
if ((global as any).existingFiles) {
(global as any).existingFiles.add(normalizedPath);
}
}
export async function applyMorphEditToFile(params: {
sandbox: any;
targetPath: string;
instructions: string;
updateSnippet: string;
}): Promise<MorphApplyResult> {
try {
if (!process.env.MORPH_API_KEY) {
return { success: false, error: 'MORPH_API_KEY not set' };
}
const { normalizedPath, fullPath } = normalizeProjectPath(params.targetPath);
// Read original code (existence validation happens here)
const initialCode = await readFileFromSandbox(params.sandbox, normalizedPath, fullPath);
const resp = await morphChatCompletionsCreate({
model: 'morph-v3-large',
messages: [
{
role: 'user',
content: `<instruction>${params.instructions || ''}</instruction>\n<code>${initialCode}</code>\n<update>${params.updateSnippet}</update>`
}
]
});
const mergedCode = (resp as any)?.choices?.[0]?.message?.content || '';
if (!mergedCode) {
return { success: false, error: 'Morph returned empty content', normalizedPath };
}
await writeFileToSandbox(params.sandbox, normalizedPath, fullPath, mergedCode);
return { success: true, normalizedPath, mergedCode };
} catch (error) {
return { success: false, error: (error as Error).message };
}
}