@hapico/cli 0.0.29 → 0.0.31
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 +382 -490
- package/bin/tools/nativewind/index.js +80 -0
- package/bin/tools/vibe/index.js +88 -108
- package/bin/tools/vibe/patch.utils.js +243 -0
- package/bun.lock +874 -0
- package/dist/index.js +382 -490
- package/dist/tools/nativewind/index.js +80 -0
- package/dist/tools/vibe/index.js +88 -108
- package/dist/tools/vibe/patch.utils.js +243 -0
- package/index.ts +475 -787
- package/package.json +5 -1
- package/tools/nativewind/index.ts +87 -0
- package/tools/vibe/index.ts +91 -108
- package/tools/vibe/patch.utils.ts +273 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hapico/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.31",
|
|
4
4
|
"description": "A simple CLI tool for project management",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -27,11 +27,15 @@
|
|
|
27
27
|
"express": "^5.2.1",
|
|
28
28
|
"inquirer": "^12.9.6",
|
|
29
29
|
"lodash": "^4.17.21",
|
|
30
|
+
"nativewind": "^4.2.2",
|
|
30
31
|
"open": "^10.2.0",
|
|
31
32
|
"ora": "^8.2.0",
|
|
32
33
|
"pako": "^2.1.0",
|
|
34
|
+
"postcss": "^8.5.8",
|
|
33
35
|
"prettier": "^3.6.2",
|
|
34
36
|
"qrcode-terminal": "^0.12.0",
|
|
37
|
+
"react-native-css-interop": "^0.2.2",
|
|
38
|
+
"tailwindcss": "3.4.17",
|
|
35
39
|
"unzipper": "^0.12.3",
|
|
36
40
|
"uuid": "^13.0.0",
|
|
37
41
|
"ws": "^8.18.3"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import postcss from "postcss";
|
|
2
|
+
import tailwindcss, { type Config } from "tailwindcss";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
// @ts-ignore
|
|
6
|
+
import nativewindPreset from "nativewind/preset";
|
|
7
|
+
|
|
8
|
+
// ==========================================
|
|
9
|
+
// FIX LỖI IMPORT REACT-NATIVE-CSS-INTEROP
|
|
10
|
+
// ==========================================
|
|
11
|
+
let compileToNative: (css: string, options?: any) => any;
|
|
12
|
+
try {
|
|
13
|
+
const compiler = require("react-native-css-interop/dist/compiler");
|
|
14
|
+
compileToNative = compiler.compile || compiler.default;
|
|
15
|
+
} catch (e) {
|
|
16
|
+
try {
|
|
17
|
+
const cssToRn = require("react-native-css-interop/dist/css-to-rn");
|
|
18
|
+
compileToNative = cssToRn.cssToReactNativeRuntime || cssToRn.default;
|
|
19
|
+
} catch (err) {
|
|
20
|
+
throw new Error("Không thể tìm thấy hàm compile. Kiểm tra lại node_modules.");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Hàm biên dịch Tailwind Class sang Compiled Object
|
|
26
|
+
*/
|
|
27
|
+
export async function generateNativeWindStyleMap(
|
|
28
|
+
contentPaths: string[],
|
|
29
|
+
rawCss: string = "@tailwind base;\n@tailwind components;\n@tailwind utilities;"
|
|
30
|
+
) {
|
|
31
|
+
try {
|
|
32
|
+
const tailwindConfig: Config = {
|
|
33
|
+
content: contentPaths,
|
|
34
|
+
presets: [nativewindPreset],
|
|
35
|
+
theme: { extend: {} },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const processor = postcss([tailwindcss(tailwindConfig)]);
|
|
39
|
+
const result = await processor.process(rawCss, {
|
|
40
|
+
from: "tailwind.css",
|
|
41
|
+
to: "output.css",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return compileToNative(result.css, { native: true });
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error("❌ Compile Style Map Error:", error);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Hàm hỗ trợ: Quét 1 thư mục và Lưu kết quả vào 1 thư mục khác
|
|
53
|
+
*/
|
|
54
|
+
export async function compileAndSaveToFolder(
|
|
55
|
+
folderToScan: string, // Thư mục chứa code cần quét (VD: "./src/components")
|
|
56
|
+
outputFilePath: string // Nơi lưu kết quả (VD: "./dist/styles.json")
|
|
57
|
+
) {
|
|
58
|
+
try {
|
|
59
|
+
// 1. Tạo đường dẫn quét toàn bộ file js, jsx, ts, tsx trong thư mục
|
|
60
|
+
// path.resolve(process.cwd(), ...) giúp lấy đúng đường dẫn gốc của project
|
|
61
|
+
const scanPattern = path.resolve(process.cwd(), folderToScan, "**/*.{js,jsx,ts,tsx}");
|
|
62
|
+
|
|
63
|
+
// console.log(`🔍 Scanning files in ${scanPattern}`);
|
|
64
|
+
|
|
65
|
+
// 2. Chạy hàm biên dịch
|
|
66
|
+
const compiledObject = await generateNativeWindStyleMap([scanPattern]);
|
|
67
|
+
|
|
68
|
+
// 3. Xử lý thư mục lưu file
|
|
69
|
+
const absoluteOutputPath = path.resolve(process.cwd(), outputFilePath);
|
|
70
|
+
const outputDir = path.dirname(absoluteOutputPath); // Lấy tên thư mục chứa file đầu ra
|
|
71
|
+
|
|
72
|
+
// Nếu thư mục đầu ra chưa tồn tại, Node.js sẽ tự động tạo thư mục đó (recursive: true)
|
|
73
|
+
if (!fs.existsSync(outputDir)) {
|
|
74
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 4. Ghi Object ra file JSON
|
|
78
|
+
fs.writeFileSync(absoluteOutputPath, JSON.stringify(compiledObject, null, 2), "utf-8");
|
|
79
|
+
|
|
80
|
+
// console.log(`✅ Sync CSS ${absoluteOutputPath}`);
|
|
81
|
+
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error("❌ Lỗi khi xuất file:", error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
package/tools/vibe/index.ts
CHANGED
|
@@ -16,7 +16,8 @@ const scanRecursively = async (
|
|
|
16
16
|
|
|
17
17
|
for (const entry of list) {
|
|
18
18
|
const fullPath = path.join(dir, entry.name);
|
|
19
|
-
|
|
19
|
+
// Chuẩn hóa đường dẫn về forward slash để nhất quán giữa các hệ điều hành
|
|
20
|
+
const relativePath = path.relative(baseDir, fullPath).split(path.sep).join('/');
|
|
20
21
|
|
|
21
22
|
if (shouldIgnorePath(relativePath)) continue;
|
|
22
23
|
|
|
@@ -252,6 +253,11 @@ const parseLLMPatch = (patchContent: string): Record<string, PatchBlock[]> => {
|
|
|
252
253
|
let searchLines: string[] = [];
|
|
253
254
|
let replaceLines: string[] = [];
|
|
254
255
|
|
|
256
|
+
// State for full file replacement detection
|
|
257
|
+
let inCodeBlock = false;
|
|
258
|
+
let codeBlockLines: string[] = [];
|
|
259
|
+
let hasSeenMarkersInBlock = false;
|
|
260
|
+
|
|
255
261
|
const isFileMarker = (line: string) => {
|
|
256
262
|
const match = line.match(/^### File:\s*[`']?([^`'\s]+)[`']?/i);
|
|
257
263
|
return match ? match[1] : null;
|
|
@@ -264,21 +270,28 @@ const parseLLMPatch = (patchContent: string): Record<string, PatchBlock[]> => {
|
|
|
264
270
|
const fileName = isFileMarker(line);
|
|
265
271
|
if (fileName) {
|
|
266
272
|
currentFile = fileName;
|
|
273
|
+
// Reset tracking when switching files
|
|
274
|
+
inCodeBlock = false;
|
|
275
|
+
codeBlockLines = [];
|
|
276
|
+
hasSeenMarkersInBlock = false;
|
|
267
277
|
continue;
|
|
268
278
|
}
|
|
269
279
|
|
|
270
280
|
if (isSearchMarker(line)) {
|
|
271
281
|
inSearch = true;
|
|
272
282
|
inReplace = false;
|
|
283
|
+
hasSeenMarkersInBlock = true;
|
|
273
284
|
searchLines = [];
|
|
274
285
|
replaceLines = [];
|
|
275
286
|
continue;
|
|
276
287
|
}
|
|
277
288
|
if (isDividerMarker(line)) {
|
|
278
289
|
if (inSearch) { inSearch = false; inReplace = true; }
|
|
290
|
+
hasSeenMarkersInBlock = true;
|
|
279
291
|
continue;
|
|
280
292
|
}
|
|
281
293
|
if (isEndMarker(line)) {
|
|
294
|
+
hasSeenMarkersInBlock = true;
|
|
282
295
|
if (inReplace || inSearch) {
|
|
283
296
|
if (searchLines.length > 0 || replaceLines.length > 0) {
|
|
284
297
|
if (!filePatches[currentFile]) filePatches[currentFile] = [];
|
|
@@ -291,8 +304,30 @@ const parseLLMPatch = (patchContent: string): Record<string, PatchBlock[]> => {
|
|
|
291
304
|
inSearch = false; inReplace = false; searchLines = []; replaceLines = [];
|
|
292
305
|
continue;
|
|
293
306
|
}
|
|
307
|
+
|
|
308
|
+
// Detect triple backticks to identify potential full-file replacements
|
|
309
|
+
if (line.trim().startsWith("```")) {
|
|
310
|
+
if (!inCodeBlock) {
|
|
311
|
+
inCodeBlock = true;
|
|
312
|
+
codeBlockLines = [];
|
|
313
|
+
hasSeenMarkersInBlock = false;
|
|
314
|
+
} else {
|
|
315
|
+
inCodeBlock = false;
|
|
316
|
+
// If the code block ended without any SEARCH/REPLACE markers, treat it as full-file code
|
|
317
|
+
if (!hasSeenMarkersInBlock && codeBlockLines.length > 0 && currentFile !== "unknown") {
|
|
318
|
+
if (!filePatches[currentFile]) filePatches[currentFile] = [];
|
|
319
|
+
filePatches[currentFile].push({
|
|
320
|
+
search: "__FULL_FILE_REPLACEMENT__",
|
|
321
|
+
replace: codeBlockLines.join("\n")
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
294
328
|
if (inSearch) searchLines.push(line);
|
|
295
329
|
else if (inReplace) replaceLines.push(line);
|
|
330
|
+
else if (inCodeBlock) codeBlockLines.push(line);
|
|
296
331
|
}
|
|
297
332
|
return filePatches;
|
|
298
333
|
};
|
|
@@ -313,6 +348,13 @@ export const applyPatch = (
|
|
|
313
348
|
for (const patch of patches) {
|
|
314
349
|
const { search, replace } = patch;
|
|
315
350
|
|
|
351
|
+
// Strategy 0: Full File Replacement (Overwrite)
|
|
352
|
+
if (search === "__FULL_FILE_REPLACEMENT__") {
|
|
353
|
+
console.log(`[ApplyPatch] Overwriting file with full content.`);
|
|
354
|
+
result = replace;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
|
|
316
358
|
// NẾU SEARCH RỖNG (INSERT/APPEND)
|
|
317
359
|
if (search.trim() === "") {
|
|
318
360
|
console.log(`[ApplyPatch] Inserting/appending block.`);
|
|
@@ -418,26 +460,54 @@ const startServer = async (rootDir: string) => {
|
|
|
418
460
|
// 2. Scan project files
|
|
419
461
|
const allFiles = await scanRecursively(rootDir, rootDir);
|
|
420
462
|
const modifiedFiles: string[] = [];
|
|
463
|
+
const appliedNormalizedPaths = new Set<string>();
|
|
421
464
|
|
|
422
|
-
// 3. Match patches to files and apply
|
|
465
|
+
// 3. Match patches to existing files and apply
|
|
423
466
|
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
467
|
const normalizedPath = file.path.replace(/\\/g, '/');
|
|
427
|
-
const
|
|
468
|
+
const patchesEntry = Object.entries(filePatches).find(([path]) => {
|
|
428
469
|
const normalizedKey = path.replace(/\\/g, '/');
|
|
429
470
|
return normalizedKey === normalizedPath || normalizedPath.endsWith('/' + normalizedKey);
|
|
430
|
-
})
|
|
471
|
+
});
|
|
431
472
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
473
|
+
if (patchesEntry) {
|
|
474
|
+
const [rawKey, patchesForFile] = patchesEntry;
|
|
475
|
+
// Only apply if the file was selected in UI
|
|
476
|
+
if (selectedPaths.includes(file.path)) {
|
|
477
|
+
console.log(`[Apply] Processing ${patchesForFile.length} blocks for: ${file.path}`);
|
|
478
|
+
const newContent = applyPatch(file.content, patchesForFile);
|
|
479
|
+
|
|
480
|
+
if (newContent !== file.content) {
|
|
481
|
+
await fs.writeFile(file.fullPath, newContent, "utf-8");
|
|
482
|
+
modifiedFiles.push(file.path);
|
|
483
|
+
}
|
|
440
484
|
}
|
|
485
|
+
appliedNormalizedPaths.add(rawKey.replace(/\\/g, '/'));
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// 4. Handle New Files (patches for paths that don't exist yet)
|
|
490
|
+
for (const [rawPath, patches] of Object.entries(filePatches)) {
|
|
491
|
+
const normalizedKey = rawPath.replace(/\\/g, '/');
|
|
492
|
+
if (normalizedKey === "unknown" || appliedNormalizedPaths.has(normalizedKey)) continue;
|
|
493
|
+
|
|
494
|
+
// Double check it doesn't exist (safety)
|
|
495
|
+
const alreadyExists = allFiles.some(f => {
|
|
496
|
+
const np = f.path.replace(/\\/g, '/');
|
|
497
|
+
return np === normalizedKey || np.endsWith('/' + normalizedKey);
|
|
498
|
+
});
|
|
499
|
+
if (alreadyExists) continue;
|
|
500
|
+
|
|
501
|
+
console.log(`[Apply] Creating new file: ${rawPath}`);
|
|
502
|
+
const newContent = applyPatch("", patches);
|
|
503
|
+
const fullPath = path.resolve(rootDir, rawPath);
|
|
504
|
+
|
|
505
|
+
try {
|
|
506
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
507
|
+
await fs.writeFile(fullPath, newContent, "utf-8");
|
|
508
|
+
modifiedFiles.push(rawPath);
|
|
509
|
+
} catch (err) {
|
|
510
|
+
console.error(`[Apply] Failed to create file ${rawPath}:`, err);
|
|
441
511
|
}
|
|
442
512
|
}
|
|
443
513
|
|
|
@@ -662,7 +732,8 @@ const startServer = async (rootDir: string) => {
|
|
|
662
732
|
|
|
663
733
|
async init() {
|
|
664
734
|
try {
|
|
665
|
-
|
|
735
|
+
// Sử dụng relative URL để tránh lỗi phân giải localhost/127.0.0.1 trên Windows
|
|
736
|
+
const res = await fetch('/api/files');
|
|
666
737
|
const data = await res.json();
|
|
667
738
|
this.rawFiles = data;
|
|
668
739
|
this.buildTree();
|
|
@@ -672,98 +743,10 @@ const startServer = async (rootDir: string) => {
|
|
|
672
743
|
this.loading = false;
|
|
673
744
|
}
|
|
674
745
|
|
|
675
|
-
// Search watcher
|
|
676
746
|
this.$watch('search', () => this.buildTree());
|
|
677
|
-
|
|
678
|
-
// Auto-generate prompt
|
|
679
747
|
this.$watch('instruction', () => this.debouncedFetchPrompt());
|
|
680
748
|
this.$watch('selectedFiles', () => this.debouncedFetchPrompt());
|
|
681
749
|
},
|
|
682
|
-
|
|
683
|
-
// ===========================
|
|
684
|
-
// Tree Logic
|
|
685
|
-
// ===========================
|
|
686
|
-
|
|
687
|
-
buildTree() {
|
|
688
|
-
// 1. Convert paths to object structure
|
|
689
|
-
const root = {};
|
|
690
|
-
const searchLower = this.search.toLowerCase();
|
|
691
|
-
|
|
692
|
-
this.rawFiles.forEach(file => {
|
|
693
|
-
if (this.search && !file.path.toLowerCase().includes(searchLower)) return;
|
|
694
|
-
|
|
695
|
-
const parts = file.path.split(/[\\\\/]/);
|
|
696
|
-
let current = root;
|
|
697
|
-
|
|
698
|
-
parts.forEach((part, index) => {
|
|
699
|
-
if (!current[part]) {
|
|
700
|
-
current[part] = {
|
|
701
|
-
name: part,
|
|
702
|
-
path: parts.slice(0, index + 1).join('/'),
|
|
703
|
-
children: {},
|
|
704
|
-
type: index === parts.length - 1 ? 'file' : 'folder',
|
|
705
|
-
fullPath: file.path // Only meaningful for file
|
|
706
|
-
};
|
|
707
|
-
}
|
|
708
|
-
current = current[part].children;
|
|
709
|
-
});
|
|
710
|
-
});
|
|
711
|
-
|
|
712
|
-
// 2. Flatten object structure into renderable list with depth
|
|
713
|
-
this.treeNodes = [];
|
|
714
|
-
const traverse = (nodeMap, depth) => {
|
|
715
|
-
// Sort: Folders first, then Files. Alphabetical.
|
|
716
|
-
const keys = Object.keys(nodeMap).sort((a, b) => {
|
|
717
|
-
const nodeA = nodeMap[a];
|
|
718
|
-
const nodeB = nodeMap[b];
|
|
719
|
-
if (nodeA.type !== nodeB.type) return nodeA.type === 'folder' ? -1 : 1;
|
|
720
|
-
return a.localeCompare(b);
|
|
721
|
-
});
|
|
722
|
-
|
|
723
|
-
keys.forEach(key => {
|
|
724
|
-
const node = nodeMap[key];
|
|
725
|
-
const flatNode = {
|
|
726
|
-
id: node.path,
|
|
727
|
-
name: node.name,
|
|
728
|
-
path: node.path, // Relative path for selection
|
|
729
|
-
type: node.type,
|
|
730
|
-
depth: depth,
|
|
731
|
-
children: Object.keys(node.children).length > 0, // boolean for UI
|
|
732
|
-
expanded: this.search.length > 0 || depth < 1, // Auto expand on search or root
|
|
733
|
-
rawChildren: node.children // Keep ref for recursion
|
|
734
|
-
};
|
|
735
|
-
|
|
736
|
-
this.treeNodes.push(flatNode);
|
|
737
|
-
if (flatNode.children && flatNode.expanded) {
|
|
738
|
-
traverse(node.children, depth + 1);
|
|
739
|
-
}
|
|
740
|
-
});
|
|
741
|
-
};
|
|
742
|
-
|
|
743
|
-
traverse(root, 0);
|
|
744
|
-
},
|
|
745
|
-
|
|
746
|
-
// Re-calculate visible nodes (folding logic) without rebuilding everything
|
|
747
|
-
// But for simplicity in this SFC, we just rebuild the list when expanded changes.
|
|
748
|
-
toggleExpand(node) {
|
|
749
|
-
if (node.type !== 'folder') return;
|
|
750
|
-
|
|
751
|
-
// Find node in treeNodes and toggle
|
|
752
|
-
// Since treeNodes is flat and generated, we need to persist state or smarter logic.
|
|
753
|
-
// Simpler: Just modify the expanded state in the current flat list is tricky because children are dynamic.
|
|
754
|
-
// Better approach for this lightweight UI:
|
|
755
|
-
// We actually need a persistent "expanded" Set to survive re-renders if we want to be fancy.
|
|
756
|
-
// For now, let's just cheat: Update the boolean in the flat list works for collapsing,
|
|
757
|
-
// but expanding requires knowing the children.
|
|
758
|
-
|
|
759
|
-
// Let's reload the tree logic but using a \`expandedPaths\` set.
|
|
760
|
-
if (this.expandedPaths.has(node.path)) {
|
|
761
|
-
this.expandedPaths.delete(node.path);
|
|
762
|
-
} else {
|
|
763
|
-
this.expandedPaths.add(node.path);
|
|
764
|
-
}
|
|
765
|
-
this.refreshTreeVisibility();
|
|
766
|
-
},
|
|
767
750
|
|
|
768
751
|
// Alternative: Just use a computed property for \`visibleNodes\`.
|
|
769
752
|
// Since Alpine isn't React, we do this manually.
|
|
@@ -791,7 +774,8 @@ const startServer = async (rootDir: string) => {
|
|
|
791
774
|
|
|
792
775
|
this.rawFiles.forEach(file => {
|
|
793
776
|
if (this.search && !file.path.toLowerCase().includes(searchLower)) return;
|
|
794
|
-
|
|
777
|
+
// File path đã được chuẩn hóa '/' từ backend
|
|
778
|
+
const parts = file.path.split('/');
|
|
795
779
|
let current = this.rootObject;
|
|
796
780
|
parts.forEach((part, index) => {
|
|
797
781
|
if (!current[part]) {
|
|
@@ -892,9 +876,8 @@ const startServer = async (rootDir: string) => {
|
|
|
892
876
|
},
|
|
893
877
|
|
|
894
878
|
getAllDescendants(folderPath) {
|
|
895
|
-
// Simple filter from rawFiles
|
|
896
879
|
return this.rawFiles
|
|
897
|
-
.filter(f => f.path
|
|
880
|
+
.filter(f => f.path === folderPath || f.path.startsWith(folderPath + '/'))
|
|
898
881
|
.map(f => f.path);
|
|
899
882
|
},
|
|
900
883
|
|
|
@@ -927,7 +910,7 @@ const startServer = async (rootDir: string) => {
|
|
|
927
910
|
}
|
|
928
911
|
this.generating = true;
|
|
929
912
|
try {
|
|
930
|
-
const res = await fetch('
|
|
913
|
+
const res = await fetch('/api/prompt', {
|
|
931
914
|
method: 'POST',
|
|
932
915
|
headers: {'Content-Type': 'application/json'},
|
|
933
916
|
body: JSON.stringify({ filePaths: Array.from(this.selectedFiles), instruction: this.instruction })
|
|
@@ -954,7 +937,7 @@ const startServer = async (rootDir: string) => {
|
|
|
954
937
|
async applyPatch() {
|
|
955
938
|
this.applying = true;
|
|
956
939
|
try {
|
|
957
|
-
const res = await fetch('
|
|
940
|
+
const res = await fetch('/api/apply', {
|
|
958
941
|
method: 'POST',
|
|
959
942
|
headers: {'Content-Type': 'application/json'},
|
|
960
943
|
body: JSON.stringify({ filePaths: Array.from(this.selectedFiles), llmResponse: this.llmResponse })
|