@hapico/cli 0.0.28 → 0.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/index.js +1 -1
- package/bin/tools/vibe/index.js +274 -88
- package/bun.lock +6 -0
- package/dist/index.js +1 -1
- package/dist/tools/vibe/index.js +274 -88
- package/index.ts +1 -1
- package/package.json +3 -1
- package/tools/vibe/index.ts +307 -104
package/tools/vibe/index.ts
CHANGED
|
@@ -1,18 +1,47 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import { exec } from "node:child_process";
|
|
6
|
-
import util from "node:util";
|
|
4
|
+
import { diff_match_patch } from "diff-match-patch";
|
|
7
5
|
import express from "express";
|
|
8
6
|
import open from "open";
|
|
9
|
-
// import net
|
|
10
|
-
import os from "os";
|
|
11
|
-
|
|
12
|
-
const execAsync = util.promisify(exec);
|
|
13
7
|
import net from "node:net";
|
|
14
8
|
|
|
9
|
+
const scanRecursively = async (
|
|
10
|
+
dir: string,
|
|
11
|
+
baseDir: string
|
|
12
|
+
): Promise<FileNode[]> => {
|
|
13
|
+
let results: FileNode[] = [];
|
|
14
|
+
try {
|
|
15
|
+
const list = await fs.readdir(dir, { withFileTypes: true });
|
|
15
16
|
|
|
17
|
+
for (const entry of list) {
|
|
18
|
+
const fullPath = path.join(dir, entry.name);
|
|
19
|
+
const relativePath = path.relative(baseDir, fullPath);
|
|
20
|
+
|
|
21
|
+
if (shouldIgnorePath(relativePath)) continue;
|
|
22
|
+
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
const subResults = await scanRecursively(fullPath, baseDir);
|
|
25
|
+
results = results.concat(subResults);
|
|
26
|
+
} else if (entry.isFile()) {
|
|
27
|
+
try {
|
|
28
|
+
const content = await fs.readFile(fullPath, "utf-8");
|
|
29
|
+
results.push({
|
|
30
|
+
path: relativePath,
|
|
31
|
+
fullPath: fullPath,
|
|
32
|
+
content: content,
|
|
33
|
+
extension: entry.name.split(".").pop() || "txt",
|
|
34
|
+
});
|
|
35
|
+
} catch (err) {
|
|
36
|
+
// Bỏ qua file nhị phân hoặc không đọc được
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
// Bỏ qua nếu lỗi permission hoặc path không tồn tại
|
|
42
|
+
}
|
|
43
|
+
return results;
|
|
44
|
+
};
|
|
16
45
|
|
|
17
46
|
// random port from 3000-3999
|
|
18
47
|
// Check if port is available would be better in real-world usage
|
|
@@ -94,118 +123,261 @@ const shouldIgnorePath = (filePath: string): boolean => {
|
|
|
94
123
|
return false;
|
|
95
124
|
};
|
|
96
125
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
126
|
+
/**
|
|
127
|
+
* ------------------------------------------------------------------
|
|
128
|
+
* TYPES
|
|
129
|
+
* ------------------------------------------------------------------
|
|
130
|
+
*/
|
|
131
|
+
interface PatchBlock {
|
|
132
|
+
search: string;
|
|
133
|
+
replace: string;
|
|
134
|
+
}
|
|
104
135
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
136
|
+
/**
|
|
137
|
+
* ------------------------------------------------------------------
|
|
138
|
+
* HELPER: STRING SIMILARITY (Levenshtein based)
|
|
139
|
+
* Tính độ giống nhau giữa 2 chuỗi (0 -> 1)
|
|
140
|
+
* ------------------------------------------------------------------
|
|
141
|
+
*/
|
|
142
|
+
const calculateSimilarity = (s1: string, s2: string): number => {
|
|
143
|
+
const longer = s1.length > s2.length ? s1 : s2;
|
|
144
|
+
const shorter = s1.length > s2.length ? s2 : s1;
|
|
145
|
+
const longerLength = longer.length;
|
|
146
|
+
if (longerLength === 0) {
|
|
147
|
+
return 1.0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Sử dụng dmp để tính Levenshtein distance cho chính xác và nhanh
|
|
151
|
+
const dmp = new diff_match_patch();
|
|
152
|
+
const diffs = dmp.diff_main(longer, shorter);
|
|
153
|
+
const levenshtein = dmp.diff_levenshtein(diffs);
|
|
154
|
+
|
|
155
|
+
return (longerLength - levenshtein) / longerLength;
|
|
156
|
+
};
|
|
108
157
|
|
|
109
|
-
|
|
158
|
+
/**
|
|
159
|
+
* ------------------------------------------------------------------
|
|
160
|
+
* HELPER: FUZZY LINE MATCHING
|
|
161
|
+
* Tìm đoạn code khớp nhất trong file gốc dựa trên điểm số (Confidence)
|
|
162
|
+
* ------------------------------------------------------------------
|
|
163
|
+
*/
|
|
164
|
+
const applyFuzzyLineMatch = (
|
|
165
|
+
original: string,
|
|
166
|
+
search: string,
|
|
167
|
+
replace: string,
|
|
168
|
+
threshold: number = 0.85 // Ngưỡng chấp nhận (85% giống nhau)
|
|
169
|
+
): { result: string | null; confidence: number } => {
|
|
170
|
+
const originalLines = original.split(/\r?\n/);
|
|
171
|
+
const searchLines = search.split(/\r?\n/);
|
|
172
|
+
|
|
173
|
+
// Lọc bỏ dòng trống ở đầu/cuối search block để match chính xác hơn
|
|
174
|
+
while (searchLines.length > 0 && searchLines[0].trim() === "") searchLines.shift();
|
|
175
|
+
while (searchLines.length > 0 && searchLines[searchLines.length - 1].trim() === "") searchLines.pop();
|
|
176
|
+
|
|
177
|
+
if (searchLines.length === 0) return { result: null, confidence: 0 };
|
|
178
|
+
|
|
179
|
+
let bestMatchIndex = -1;
|
|
180
|
+
let bestMatchScore = 0;
|
|
181
|
+
|
|
182
|
+
// Duyệt qua file gốc (Sliding Window)
|
|
183
|
+
// Chỉ cần duyệt đến vị trí mà số dòng còn lại đủ để chứa searchLines
|
|
184
|
+
for (let i = 0; i <= originalLines.length - searchLines.length; i++) {
|
|
185
|
+
let currentScoreTotal = 0;
|
|
186
|
+
let possible = true;
|
|
110
187
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
188
|
+
for (let j = 0; j < searchLines.length; j++) {
|
|
189
|
+
const originalLine = originalLines[i + j].trim();
|
|
190
|
+
const searchLine = searchLines[j].trim();
|
|
191
|
+
|
|
192
|
+
// So sánh nhanh: Nếu giống hệt nhau
|
|
193
|
+
if (originalLine === searchLine) {
|
|
194
|
+
currentScoreTotal += 1;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// So sánh chậm: Tính độ tương đồng
|
|
199
|
+
const similarity = calculateSimilarity(originalLine, searchLine);
|
|
200
|
+
|
|
201
|
+
// Nếu một dòng quá khác biệt (< 60%), coi như block này không khớp
|
|
202
|
+
// (Giúp tối ưu hiệu năng, cắt nhánh sớm)
|
|
203
|
+
if (similarity < 0.6) {
|
|
204
|
+
possible = false;
|
|
205
|
+
break;
|
|
126
206
|
}
|
|
207
|
+
currentScoreTotal += similarity;
|
|
127
208
|
}
|
|
128
|
-
} catch (error) {
|
|
129
|
-
// Bỏ qua nếu lỗi permission hoặc path không tồn tại
|
|
130
|
-
}
|
|
131
|
-
return results;
|
|
132
|
-
};
|
|
133
209
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
210
|
+
if (possible) {
|
|
211
|
+
const avgScore = currentScoreTotal / searchLines.length;
|
|
212
|
+
if (avgScore > bestMatchScore) {
|
|
213
|
+
bestMatchScore = avgScore;
|
|
214
|
+
bestMatchIndex = i;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
137
218
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
replaceBlock: string
|
|
142
|
-
): string | null => {
|
|
143
|
-
const originalLines = originalContent.split(/\r?\n/);
|
|
144
|
-
const searchLines = searchBlock.split(/\r?\n/);
|
|
145
|
-
|
|
146
|
-
while (searchLines.length > 0 && searchLines[0].trim() === "") {
|
|
147
|
-
searchLines.shift();
|
|
219
|
+
// LOG CONFIDENCE ĐỂ DEBUG
|
|
220
|
+
if (bestMatchScore > 0) {
|
|
221
|
+
console.log(`[FuzzyMatch] Best match score: ${(bestMatchScore * 100).toFixed(2)}% at line ${bestMatchIndex + 1}`);
|
|
148
222
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
searchLines
|
|
223
|
+
|
|
224
|
+
// Nếu tìm thấy vị trí có độ tin cậy cao hơn ngưỡng
|
|
225
|
+
if (bestMatchIndex !== -1 && bestMatchScore >= threshold) {
|
|
226
|
+
const before = originalLines.slice(0, bestMatchIndex);
|
|
227
|
+
// Lưu ý: Đoạn replace thay thế đúng số lượng dòng của searchLines
|
|
228
|
+
// (Giả định search và match trong original có cùng số dòng hiển thị)
|
|
229
|
+
const after = originalLines.slice(bestMatchIndex + searchLines.length);
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
result: [...before, replace, ...after].join("\n"),
|
|
233
|
+
confidence: bestMatchScore
|
|
234
|
+
};
|
|
154
235
|
}
|
|
155
236
|
|
|
156
|
-
|
|
237
|
+
return { result: null, confidence: bestMatchScore };
|
|
238
|
+
};
|
|
157
239
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
240
|
+
/**
|
|
241
|
+
* ------------------------------------------------------------------
|
|
242
|
+
* 1. STATE MACHINE PARSER (FILE-AWARE)
|
|
243
|
+
* ------------------------------------------------------------------
|
|
244
|
+
*/
|
|
245
|
+
const parseLLMPatch = (patchContent: string): Record<string, PatchBlock[]> => {
|
|
246
|
+
const lines = patchContent.split(/\r?\n/);
|
|
247
|
+
const filePatches: Record<string, PatchBlock[]> = {};
|
|
248
|
+
|
|
249
|
+
let currentFile = "unknown";
|
|
250
|
+
let inSearch = false;
|
|
251
|
+
let inReplace = false;
|
|
252
|
+
let searchLines: string[] = [];
|
|
253
|
+
let replaceLines: string[] = [];
|
|
254
|
+
|
|
255
|
+
const isFileMarker = (line: string) => {
|
|
256
|
+
const match = line.match(/^### File:\s*[`']?([^`'\s]+)[`']?/i);
|
|
257
|
+
return match ? match[1] : null;
|
|
258
|
+
};
|
|
259
|
+
const isSearchMarker = (line: string) => /^\s*<{7,}\s*SEARCH\s*$/.test(line);
|
|
260
|
+
const isDividerMarker = (line: string) => /^\s*={7,}\s*$/.test(line);
|
|
261
|
+
const isEndMarker = (line: string) => /^\s*>{7,}\s*REPLACE\s*$/.test(line);
|
|
262
|
+
|
|
263
|
+
for (const line of lines) {
|
|
264
|
+
const fileName = isFileMarker(line);
|
|
265
|
+
if (fileName) {
|
|
266
|
+
currentFile = fileName;
|
|
267
|
+
continue;
|
|
165
268
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
269
|
+
|
|
270
|
+
if (isSearchMarker(line)) {
|
|
271
|
+
inSearch = true;
|
|
272
|
+
inReplace = false;
|
|
273
|
+
searchLines = [];
|
|
274
|
+
replaceLines = [];
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (isDividerMarker(line)) {
|
|
278
|
+
if (inSearch) { inSearch = false; inReplace = true; }
|
|
279
|
+
continue;
|
|
170
280
|
}
|
|
281
|
+
if (isEndMarker(line)) {
|
|
282
|
+
if (inReplace || inSearch) {
|
|
283
|
+
if (searchLines.length > 0 || replaceLines.length > 0) {
|
|
284
|
+
if (!filePatches[currentFile]) filePatches[currentFile] = [];
|
|
285
|
+
filePatches[currentFile].push({
|
|
286
|
+
search: searchLines.join("\n"),
|
|
287
|
+
replace: replaceLines.join("\n")
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
inSearch = false; inReplace = false; searchLines = []; replaceLines = [];
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (inSearch) searchLines.push(line);
|
|
295
|
+
else if (inReplace) replaceLines.push(line);
|
|
171
296
|
}
|
|
172
|
-
return
|
|
297
|
+
return filePatches;
|
|
173
298
|
};
|
|
174
299
|
|
|
175
|
-
|
|
300
|
+
/**
|
|
301
|
+
* ------------------------------------------------------------------
|
|
302
|
+
* 2. APPLY PATCH (UPDATED)
|
|
303
|
+
* ------------------------------------------------------------------
|
|
304
|
+
*/
|
|
305
|
+
export const applyPatch = (
|
|
306
|
+
originalContent: string,
|
|
307
|
+
patches: PatchBlock[]
|
|
308
|
+
): string => {
|
|
309
|
+
if (!patches || patches.length === 0) return originalContent;
|
|
310
|
+
|
|
176
311
|
let result = originalContent;
|
|
177
|
-
const patchRegex =
|
|
178
|
-
/<<<<<<< SEARCH\s*\n([\s\S]*?)\n?=======\s*\n([\s\S]*?)\n?>>>>>>> REPLACE/g;
|
|
179
|
-
const matches = [...patchContent.matchAll(patchRegex)];
|
|
180
312
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
313
|
+
for (const patch of patches) {
|
|
314
|
+
const { search, replace } = patch;
|
|
184
315
|
|
|
185
|
-
|
|
186
|
-
|
|
316
|
+
// NẾU SEARCH RỖNG (INSERT/APPEND)
|
|
317
|
+
if (search.trim() === "") {
|
|
318
|
+
console.log(`[ApplyPatch] Inserting/appending block.`);
|
|
319
|
+
result += `\n${replace}`;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
187
322
|
|
|
188
|
-
|
|
189
|
-
|
|
323
|
+
// --- STRATEGY 1: EXACT MATCH ---
|
|
324
|
+
if (result.includes(search)) {
|
|
325
|
+
console.log(`[ApplyPatch] Exact match found.`);
|
|
326
|
+
result = result.replace(search, replace);
|
|
190
327
|
continue;
|
|
191
328
|
}
|
|
192
329
|
|
|
193
|
-
|
|
194
|
-
const
|
|
330
|
+
// --- STRATEGY 2: NORMALIZED MATCH (Trim) ---
|
|
331
|
+
const trimmedSearch = search.trim();
|
|
195
332
|
if (result.includes(trimmedSearch)) {
|
|
196
|
-
|
|
333
|
+
console.log(`[ApplyPatch] Trimmed match found.`);
|
|
334
|
+
result = result.replace(trimmedSearch, replace.trim());
|
|
197
335
|
continue;
|
|
198
336
|
}
|
|
337
|
+
|
|
338
|
+
// --- STRATEGY 3: FUZZY LINE MATCH (BEST FOR CODE) ---
|
|
339
|
+
// Tìm vị trí tốt nhất dựa trên similarity score của từng dòng
|
|
340
|
+
const fuzzyMatch = applyFuzzyLineMatch(result, search, replace, 0.80); // 80% confidence
|
|
341
|
+
if (fuzzyMatch.result !== null) {
|
|
342
|
+
console.log(`[ApplyPatch] Fuzzy line match applied (Confidence: ${(fuzzyMatch.confidence * 100).toFixed(0)}%)`);
|
|
343
|
+
result = fuzzyMatch.result;
|
|
344
|
+
continue;
|
|
345
|
+
} else {
|
|
346
|
+
console.warn(`[ApplyPatch] Failed to match block. Max confidence was: ${(fuzzyMatch.confidence * 100).toFixed(0)}%`);
|
|
347
|
+
}
|
|
199
348
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
349
|
+
// --- STRATEGY 4: DMP FALLBACK (LAST RESORT) ---
|
|
350
|
+
// Nếu cả fuzzy line match cũng tạch (ví dụ do search block quá ngắn hoặc cấu trúc quá nát)
|
|
351
|
+
// thì mới dùng DMP patch_apply.
|
|
352
|
+
try {
|
|
353
|
+
const dmp = new diff_match_patch();
|
|
354
|
+
dmp.Match_Threshold = 0.5;
|
|
355
|
+
dmp.Match_Distance = 1000;
|
|
356
|
+
|
|
357
|
+
const dmpPatches = dmp.patch_make(search, replace);
|
|
358
|
+
const [newText, applyResults] = dmp.patch_apply(dmpPatches, result);
|
|
359
|
+
|
|
360
|
+
// Kiểm tra xem có apply được hết các hunk không
|
|
361
|
+
const successCount = applyResults.filter(Boolean).length;
|
|
362
|
+
const successRate = successCount / applyResults.length;
|
|
363
|
+
|
|
364
|
+
if (successRate === 1 && newText !== result) {
|
|
365
|
+
console.log(`[ApplyPatch] DMP Patch applied via fuzzy logic.`);
|
|
366
|
+
result = newText;
|
|
367
|
+
} else {
|
|
368
|
+
console.error(`[ApplyPatch] All strategies failed for block starting with: ${search.substring(0, 30)}...`);
|
|
369
|
+
// Tuỳ chọn: Throw error để báo cho LLM biết là apply thất bại
|
|
370
|
+
// throw new Error("Could not apply patch block");
|
|
371
|
+
}
|
|
372
|
+
} catch (e) {
|
|
373
|
+
console.error(`[ApplyPatch] DMP error:`, e);
|
|
204
374
|
}
|
|
205
375
|
}
|
|
376
|
+
|
|
206
377
|
return result;
|
|
207
378
|
};
|
|
208
379
|
|
|
380
|
+
|
|
209
381
|
// ==========================================
|
|
210
382
|
// 3. WEB UI SERVER (UPDATED UI)
|
|
211
383
|
// ==========================================
|
|
@@ -238,18 +410,50 @@ const startServer = async (rootDir: string) => {
|
|
|
238
410
|
|
|
239
411
|
// API 3: Apply Patch
|
|
240
412
|
app.post("/api/apply", async (req, res) => {
|
|
241
|
-
const { llmResponse, filePaths } = req.body;
|
|
413
|
+
const { llmResponse, filePaths: selectedPaths } = req.body;
|
|
414
|
+
|
|
415
|
+
// 1. Parse all patches from LLM response
|
|
416
|
+
const filePatches = parseLLMPatch(llmResponse);
|
|
417
|
+
|
|
418
|
+
// 2. Scan project files
|
|
242
419
|
const allFiles = await scanRecursively(rootDir, rootDir);
|
|
243
|
-
const selectedFiles = allFiles.filter((f) => filePaths.includes(f.path));
|
|
244
|
-
|
|
245
420
|
const modifiedFiles: string[] = [];
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
421
|
+
|
|
422
|
+
// 3. Match patches to files and apply
|
|
423
|
+
for (const file of allFiles) {
|
|
424
|
+
// Find patches meant for this specific file path
|
|
425
|
+
// Note: LLM might use different path separators, so we normalize
|
|
426
|
+
const normalizedPath = file.path.replace(/\\/g, '/');
|
|
427
|
+
const patchesForFile = Object.entries(filePatches).find(([path]) => {
|
|
428
|
+
const normalizedKey = path.replace(/\\/g, '/');
|
|
429
|
+
return normalizedKey === normalizedPath || normalizedPath.endsWith('/' + normalizedKey);
|
|
430
|
+
})?.[1];
|
|
431
|
+
|
|
432
|
+
// Only apply if the file was selected in UI AND has patches in the response
|
|
433
|
+
if (selectedPaths.includes(file.path) && patchesForFile) {
|
|
434
|
+
console.log(`[Apply] Processing ${patchesForFile.length} blocks for: ${file.path}`);
|
|
435
|
+
const newContent = applyPatch(file.content, patchesForFile);
|
|
436
|
+
|
|
437
|
+
if (newContent !== file.content) {
|
|
438
|
+
await fs.writeFile(file.fullPath, newContent, "utf-8");
|
|
439
|
+
modifiedFiles.push(file.path);
|
|
440
|
+
}
|
|
251
441
|
}
|
|
252
442
|
}
|
|
443
|
+
|
|
444
|
+
// Fallback: If no matched files but only one file was selected, try applying "unknown" patches
|
|
445
|
+
if (modifiedFiles.length === 0 && selectedPaths.length === 1 && filePatches["unknown"]) {
|
|
446
|
+
const file = allFiles.find(f => f.path === selectedPaths[0]);
|
|
447
|
+
if (file) {
|
|
448
|
+
console.log(`[Apply] Fallback: Applying unknown blocks to single selected file: ${file.path}`);
|
|
449
|
+
const newContent = applyPatch(file.content, filePatches["unknown"]);
|
|
450
|
+
if (newContent !== file.content) {
|
|
451
|
+
await fs.writeFile(file.fullPath, newContent, "utf-8");
|
|
452
|
+
modifiedFiles.push(file.path);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
253
457
|
res.json({ modified: modifiedFiles });
|
|
254
458
|
});
|
|
255
459
|
|
|
@@ -376,7 +580,8 @@ const startServer = async (rootDir: string) => {
|
|
|
376
580
|
|
|
377
581
|
<div class="flex-1 flex flex-col bg-gray-850 min-w-0">
|
|
378
582
|
|
|
379
|
-
|
|
583
|
+
<!-- 1. Instruction (Takes more space and resizable) -->
|
|
584
|
+
<div class="flex-1 p-6 flex flex-col min-h-0 border-b border-apple-border">
|
|
380
585
|
<div class="flex justify-between items-end mb-3">
|
|
381
586
|
<div>
|
|
382
587
|
<h2 class="text-sm font-semibold text-gray-200">1. Instruction</h2>
|
|
@@ -392,12 +597,12 @@ const startServer = async (rootDir: string) => {
|
|
|
392
597
|
</button>
|
|
393
598
|
</div>
|
|
394
599
|
<textarea x-model="instruction"
|
|
395
|
-
class="w-full bg-apple-panel border border-apple-border rounded p-3 text-sm text-gray-200 placeholder-gray-600 focus:outline-none focus:border-apple-blue/50 focus:ring-1 focus:ring-apple-blue/50 transition resize-
|
|
396
|
-
rows="3"
|
|
600
|
+
class="w-full flex-1 bg-apple-panel border border-apple-border rounded p-3 text-sm text-gray-200 placeholder-gray-600 focus:outline-none focus:border-apple-blue/50 focus:ring-1 focus:ring-apple-blue/50 transition resize-y font-mono min-h-[120px]"
|
|
397
601
|
placeholder="e.g. Refactor the Button component to use TypeScript interfaces..."></textarea>
|
|
398
602
|
</div>
|
|
399
603
|
|
|
400
|
-
|
|
604
|
+
<!-- 2. LLM Patch (Fixed smaller size) -->
|
|
605
|
+
<div class="h-64 p-6 flex flex-col flex-shrink-0">
|
|
401
606
|
<div class="flex justify-between items-end mb-3">
|
|
402
607
|
<div>
|
|
403
608
|
<h2 class="text-sm font-semibold text-gray-200">2. LLM Patch</h2>
|
|
@@ -410,11 +615,9 @@ const startServer = async (rootDir: string) => {
|
|
|
410
615
|
<span x-show="applying">Applying...</span>
|
|
411
616
|
</button>
|
|
412
617
|
</div>
|
|
413
|
-
<
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
placeholder="<<<<<<< SEARCH..."></textarea>
|
|
417
|
-
</div>
|
|
618
|
+
<textarea x-model="llmResponse"
|
|
619
|
+
class="w-full h-40 bg-apple-panel border border-apple-border rounded p-3 text-sm text-gray-300 font-mono placeholder-gray-600 focus:outline-none focus:border-emerald-500/50 focus:ring-1 focus:ring-emerald-500/50 transition resize-none leading-relaxed"
|
|
620
|
+
placeholder="<<<<<<< SEARCH..."></textarea>
|
|
418
621
|
</div>
|
|
419
622
|
|
|
420
623
|
</div>
|