apply-ai-code parser silently drops a file and corrupts another when a </file> tag is missing

Author: thejesh23Created Jul 20, 2026Updated Jul 20, 2026

Summary

The file-block parser in apply-ai-code silently drops a file and corrupts another when the model's output is missing a </file> closing tag on an earlier <file> block. The dropped file is never written, and the corrupted file is marked as "complete" so no truncation warning is emitted.

Location

  • app/api/apply-ai-code/route.ts:32 (parseAIResponse)
  • app/api/apply-ai-code-stream/route.ts:70 (same regex)
  • app/api/generate-ai-code-stream/route.ts:1568 (strict variant of the same parser)
typescript
const fileRegex = /<file path="([^"]+)">([\s\S]*?)(?:<\/file>|$)/g;

Root cause

The lazy [\s\S]*? stops at the first place the trailing group can match. If an earlier <file> block is missing its </file>, the only </file> (or end-of-string) is after a later file, so the first capture spans across the next file's opening tag — absorbing that later file's tag + body and never emitting it as its own entry. There is no (?=<file path=") boundary between blocks.

Worse, in apply-ai-code/route.ts the merged match ends with </file>, so hasClosingTag (line 37) is true and the block is treated as complete — no "appears to be truncated" warning fires, and the second file is silently lost.

Reproduction

Input (file A is missing its </file>):

<file path="src/A.jsx">AAA<file path="src/B.jsx">BBB</file>

Current parse result:

json
[{ "path": "src/A.jsx", "content": "AAA<file path=\"src/B.jsx\">BBB", "hasClosingTag": true }]

src/B.jsx is dropped entirely, and src/A.jsx is written with the next file's tag embedded in its body — and flagged complete, so no warning.

Impact

A single malformed/truncated </file> (common with LLM output) silently discards a whole file the user asked for and writes garbage into another, with no diagnostic. The user sees a broken app and no indication a file went missing.

Suggested fix

Add a (?=<file path=") look-ahead as an additional block terminator so a block always ends at the next file's opening tag:

diff
-const fileRegex = /<file path="([^"]+)">([\s\S]*?)(?:<\/file>|$)/g;
+const fileRegex = /<file path="([^"]+)">([\s\S]*?)(?:<\/file>|(?=<file path=")|$)/g;

With the fix the reproduction above parses to [("src/A.jsx","AAA"), ("src/B.jsx","BBB")], A is correctly flagged as missing its closing tag, and well-formed input / a truncated final file still parse exactly as before.

I have a fix ready and will open a PR referencing this issue.