@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/dist/tools/vibe/index.js
CHANGED
|
@@ -3,16 +3,48 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.vibe = void 0;
|
|
6
|
+
exports.vibe = exports.applyPatch = void 0;
|
|
7
7
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
8
8
|
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
9
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
-
const
|
|
11
|
-
const node_util_1 = __importDefault(require("node:util"));
|
|
10
|
+
const diff_match_patch_1 = require("diff-match-patch");
|
|
12
11
|
const express_1 = __importDefault(require("express"));
|
|
13
12
|
const open_1 = __importDefault(require("open"));
|
|
14
|
-
const execAsync = node_util_1.default.promisify(node_child_process_1.exec);
|
|
15
13
|
const node_net_1 = __importDefault(require("node:net"));
|
|
14
|
+
const scanRecursively = async (dir, baseDir) => {
|
|
15
|
+
let results = [];
|
|
16
|
+
try {
|
|
17
|
+
const list = await promises_1.default.readdir(dir, { withFileTypes: true });
|
|
18
|
+
for (const entry of list) {
|
|
19
|
+
const fullPath = node_path_1.default.join(dir, entry.name);
|
|
20
|
+
const relativePath = node_path_1.default.relative(baseDir, fullPath);
|
|
21
|
+
if (shouldIgnorePath(relativePath))
|
|
22
|
+
continue;
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
const subResults = await scanRecursively(fullPath, baseDir);
|
|
25
|
+
results = results.concat(subResults);
|
|
26
|
+
}
|
|
27
|
+
else if (entry.isFile()) {
|
|
28
|
+
try {
|
|
29
|
+
const content = await promises_1.default.readFile(fullPath, "utf-8");
|
|
30
|
+
results.push({
|
|
31
|
+
path: relativePath,
|
|
32
|
+
fullPath: fullPath,
|
|
33
|
+
content: content,
|
|
34
|
+
extension: entry.name.split(".").pop() || "txt",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
// Bỏ qua file nhị phân hoặc không đọc được
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
// Bỏ qua nếu lỗi permission hoặc path không tồn tại
|
|
45
|
+
}
|
|
46
|
+
return results;
|
|
47
|
+
};
|
|
16
48
|
// random port from 3000-3999
|
|
17
49
|
// Check if port is available would be better in real-world usage
|
|
18
50
|
const isPortInUse = (port) => {
|
|
@@ -81,98 +113,226 @@ const shouldIgnorePath = (filePath) => {
|
|
|
81
113
|
}
|
|
82
114
|
return false;
|
|
83
115
|
};
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
else if (entry.isFile()) {
|
|
98
|
-
try {
|
|
99
|
-
const content = await promises_1.default.readFile(fullPath, "utf-8");
|
|
100
|
-
results.push({
|
|
101
|
-
path: relativePath,
|
|
102
|
-
fullPath: fullPath,
|
|
103
|
-
content: content,
|
|
104
|
-
extension: entry.name.split(".").pop() || "txt",
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
catch (err) {
|
|
108
|
-
// Bỏ qua file nhị phân hoặc không đọc được
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
catch (error) {
|
|
114
|
-
// Bỏ qua nếu lỗi permission hoặc path không tồn tại
|
|
116
|
+
/**
|
|
117
|
+
* ------------------------------------------------------------------
|
|
118
|
+
* HELPER: STRING SIMILARITY (Levenshtein based)
|
|
119
|
+
* Tính độ giống nhau giữa 2 chuỗi (0 -> 1)
|
|
120
|
+
* ------------------------------------------------------------------
|
|
121
|
+
*/
|
|
122
|
+
const calculateSimilarity = (s1, s2) => {
|
|
123
|
+
const longer = s1.length > s2.length ? s1 : s2;
|
|
124
|
+
const shorter = s1.length > s2.length ? s2 : s1;
|
|
125
|
+
const longerLength = longer.length;
|
|
126
|
+
if (longerLength === 0) {
|
|
127
|
+
return 1.0;
|
|
115
128
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
129
|
+
// Sử dụng dmp để tính Levenshtein distance cho chính xác và nhanh
|
|
130
|
+
const dmp = new diff_match_patch_1.diff_match_patch();
|
|
131
|
+
const diffs = dmp.diff_main(longer, shorter);
|
|
132
|
+
const levenshtein = dmp.diff_levenshtein(diffs);
|
|
133
|
+
return (longerLength - levenshtein) / longerLength;
|
|
120
134
|
};
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
135
|
+
/**
|
|
136
|
+
* ------------------------------------------------------------------
|
|
137
|
+
* HELPER: FUZZY LINE MATCHING
|
|
138
|
+
* Tìm đoạn code khớp nhất trong file gốc dựa trên điểm số (Confidence)
|
|
139
|
+
* ------------------------------------------------------------------
|
|
140
|
+
*/
|
|
141
|
+
const applyFuzzyLineMatch = (original, search, replace, threshold = 0.85 // Ngưỡng chấp nhận (85% giống nhau)
|
|
142
|
+
) => {
|
|
143
|
+
const originalLines = original.split(/\r?\n/);
|
|
144
|
+
const searchLines = search.split(/\r?\n/);
|
|
145
|
+
// Lọc bỏ dòng trống ở đầu/cuối search block để match chính xác hơn
|
|
146
|
+
while (searchLines.length > 0 && searchLines[0].trim() === "")
|
|
125
147
|
searchLines.shift();
|
|
126
|
-
|
|
127
|
-
while (searchLines.length > 0 &&
|
|
128
|
-
searchLines[searchLines.length - 1].trim() === "") {
|
|
148
|
+
while (searchLines.length > 0 && searchLines[searchLines.length - 1].trim() === "")
|
|
129
149
|
searchLines.pop();
|
|
130
|
-
}
|
|
131
150
|
if (searchLines.length === 0)
|
|
132
|
-
return null;
|
|
151
|
+
return { result: null, confidence: 0 };
|
|
152
|
+
let bestMatchIndex = -1;
|
|
153
|
+
let bestMatchScore = 0;
|
|
154
|
+
// Duyệt qua file gốc (Sliding Window)
|
|
155
|
+
// Chỉ cần duyệt đến vị trí mà số dòng còn lại đủ để chứa searchLines
|
|
133
156
|
for (let i = 0; i <= originalLines.length - searchLines.length; i++) {
|
|
134
|
-
let
|
|
157
|
+
let currentScoreTotal = 0;
|
|
158
|
+
let possible = true;
|
|
135
159
|
for (let j = 0; j < searchLines.length; j++) {
|
|
136
|
-
|
|
137
|
-
|
|
160
|
+
const originalLine = originalLines[i + j].trim();
|
|
161
|
+
const searchLine = searchLines[j].trim();
|
|
162
|
+
// So sánh nhanh: Nếu giống hệt nhau
|
|
163
|
+
if (originalLine === searchLine) {
|
|
164
|
+
currentScoreTotal += 1;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
// So sánh chậm: Tính độ tương đồng
|
|
168
|
+
const similarity = calculateSimilarity(originalLine, searchLine);
|
|
169
|
+
// Nếu một dòng quá khác biệt (< 60%), coi như block này không khớp
|
|
170
|
+
// (Giúp tối ưu hiệu năng, cắt nhánh sớm)
|
|
171
|
+
if (similarity < 0.6) {
|
|
172
|
+
possible = false;
|
|
138
173
|
break;
|
|
139
174
|
}
|
|
175
|
+
currentScoreTotal += similarity;
|
|
140
176
|
}
|
|
141
|
-
if (
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
177
|
+
if (possible) {
|
|
178
|
+
const avgScore = currentScoreTotal / searchLines.length;
|
|
179
|
+
if (avgScore > bestMatchScore) {
|
|
180
|
+
bestMatchScore = avgScore;
|
|
181
|
+
bestMatchIndex = i;
|
|
182
|
+
}
|
|
145
183
|
}
|
|
146
184
|
}
|
|
147
|
-
|
|
185
|
+
// LOG CONFIDENCE ĐỂ DEBUG
|
|
186
|
+
if (bestMatchScore > 0) {
|
|
187
|
+
console.log(`[FuzzyMatch] Best match score: ${(bestMatchScore * 100).toFixed(2)}% at line ${bestMatchIndex + 1}`);
|
|
188
|
+
}
|
|
189
|
+
// Nếu tìm thấy vị trí có độ tin cậy cao hơn ngưỡng
|
|
190
|
+
if (bestMatchIndex !== -1 && bestMatchScore >= threshold) {
|
|
191
|
+
const before = originalLines.slice(0, bestMatchIndex);
|
|
192
|
+
// Lưu ý: Đoạn replace thay thế đúng số lượng dòng của searchLines
|
|
193
|
+
// (Giả định search và match trong original có cùng số dòng hiển thị)
|
|
194
|
+
const after = originalLines.slice(bestMatchIndex + searchLines.length);
|
|
195
|
+
return {
|
|
196
|
+
result: [...before, replace, ...after].join("\n"),
|
|
197
|
+
confidence: bestMatchScore
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return { result: null, confidence: bestMatchScore };
|
|
148
201
|
};
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
202
|
+
/**
|
|
203
|
+
* ------------------------------------------------------------------
|
|
204
|
+
* 1. STATE MACHINE PARSER (FILE-AWARE)
|
|
205
|
+
* ------------------------------------------------------------------
|
|
206
|
+
*/
|
|
207
|
+
const parseLLMPatch = (patchContent) => {
|
|
208
|
+
const lines = patchContent.split(/\r?\n/);
|
|
209
|
+
const filePatches = {};
|
|
210
|
+
let currentFile = "unknown";
|
|
211
|
+
let inSearch = false;
|
|
212
|
+
let inReplace = false;
|
|
213
|
+
let searchLines = [];
|
|
214
|
+
let replaceLines = [];
|
|
215
|
+
const isFileMarker = (line) => {
|
|
216
|
+
const match = line.match(/^### File:\s*[`']?([^`'\s]+)[`']?/i);
|
|
217
|
+
return match ? match[1] : null;
|
|
218
|
+
};
|
|
219
|
+
const isSearchMarker = (line) => /^\s*<{7,}\s*SEARCH\s*$/.test(line);
|
|
220
|
+
const isDividerMarker = (line) => /^\s*={7,}\s*$/.test(line);
|
|
221
|
+
const isEndMarker = (line) => /^\s*>{7,}\s*REPLACE\s*$/.test(line);
|
|
222
|
+
for (const line of lines) {
|
|
223
|
+
const fileName = isFileMarker(line);
|
|
224
|
+
if (fileName) {
|
|
225
|
+
currentFile = fileName;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (isSearchMarker(line)) {
|
|
229
|
+
inSearch = true;
|
|
230
|
+
inReplace = false;
|
|
231
|
+
searchLines = [];
|
|
232
|
+
replaceLines = [];
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (isDividerMarker(line)) {
|
|
236
|
+
if (inSearch) {
|
|
237
|
+
inSearch = false;
|
|
238
|
+
inReplace = true;
|
|
239
|
+
}
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (isEndMarker(line)) {
|
|
243
|
+
if (inReplace || inSearch) {
|
|
244
|
+
if (searchLines.length > 0 || replaceLines.length > 0) {
|
|
245
|
+
if (!filePatches[currentFile])
|
|
246
|
+
filePatches[currentFile] = [];
|
|
247
|
+
filePatches[currentFile].push({
|
|
248
|
+
search: searchLines.join("\n"),
|
|
249
|
+
replace: replaceLines.join("\n")
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
inSearch = false;
|
|
254
|
+
inReplace = false;
|
|
255
|
+
searchLines = [];
|
|
256
|
+
replaceLines = [];
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (inSearch)
|
|
260
|
+
searchLines.push(line);
|
|
261
|
+
else if (inReplace)
|
|
262
|
+
replaceLines.push(line);
|
|
155
263
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
264
|
+
return filePatches;
|
|
265
|
+
};
|
|
266
|
+
/**
|
|
267
|
+
* ------------------------------------------------------------------
|
|
268
|
+
* 2. APPLY PATCH (UPDATED)
|
|
269
|
+
* ------------------------------------------------------------------
|
|
270
|
+
*/
|
|
271
|
+
const applyPatch = (originalContent, patches) => {
|
|
272
|
+
if (!patches || patches.length === 0)
|
|
273
|
+
return originalContent;
|
|
274
|
+
let result = originalContent;
|
|
275
|
+
for (const patch of patches) {
|
|
276
|
+
const { search, replace } = patch;
|
|
277
|
+
// NẾU SEARCH RỖNG (INSERT/APPEND)
|
|
278
|
+
if (search.trim() === "") {
|
|
279
|
+
console.log(`[ApplyPatch] Inserting/appending block.`);
|
|
280
|
+
result += `\n${replace}`;
|
|
160
281
|
continue;
|
|
161
282
|
}
|
|
162
|
-
|
|
163
|
-
|
|
283
|
+
// --- STRATEGY 1: EXACT MATCH ---
|
|
284
|
+
if (result.includes(search)) {
|
|
285
|
+
console.log(`[ApplyPatch] Exact match found.`);
|
|
286
|
+
result = result.replace(search, replace);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
// --- STRATEGY 2: NORMALIZED MATCH (Trim) ---
|
|
290
|
+
const trimmedSearch = search.trim();
|
|
164
291
|
if (result.includes(trimmedSearch)) {
|
|
165
|
-
|
|
292
|
+
console.log(`[ApplyPatch] Trimmed match found.`);
|
|
293
|
+
result = result.replace(trimmedSearch, replace.trim());
|
|
166
294
|
continue;
|
|
167
295
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
296
|
+
// --- STRATEGY 3: FUZZY LINE MATCH (BEST FOR CODE) ---
|
|
297
|
+
// Tìm vị trí tốt nhất dựa trên similarity score của từng dòng
|
|
298
|
+
const fuzzyMatch = applyFuzzyLineMatch(result, search, replace, 0.80); // 80% confidence
|
|
299
|
+
if (fuzzyMatch.result !== null) {
|
|
300
|
+
console.log(`[ApplyPatch] Fuzzy line match applied (Confidence: ${(fuzzyMatch.confidence * 100).toFixed(0)}%)`);
|
|
301
|
+
result = fuzzyMatch.result;
|
|
171
302
|
continue;
|
|
172
303
|
}
|
|
304
|
+
else {
|
|
305
|
+
console.warn(`[ApplyPatch] Failed to match block. Max confidence was: ${(fuzzyMatch.confidence * 100).toFixed(0)}%`);
|
|
306
|
+
}
|
|
307
|
+
// --- STRATEGY 4: DMP FALLBACK (LAST RESORT) ---
|
|
308
|
+
// 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)
|
|
309
|
+
// thì mới dùng DMP patch_apply.
|
|
310
|
+
try {
|
|
311
|
+
const dmp = new diff_match_patch_1.diff_match_patch();
|
|
312
|
+
dmp.Match_Threshold = 0.5;
|
|
313
|
+
dmp.Match_Distance = 1000;
|
|
314
|
+
const dmpPatches = dmp.patch_make(search, replace);
|
|
315
|
+
const [newText, applyResults] = dmp.patch_apply(dmpPatches, result);
|
|
316
|
+
// Kiểm tra xem có apply được hết các hunk không
|
|
317
|
+
const successCount = applyResults.filter(Boolean).length;
|
|
318
|
+
const successRate = successCount / applyResults.length;
|
|
319
|
+
if (successRate === 1 && newText !== result) {
|
|
320
|
+
console.log(`[ApplyPatch] DMP Patch applied via fuzzy logic.`);
|
|
321
|
+
result = newText;
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
console.error(`[ApplyPatch] All strategies failed for block starting with: ${search.substring(0, 30)}...`);
|
|
325
|
+
// Tuỳ chọn: Throw error để báo cho LLM biết là apply thất bại
|
|
326
|
+
// throw new Error("Could not apply patch block");
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
catch (e) {
|
|
330
|
+
console.error(`[ApplyPatch] DMP error:`, e);
|
|
331
|
+
}
|
|
173
332
|
}
|
|
174
333
|
return result;
|
|
175
334
|
};
|
|
335
|
+
exports.applyPatch = applyPatch;
|
|
176
336
|
// ==========================================
|
|
177
337
|
// 3. WEB UI SERVER (UPDATED UI)
|
|
178
338
|
// ==========================================
|
|
@@ -199,15 +359,42 @@ const startServer = async (rootDir) => {
|
|
|
199
359
|
});
|
|
200
360
|
// API 3: Apply Patch
|
|
201
361
|
app.post("/api/apply", async (req, res) => {
|
|
202
|
-
|
|
362
|
+
var _a;
|
|
363
|
+
const { llmResponse, filePaths: selectedPaths } = req.body;
|
|
364
|
+
// 1. Parse all patches from LLM response
|
|
365
|
+
const filePatches = parseLLMPatch(llmResponse);
|
|
366
|
+
// 2. Scan project files
|
|
203
367
|
const allFiles = await scanRecursively(rootDir, rootDir);
|
|
204
|
-
const selectedFiles = allFiles.filter((f) => filePaths.includes(f.path));
|
|
205
368
|
const modifiedFiles = [];
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
369
|
+
// 3. Match patches to files and apply
|
|
370
|
+
for (const file of allFiles) {
|
|
371
|
+
// Find patches meant for this specific file path
|
|
372
|
+
// Note: LLM might use different path separators, so we normalize
|
|
373
|
+
const normalizedPath = file.path.replace(/\\/g, '/');
|
|
374
|
+
const patchesForFile = (_a = Object.entries(filePatches).find(([path]) => {
|
|
375
|
+
const normalizedKey = path.replace(/\\/g, '/');
|
|
376
|
+
return normalizedKey === normalizedPath || normalizedPath.endsWith('/' + normalizedKey);
|
|
377
|
+
})) === null || _a === void 0 ? void 0 : _a[1];
|
|
378
|
+
// Only apply if the file was selected in UI AND has patches in the response
|
|
379
|
+
if (selectedPaths.includes(file.path) && patchesForFile) {
|
|
380
|
+
console.log(`[Apply] Processing ${patchesForFile.length} blocks for: ${file.path}`);
|
|
381
|
+
const newContent = (0, exports.applyPatch)(file.content, patchesForFile);
|
|
382
|
+
if (newContent !== file.content) {
|
|
383
|
+
await promises_1.default.writeFile(file.fullPath, newContent, "utf-8");
|
|
384
|
+
modifiedFiles.push(file.path);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// Fallback: If no matched files but only one file was selected, try applying "unknown" patches
|
|
389
|
+
if (modifiedFiles.length === 0 && selectedPaths.length === 1 && filePatches["unknown"]) {
|
|
390
|
+
const file = allFiles.find(f => f.path === selectedPaths[0]);
|
|
391
|
+
if (file) {
|
|
392
|
+
console.log(`[Apply] Fallback: Applying unknown blocks to single selected file: ${file.path}`);
|
|
393
|
+
const newContent = (0, exports.applyPatch)(file.content, filePatches["unknown"]);
|
|
394
|
+
if (newContent !== file.content) {
|
|
395
|
+
await promises_1.default.writeFile(file.fullPath, newContent, "utf-8");
|
|
396
|
+
modifiedFiles.push(file.path);
|
|
397
|
+
}
|
|
211
398
|
}
|
|
212
399
|
}
|
|
213
400
|
res.json({ modified: modifiedFiles });
|
|
@@ -335,7 +522,8 @@ const startServer = async (rootDir) => {
|
|
|
335
522
|
|
|
336
523
|
<div class="flex-1 flex flex-col bg-gray-850 min-w-0">
|
|
337
524
|
|
|
338
|
-
|
|
525
|
+
<!-- 1. Instruction (Takes more space and resizable) -->
|
|
526
|
+
<div class="flex-1 p-6 flex flex-col min-h-0 border-b border-apple-border">
|
|
339
527
|
<div class="flex justify-between items-end mb-3">
|
|
340
528
|
<div>
|
|
341
529
|
<h2 class="text-sm font-semibold text-gray-200">1. Instruction</h2>
|
|
@@ -351,12 +539,12 @@ const startServer = async (rootDir) => {
|
|
|
351
539
|
</button>
|
|
352
540
|
</div>
|
|
353
541
|
<textarea x-model="instruction"
|
|
354
|
-
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-
|
|
355
|
-
rows="3"
|
|
542
|
+
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]"
|
|
356
543
|
placeholder="e.g. Refactor the Button component to use TypeScript interfaces..."></textarea>
|
|
357
544
|
</div>
|
|
358
545
|
|
|
359
|
-
|
|
546
|
+
<!-- 2. LLM Patch (Fixed smaller size) -->
|
|
547
|
+
<div class="h-64 p-6 flex flex-col flex-shrink-0">
|
|
360
548
|
<div class="flex justify-between items-end mb-3">
|
|
361
549
|
<div>
|
|
362
550
|
<h2 class="text-sm font-semibold text-gray-200">2. LLM Patch</h2>
|
|
@@ -369,11 +557,9 @@ const startServer = async (rootDir) => {
|
|
|
369
557
|
<span x-show="applying">Applying...</span>
|
|
370
558
|
</button>
|
|
371
559
|
</div>
|
|
372
|
-
<
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
placeholder="<<<<<<< SEARCH..."></textarea>
|
|
376
|
-
</div>
|
|
560
|
+
<textarea x-model="llmResponse"
|
|
561
|
+
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"
|
|
562
|
+
placeholder="<<<<<<< SEARCH..."></textarea>
|
|
377
563
|
</div>
|
|
378
564
|
|
|
379
565
|
</div>
|
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hapico/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.29",
|
|
4
4
|
"description": "A simple CLI tool for project management",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"@babel/standalone": "^7.28.2",
|
|
24
24
|
"axios": "^1.11.0",
|
|
25
25
|
"commander": "^14.0.0",
|
|
26
|
+
"diff-match-patch": "^1.0.5",
|
|
26
27
|
"express": "^5.2.1",
|
|
27
28
|
"inquirer": "^12.9.6",
|
|
28
29
|
"lodash": "^4.17.21",
|
|
@@ -38,6 +39,7 @@
|
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"@types/babel__standalone": "^7.1.9",
|
|
40
41
|
"@types/commander": "^2.12.5",
|
|
42
|
+
"@types/diff-match-patch": "^1.0.36",
|
|
41
43
|
"@types/express": "^5.0.6",
|
|
42
44
|
"@types/lodash": "^4.17.20",
|
|
43
45
|
"@types/node": "^24.1.0",
|