File size: 11,649 Bytes
61e6dfe
 
 
 
 
 
 
 
 
 
 
318e485
 
61e6dfe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
import {
  END_FILE_CONTENT,
  END_PROJECT_NAME,
  START_FILE_CONTENT,
  START_PROJECT_NAME,
  SEARCH_START,
  DIVIDER,
  REPLACE_END,
} from "./prompts";
import { File } from "./type";

// todo: the Editing stuffs in message doesnt show when it"s a SEARCH and REPLACE operation, I mean it shows it but only at the end of the message, not during the generation. fix that.

/**
 * Validates that a filename has an extension.
 * Returns the filename if valid, null otherwise.
 * Allows dotfiles (like .gitignore) and regular files with extensions (like app.py).
 */
const validateFilename = (filename: string): string | null => {
  if (!filename) return null;

  const trimmed = filename.trim();
  if (!trimmed) return null;

  // Find the last dot in the filename
  const lastDotIndex = trimmed.lastIndexOf(".");

  // No dot found - invalid (no extension)
  if (lastDotIndex === -1) return null;

  // Dot at the end - invalid (no extension after dot)
  if (lastDotIndex === trimmed.length - 1) return null;

  // Dot at the start - this is a dotfile (like .gitignore), which is valid
  // Dot in the middle - this is a regular file with extension (like app.py), which is valid
  // Both cases are acceptable as long as there's something after the dot

  return trimmed;
};

/**
 * Check if the end of a string contains a partial match of a target string
 */
const hasPartialMarkerAtEnd = (text: string, marker: string): number => {
  // Check for partial matches starting from length 3 (to avoid false positives with common substrings)
  for (
    let len = Math.max(3, Math.floor(marker.length / 2));
    len < marker.length;
    len++
  ) {
    const partialMarker = marker.substring(0, len);
    if (text.endsWith(partialMarker)) {
      return len; // Return the length of the partial match
    }
  }
  return 0; // No partial match found
};

/**
 * Extract message content by removing all special markers and their content
 */
const extractMessageContent = (message: string): string => {
  let result = message;

  // Remove all complete START_FILE_CONTENT...END_FILE_CONTENT blocks
  const fileContentRegex = new RegExp(
    `${START_FILE_CONTENT}[\\s\\S]*?${END_FILE_CONTENT}`,
    "g"
  );
  result = result.replace(fileContentRegex, "");

  // Remove incomplete START_FILE_CONTENT blocks (streaming - no END_FILE_CONTENT yet)
  const incompleteFileIndex = result.indexOf(START_FILE_CONTENT);
  if (incompleteFileIndex !== -1) {
    result = result.substring(0, incompleteFileIndex);
  }

  // Remove all complete START_PROJECT_NAME...END_PROJECT_NAME blocks
  const projectNameRegex = new RegExp(
    `${START_PROJECT_NAME}[\\s\\S]*?${END_PROJECT_NAME}`,
    "g"
  );
  result = result.replace(projectNameRegex, "");

  // Remove incomplete START_PROJECT_NAME blocks (streaming - no END_PROJECT_NAME yet)
  const incompleteProjectIndex = result.indexOf(START_PROJECT_NAME);
  if (incompleteProjectIndex !== -1) {
    result = result.substring(0, incompleteProjectIndex);
  }

  // Remove all complete SEARCH_START...REPLACE_END blocks
  const searchReplaceRegex = new RegExp(
    `${SEARCH_START}[\\s\\S]*?${REPLACE_END}`,
    "g"
  );
  result = result.replace(searchReplaceRegex, "");

  // Remove incomplete SEARCH_START blocks (streaming - no REPLACE_END yet)
  const incompleteSearchIndex = result.indexOf(SEARCH_START);
  if (incompleteSearchIndex !== -1) {
    result = result.substring(0, incompleteSearchIndex);
  }

  // Handle incomplete/partial markers at the end (for streaming)
  // This is critical to prevent showing marker text to users during streaming
  const markers = [START_FILE_CONTENT, START_PROJECT_NAME, SEARCH_START];

  for (const marker of markers) {
    const partialLength = hasPartialMarkerAtEnd(result, marker);
    if (partialLength > 0) {
      result = result.substring(0, result.length - partialLength);
      break; // Only one marker can be at the end
    }
  }

  // Clean up extra whitespace
  return result.trim();
};

export const formatResponse = (message: string, currentFiles: File[]) => {
  // Track which files have been created or modified
  const modifiedFiles = new Map<string, File>();

  // Extract message content (everything outside special markers)
  const messageContent = extractMessageContent(message);

  // Extract project title
  let projectTitle =
    message.split(START_PROJECT_NAME)[1]?.split(END_PROJECT_NAME)[0]?.trim() ??
    "";

  // Handle partial project title markers
  if (projectTitle && !message.includes(END_PROJECT_NAME)) {
    for (let i = END_PROJECT_NAME.length - 1; i > 0; i--) {
      const partialMarker = END_PROJECT_NAME.substring(0, i);
      if (projectTitle.endsWith(partialMarker)) {
        projectTitle = projectTitle
          .substring(0, projectTitle.length - partialMarker.length)
          .trim();
        break;
      }
    }
  }

  if (message.includes(SEARCH_START)) {
    const searchSections = message.split(SEARCH_START).slice(1);

    for (
      let sectionIndex = 0;
      sectionIndex < searchSections.length;
      sectionIndex++
    ) {
      const section = searchSections[sectionIndex];
      const isLastSection = sectionIndex === searchSections.length - 1;

      // Check if this section has a complete REPLACE_END marker
      const hasReplaceEnd = section.includes(REPLACE_END);

      // For incomplete sections (last section without REPLACE_END), skip processing
      // to avoid applying partial replacements that corrupt the file
      if (isLastSection && !hasReplaceEnd) {
        continue;
      }

      const searchPart = section.split(REPLACE_END)[0];
      if (!searchPart) continue;

      const dividerIndex = searchPart.indexOf(DIVIDER);
      if (dividerIndex === -1) continue;

      const searchContent = searchPart.substring(0, dividerIndex);
      const replaceContent = searchPart.substring(
        dividerIndex + DIVIDER.length
      );

      if (!searchContent || !replaceContent) continue;

      // Extract filename from the first line (same line as SEARCH_START)
      const firstNewlineIndex = searchContent.indexOf("\n");
      let filename = "";
      let searchContentAfterFilename = searchContent;

      if (firstNewlineIndex !== -1) {
        // Filename is on the first line (same line as SEARCH_START marker)
        filename = searchContent.substring(0, firstNewlineIndex).trim();
        searchContentAfterFilename = searchContent.substring(
          firstNewlineIndex + 1
        );
      } else {
        // No newline found, entire content might be just the filename
        filename = searchContent.trim();
        searchContentAfterFilename = "";
      }

      // Validate filename has extension
      const validatedFilename = validateFilename(filename);
      if (!validatedFilename) continue;
      filename = validatedFilename;

      const searchCodeStart = searchContentAfterFilename.indexOf("```");
      if (searchCodeStart === -1) continue;

      const searchCodeBlock =
        searchContentAfterFilename.substring(searchCodeStart);
      let searchMatch = searchCodeBlock.match(/^```[\w]*\n?([\s\S]*?)```/);
      if (!searchMatch) {
        searchMatch = searchCodeBlock.match(/^```[\w]*\n?([\s\S]*)/);
      }
      if (!searchMatch) continue;

      const searchText = searchMatch[1].trim();

      const replaceCodeStart = replaceContent.indexOf("```");
      if (replaceCodeStart === -1) continue;

      const replaceCodeBlock = replaceContent.substring(replaceCodeStart);
      let replaceMatch = replaceCodeBlock.match(/^```[\w]*\n?([\s\S]*?)```/);
      if (!replaceMatch) {
        replaceMatch = replaceCodeBlock.match(/^```[\w]*\n?([\s\S]*)/);
      }
      if (!replaceMatch) continue;

      let replaceText = replaceMatch[1];
      for (let i = 1; i < 3; i++) {
        const partialClosing = "`".repeat(i);
        if (replaceText.endsWith(partialClosing)) {
          replaceText = replaceText.substring(0, replaceText.length - i);
          break;
        }
      }
      replaceText = replaceText.trim();

      // First check if we already have a modified version of this file in the current operation
      const existingModifiedFile = modifiedFiles.get(filename);
      const fileToModify =
        existingModifiedFile || currentFiles.find((f) => f.path === filename);

      if (fileToModify && fileToModify.content) {
        const newContent = fileToModify.content.replace(
          searchText,
          replaceText
        );
        if (newContent !== fileToModify.content) {
          modifiedFiles.set(filename, { path: filename, content: newContent });
        }
      } else if (!fileToModify) {
        modifiedFiles.set(filename, { path: filename, content: replaceText });
      }
    }
  }

  // Handle new file creation blocks
  if (message.includes(START_FILE_CONTENT)) {
    const fileSections = message.split(START_FILE_CONTENT).slice(1);

    for (const section of fileSections) {
      const rawContent = section.split(END_FILE_CONTENT)[0] || "";

      // Extract filename - it's on the same line as START_FILE_CONTENT (before first newline)
      const firstNewlineIndex = rawContent.indexOf("\n");
      let path = "";
      let contentAfterFilename = rawContent;

      if (firstNewlineIndex !== -1) {
        // Filename is before the first newline (same line as START_FILE_CONTENT marker)
        path = rawContent.substring(0, firstNewlineIndex).trim();
        contentAfterFilename = rawContent.substring(firstNewlineIndex + 1);
      } else {
        // No newline found, entire content might be just the filename
        path = rawContent.trim();
        contentAfterFilename = "";
      }

      // Validate that path has an extension
      const validatedPath = validateFilename(path);
      if (!validatedPath) continue;
      path = validatedPath;

      // Find code block
      const codeBlockStart = contentAfterFilename.indexOf("```");

      if (codeBlockStart === -1) {
        // No code block, use raw content after filename
        const content = contentAfterFilename.trim();
        if (content || !currentFiles.find((f) => f.path === path)) {
          // Always add files from the message
          modifiedFiles.set(path, { path, content });
        }
      } else {
        // Has code block - extract content between ``` markers
        const afterCodeBlockStart =
          contentAfterFilename.substring(codeBlockStart);

        // Try to match complete code block first
        const completeMatch = afterCodeBlockStart.match(
          /^```[\w]*\n?([\s\S]*?)```/
        );

        if (completeMatch) {
          const content = completeMatch[1].trim();
          modifiedFiles.set(path, { path, content });
        } else {
          // Incomplete code block (streaming) - match everything after opening ```
          const incompleteMatch = afterCodeBlockStart.match(
            /^```[\w]*\n?([\s\S]*)/
          );
          if (incompleteMatch) {
            let content = incompleteMatch[1];
            // Remove trailing partial closing marker if present
            for (let i = 1; i < 3; i++) {
              const partialClosing = "`".repeat(i);
              if (content.endsWith(partialClosing)) {
                content = content.substring(0, content.length - i);
                break;
              }
            }
            content = content.trim();
            modifiedFiles.set(path, { path, content });
          }
        }
      }
    }
  }

  // Convert modified files map to array
  const files = Array.from(modifiedFiles.values())?.filter(
    (file) => file.path !== "README.md"
  );

  return {
    messageContent,
    files,
    projectTitle,
  };
};