@dyyz1993/create-agent 2.0.1 → 2.1.0
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/package.json +1 -1
- package/src/commands/create.ts +31 -31
- package/src/commands/workspace.ts +112 -105
- package/src/lib/copy.ts +1 -0
- package/templates/agent/electron/main.js +46 -0
- package/templates/agent/electron/preload.js +5 -0
- package/templates/agent/electron-builder.json +40 -0
- package/templates/agent/eslint.config.mjs +2 -0
- package/templates/agent/package.json +60 -2
- package/templates/agent/src/mainview/App.tsx +29 -26
- package/templates/agent/src/mainview/components/chat/ChatPanel.tsx +88 -88
- package/templates/agent/src/mainview/components/file-preview/VirtualizedCodeView.tsx +105 -81
- package/templates/agent/src/mainview/components/search/SearchPanel.tsx +427 -378
- package/templates/agent/src/mainview/components/todo/TodoPanel.tsx +3 -3
- package/templates/agent/src/mainview/hooks/use-input-history.ts +70 -61
- package/templates/agent/src/mainview/lib/api-client.ts +1 -4
- package/templates/agent/src/mainview/main.tsx +4 -10
- package/templates/agent/src/mainview/stores/use-feed-store.ts +107 -107
- package/templates/agent/src/mainview/utils/drop-handler.ts +114 -115
- package/templates/agent/src/server-config.ts +1 -1
- package/templates/agent/src/server.ts +1 -2
- package/templates/agent/src/shared/handlers/chat.ts +5 -5
- package/templates/agent/src/shared/handlers/debug.ts +5 -1
- package/templates/agent/src/shared/handlers/git.ts +286 -243
- package/templates/agent/src/shared/http-routes.ts +1 -1
- package/templates/agent/src/shared/lib/bash-security.ts +43 -43
- package/templates/agent/tsconfig.ipc.json +5 -1
- package/templates/agent/tsconfig.json +3 -1
- package/templates/chat/package.json +3 -0
- package/templates/chat/src/mainview/hooks/use-input-history.ts +70 -61
- package/templates/chat/src/mainview/lib/api-client.ts +2 -5
- package/templates/chat/src/mainview/main.tsx +10 -7
- package/templates/chat/src/server-config.ts +1 -1
- package/templates/chat/src/server.ts +1 -2
- package/templates/chat/src/shared/handlers/chat.ts +5 -5
- package/templates/chat/src/shared/handlers/debug.ts +5 -1
- package/templates/chat/src/shared/http-routes.ts +1 -1
- package/templates/chat/tsconfig.ipc.json +5 -1
- package/templates/chat/tsconfig.json +12 -2
- package/templates/general/package.json +3 -0
- package/templates/general/src/mainview/components/file-preview/VirtualizedCodeView.tsx +101 -81
- package/templates/general/src/mainview/components/search/SearchPanel.tsx +429 -378
- package/templates/general/src/mainview/hooks/use-input-history.ts +70 -61
- package/templates/general/src/mainview/lib/api-client.ts +2 -5
- package/templates/general/src/mainview/main.tsx +10 -7
- package/templates/general/src/mainview/stores/use-feed-store.ts +107 -107
- package/templates/general/src/mainview/utils/drop-handler.ts +114 -115
- package/templates/general/src/server-config.ts +1 -1
- package/templates/general/src/server.ts +1 -2
- package/templates/general/src/shared/handlers/chat.ts +5 -5
- package/templates/general/src/shared/handlers/debug.ts +5 -1
- package/templates/general/src/shared/handlers/git.ts +286 -243
- package/templates/general/src/shared/http-routes.ts +1 -1
- package/templates/general/tsconfig.ipc.json +5 -1
- package/templates/general/tsconfig.json +12 -2
- package/templates/shared/components/ErrorBoundary.tsx +50 -49
- package/templates/shared/http-routes.ts +210 -190
|
@@ -1,404 +1,453 @@
|
|
|
1
1
|
import { useState, useCallback, useRef, useEffect } from "react";
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
Search,
|
|
4
|
+
CaseSensitive,
|
|
5
|
+
WholeWord,
|
|
6
|
+
Regex,
|
|
7
|
+
FileText,
|
|
8
|
+
ChevronRight,
|
|
9
|
+
ChevronDown,
|
|
10
|
+
Loader2,
|
|
11
|
+
X,
|
|
12
12
|
} from "lucide-react";
|
|
13
13
|
import { apiClient } from "../../lib/api-client";
|
|
14
14
|
import { useExplorerStore } from "../../stores/use-explorer-store";
|
|
15
15
|
import type { TreeNode } from "../../types";
|
|
16
16
|
|
|
17
17
|
interface SearchMatch {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
file: string;
|
|
19
|
+
line: number;
|
|
20
|
+
column: number;
|
|
21
|
+
text: string;
|
|
22
|
+
highlightStart: number;
|
|
23
|
+
highlightEnd: number;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
interface FileGroup {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
filePath: string;
|
|
28
|
+
relativePath: string;
|
|
29
|
+
matches: SearchMatch[];
|
|
30
|
+
expanded: boolean;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
const SKIP_DIRS = new Set([
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
34
|
+
"node_modules",
|
|
35
|
+
".git",
|
|
36
|
+
"dist",
|
|
37
|
+
"build",
|
|
38
|
+
".next",
|
|
39
|
+
".nuxt",
|
|
40
|
+
"coverage",
|
|
41
|
+
".cache",
|
|
42
|
+
".turbo",
|
|
43
|
+
".vercel",
|
|
44
|
+
"out",
|
|
45
|
+
".output",
|
|
46
46
|
]);
|
|
47
47
|
|
|
48
48
|
const TEXT_EXTENSIONS = new Set([
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
49
|
+
"ts",
|
|
50
|
+
"tsx",
|
|
51
|
+
"js",
|
|
52
|
+
"jsx",
|
|
53
|
+
"mjs",
|
|
54
|
+
"cjs",
|
|
55
|
+
"json",
|
|
56
|
+
"md",
|
|
57
|
+
"mdx",
|
|
58
|
+
"yaml",
|
|
59
|
+
"yml",
|
|
60
|
+
"toml",
|
|
61
|
+
"css",
|
|
62
|
+
"scss",
|
|
63
|
+
"less",
|
|
64
|
+
"sass",
|
|
65
|
+
"html",
|
|
66
|
+
"htm",
|
|
67
|
+
"xml",
|
|
68
|
+
"svg",
|
|
69
|
+
"py",
|
|
70
|
+
"rb",
|
|
71
|
+
"go",
|
|
72
|
+
"rs",
|
|
73
|
+
"java",
|
|
74
|
+
"kt",
|
|
75
|
+
"swift",
|
|
76
|
+
"c",
|
|
77
|
+
"cpp",
|
|
78
|
+
"h",
|
|
79
|
+
"hpp",
|
|
80
|
+
"sh",
|
|
81
|
+
"bash",
|
|
82
|
+
"zsh",
|
|
83
|
+
"fish",
|
|
84
|
+
"sql",
|
|
85
|
+
"graphql",
|
|
86
|
+
"gql",
|
|
87
|
+
"txt",
|
|
88
|
+
"csv",
|
|
89
|
+
"env",
|
|
90
|
+
"gitignore",
|
|
91
|
+
"eslintrc",
|
|
92
|
+
"prettierrc",
|
|
93
|
+
"conf",
|
|
94
|
+
"ini",
|
|
95
|
+
"cfg",
|
|
96
|
+
"log",
|
|
97
|
+
"vue",
|
|
98
|
+
"svelte",
|
|
59
99
|
]);
|
|
60
100
|
|
|
61
101
|
function isTextFile(name: string): boolean {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
102
|
+
const dotIdx = name.lastIndexOf(".");
|
|
103
|
+
if (dotIdx === -1) return false;
|
|
104
|
+
const ext = name.slice(dotIdx + 1).toLowerCase();
|
|
105
|
+
if (TEXT_EXTENSIONS.has(ext)) return true;
|
|
106
|
+
if (name.startsWith(".") && TEXT_EXTENSIONS.has(name.slice(1))) return true;
|
|
107
|
+
return false;
|
|
68
108
|
}
|
|
69
109
|
|
|
70
|
-
function buildSearchRegex(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
110
|
+
function buildSearchRegex(
|
|
111
|
+
query: string,
|
|
112
|
+
caseSensitive: boolean,
|
|
113
|
+
wholeWord: boolean,
|
|
114
|
+
useRegex: boolean
|
|
115
|
+
): RegExp {
|
|
116
|
+
let pattern = useRegex ? query : query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
117
|
+
if (wholeWord) pattern = `\\b${pattern}\\b`;
|
|
118
|
+
return new RegExp(pattern, caseSensitive ? "g" : "gi");
|
|
74
119
|
}
|
|
75
120
|
|
|
76
121
|
export function SearchPanel() {
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
122
|
+
const [query, setQuery] = useState("");
|
|
123
|
+
const [caseSensitive, setCaseSensitive] = useState(false);
|
|
124
|
+
const [wholeWord, setWholeWord] = useState(false);
|
|
125
|
+
const [useRegex, setUseRegex] = useState(false);
|
|
126
|
+
const [results, setResults] = useState<FileGroup[]>([]);
|
|
127
|
+
const [isSearching, setIsSearching] = useState(false);
|
|
128
|
+
const [totalMatches, setTotalMatches] = useState(0);
|
|
129
|
+
const [filesSearched, setFilesSearched] = useState(0);
|
|
130
|
+
const [hasSearched, setHasSearched] = useState(false);
|
|
131
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
132
|
+
const abortRef = useRef(false);
|
|
133
|
+
|
|
134
|
+
const currentPath = useExplorerStore((s) => s.currentPath);
|
|
135
|
+
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
inputRef.current?.focus();
|
|
138
|
+
}, []);
|
|
139
|
+
|
|
140
|
+
const collectFiles = useCallback(async (dirPath: string): Promise<string[]> => {
|
|
141
|
+
const files: string[] = [];
|
|
142
|
+
const queue = [dirPath];
|
|
143
|
+
|
|
144
|
+
while (queue.length > 0 && !abortRef.current) {
|
|
145
|
+
const current = queue.shift()!;
|
|
146
|
+
try {
|
|
147
|
+
const res = await apiClient.call("file.listDir", { path: current });
|
|
148
|
+
for (const entry of res.entries) {
|
|
149
|
+
if (abortRef.current) break;
|
|
150
|
+
if (entry.name.startsWith(".") && entry.name !== ".env" && entry.name !== ".env.local")
|
|
151
|
+
continue;
|
|
152
|
+
if (entry.type === "directory") {
|
|
153
|
+
if (!SKIP_DIRS.has(entry.name)) queue.push(entry.path);
|
|
154
|
+
} else if (isTextFile(entry.name)) {
|
|
155
|
+
files.push(entry.path);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
// skip unreadable dirs
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return files;
|
|
163
|
+
}, []);
|
|
164
|
+
|
|
165
|
+
const performSearch = useCallback(async () => {
|
|
166
|
+
if (!query.trim() || !currentPath) return;
|
|
167
|
+
|
|
168
|
+
abortRef.current = false;
|
|
169
|
+
setIsSearching(true);
|
|
170
|
+
setHasSearched(true);
|
|
171
|
+
setResults([]);
|
|
172
|
+
setTotalMatches(0);
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const regex = buildSearchRegex(query.trim(), caseSensitive, wholeWord, useRegex);
|
|
176
|
+
const filePaths = await collectFiles(currentPath);
|
|
177
|
+
const matchMap = new Map<string, SearchMatch[]>();
|
|
178
|
+
let matchCount = 0;
|
|
179
|
+
|
|
180
|
+
for (const fp of filePaths) {
|
|
181
|
+
if (abortRef.current) break;
|
|
182
|
+
try {
|
|
183
|
+
const res = await apiClient.call("file.readFile", { path: fp });
|
|
184
|
+
const lines = res.content.split("\n");
|
|
185
|
+
for (let i = 0; i < lines.length; i++) {
|
|
186
|
+
const line = lines[i]!;
|
|
187
|
+
regex.lastIndex = 0;
|
|
188
|
+
const m = regex.exec(line);
|
|
189
|
+
if (m) {
|
|
190
|
+
if (!matchMap.has(fp)) matchMap.set(fp, []);
|
|
191
|
+
matchMap.get(fp)!.push({
|
|
192
|
+
file: fp,
|
|
193
|
+
line: i + 1,
|
|
194
|
+
column: m.index + 1,
|
|
195
|
+
text: line!,
|
|
196
|
+
highlightStart: m.index,
|
|
197
|
+
highlightEnd: m.index + m[0]!.length,
|
|
198
|
+
});
|
|
199
|
+
matchCount++;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
// skip unreadable files
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
setFilesSearched(filePaths.length);
|
|
208
|
+
|
|
209
|
+
const rootPath = currentPath.endsWith("/") ? currentPath : currentPath + "/";
|
|
210
|
+
const groups: FileGroup[] = [];
|
|
211
|
+
for (const [filePath, matches] of matchMap) {
|
|
212
|
+
groups.push({
|
|
213
|
+
filePath,
|
|
214
|
+
relativePath: filePath.startsWith(rootPath) ? filePath.slice(rootPath.length) : filePath,
|
|
215
|
+
matches,
|
|
216
|
+
expanded: true,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
setResults(groups);
|
|
221
|
+
setTotalMatches(matchCount);
|
|
222
|
+
} catch {
|
|
223
|
+
// search error
|
|
224
|
+
} finally {
|
|
225
|
+
setIsSearching(false);
|
|
226
|
+
}
|
|
227
|
+
}, [query, caseSensitive, wholeWord, useRegex, currentPath, collectFiles]);
|
|
228
|
+
|
|
229
|
+
const cancelSearch = useCallback(() => {
|
|
230
|
+
abortRef.current = true;
|
|
231
|
+
}, []);
|
|
232
|
+
|
|
233
|
+
const handleKeyDown = useCallback(
|
|
234
|
+
(e: React.KeyboardEvent) => {
|
|
235
|
+
if (e.key === "Enter" && !isSearching) {
|
|
236
|
+
performSearch();
|
|
237
|
+
}
|
|
238
|
+
if (e.key === "Escape") {
|
|
239
|
+
setQuery("");
|
|
240
|
+
inputRef.current?.focus();
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
[performSearch, isSearching]
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const toggleGroup = useCallback((index: number) => {
|
|
247
|
+
setResults((prev) => prev.map((g, i) => (i === index ? { ...g, expanded: !g.expanded } : g)));
|
|
248
|
+
}, []);
|
|
249
|
+
|
|
250
|
+
const handleOpenFile = useCallback((filePath: string, _line: number) => {
|
|
251
|
+
const node: TreeNode = {
|
|
252
|
+
name: filePath.split("/").pop() || filePath,
|
|
253
|
+
path: filePath,
|
|
254
|
+
type: "file",
|
|
255
|
+
};
|
|
256
|
+
useExplorerStore.getState().openFile(node);
|
|
257
|
+
}, []);
|
|
258
|
+
|
|
259
|
+
const modKey =
|
|
260
|
+
typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent) ? "⌘" : "Ctrl";
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<div className="flex flex-col h-full bg-[var(--color-bg-primary)]">
|
|
264
|
+
{/* Header */}
|
|
265
|
+
<div className="px-3 py-2 text-xs font-semibold text-[var(--color-text-tertiary)] uppercase tracking-wide border-b border-[var(--color-border-primary)] flex items-center justify-between flex-shrink-0">
|
|
266
|
+
<div className="flex items-center gap-1.5">
|
|
267
|
+
<Search className="w-3.5 h-3.5" />
|
|
268
|
+
Search
|
|
269
|
+
</div>
|
|
270
|
+
<kbd className="px-1.5 py-0.5 text-[10px] text-[var(--color-text-placeholder)] bg-[var(--color-bg-secondary)] border border-[var(--color-border-primary)] rounded font-mono">
|
|
271
|
+
{modKey}⇧F
|
|
272
|
+
</kbd>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
{/* Search input */}
|
|
276
|
+
<div className="p-2 border-b border-[var(--color-border-primary)] flex-shrink-0">
|
|
277
|
+
<div className="relative">
|
|
278
|
+
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-[var(--color-text-placeholder)]" />
|
|
279
|
+
<input
|
|
280
|
+
ref={inputRef}
|
|
281
|
+
type="text"
|
|
282
|
+
value={query}
|
|
283
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
284
|
+
onKeyDown={handleKeyDown}
|
|
285
|
+
placeholder="Search in files..."
|
|
286
|
+
className="w-full pl-7 pr-7 py-1.5 text-xs bg-[var(--color-bg-secondary)] rounded text-[var(--color-text-primary)] border border-[var(--color-border-secondary)] focus:border-[var(--color-accent)] focus:outline-none"
|
|
287
|
+
/>
|
|
288
|
+
{query && (
|
|
289
|
+
<button
|
|
290
|
+
onClick={() => {
|
|
291
|
+
setQuery("");
|
|
292
|
+
inputRef.current?.focus();
|
|
293
|
+
}}
|
|
294
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 text-[var(--color-text-placeholder)] hover:text-[var(--color-text-secondary)]"
|
|
295
|
+
>
|
|
296
|
+
<X className="w-3 h-3" />
|
|
297
|
+
</button>
|
|
298
|
+
)}
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
{/* Toggle buttons */}
|
|
302
|
+
<div className="flex items-center gap-0.5 mt-1.5">
|
|
303
|
+
<button
|
|
304
|
+
onClick={() => setCaseSensitive((v) => !v)}
|
|
305
|
+
className={`p-1 rounded text-xs transition-colors ${
|
|
306
|
+
caseSensitive
|
|
307
|
+
? "bg-[var(--color-accent)]/30 text-[var(--color-text-accent)] border border-[var(--color-accent)]/50"
|
|
308
|
+
: "text-[var(--color-text-placeholder)] hover:text-[var(--color-text-secondary)] border border-transparent"
|
|
309
|
+
}`}
|
|
310
|
+
title="Match Case"
|
|
311
|
+
>
|
|
312
|
+
<CaseSensitive className="w-3.5 h-3.5" />
|
|
313
|
+
</button>
|
|
314
|
+
<button
|
|
315
|
+
onClick={() => setWholeWord((v) => !v)}
|
|
316
|
+
className={`p-1 rounded text-xs transition-colors ${
|
|
317
|
+
wholeWord
|
|
318
|
+
? "bg-[var(--color-accent)]/30 text-[var(--color-text-accent)] border border-[var(--color-accent)]/50"
|
|
319
|
+
: "text-[var(--color-text-placeholder)] hover:text-[var(--color-text-secondary)] border border-transparent"
|
|
320
|
+
}`}
|
|
321
|
+
title="Match Whole Word"
|
|
322
|
+
>
|
|
323
|
+
<WholeWord className="w-3.5 h-3.5" />
|
|
324
|
+
</button>
|
|
325
|
+
<button
|
|
326
|
+
onClick={() => setUseRegex((v) => !v)}
|
|
327
|
+
className={`p-1 rounded text-xs transition-colors ${
|
|
328
|
+
useRegex
|
|
329
|
+
? "bg-[var(--color-accent)]/30 text-[var(--color-text-accent)] border border-[var(--color-accent)]/50"
|
|
330
|
+
: "text-[var(--color-text-placeholder)] hover:text-[var(--color-text-secondary)] border border-transparent"
|
|
331
|
+
}`}
|
|
332
|
+
title="Use Regular Expression"
|
|
333
|
+
>
|
|
334
|
+
<Regex className="w-3.5 h-3.5" />
|
|
335
|
+
</button>
|
|
336
|
+
|
|
337
|
+
<div className="flex-1" />
|
|
338
|
+
|
|
339
|
+
{isSearching ? (
|
|
340
|
+
<button
|
|
341
|
+
onClick={cancelSearch}
|
|
342
|
+
className="px-2 py-1 text-xs text-[var(--color-text-error)] hover:text-red-300 transition-colors flex items-center gap-1"
|
|
343
|
+
>
|
|
344
|
+
<Loader2 className="w-3 h-3 animate-spin" />
|
|
345
|
+
Cancel
|
|
346
|
+
</button>
|
|
347
|
+
) : (
|
|
348
|
+
query.trim() && (
|
|
349
|
+
<button
|
|
350
|
+
onClick={performSearch}
|
|
351
|
+
className="px-2 py-1 text-xs bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] rounded transition-colors"
|
|
352
|
+
>
|
|
353
|
+
Search
|
|
354
|
+
</button>
|
|
355
|
+
)
|
|
356
|
+
)}
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
|
|
360
|
+
{/* Results header */}
|
|
361
|
+
{hasSearched && !isSearching && (
|
|
362
|
+
<div className="px-3 py-1.5 text-[11px] text-[var(--color-text-placeholder)] border-b border-[var(--color-bg-secondary)] flex-shrink-0">
|
|
363
|
+
{totalMatches > 0 ? (
|
|
364
|
+
<>
|
|
365
|
+
<span className="text-[var(--color-text-accent)] font-medium">{totalMatches}</span>{" "}
|
|
366
|
+
result{totalMatches !== 1 && "s"} in{" "}
|
|
367
|
+
<span className="text-[var(--color-text-accent)] font-medium">{results.length}</span>{" "}
|
|
368
|
+
file
|
|
369
|
+
{results.length !== 1 && "s"}
|
|
370
|
+
<span className="ml-2 text-gray-600">({filesSearched} files searched)</span>
|
|
371
|
+
</>
|
|
372
|
+
) : (
|
|
373
|
+
"No results found"
|
|
374
|
+
)}
|
|
375
|
+
</div>
|
|
376
|
+
)}
|
|
377
|
+
|
|
378
|
+
{/* Loading state */}
|
|
379
|
+
{isSearching && (
|
|
380
|
+
<div className="flex items-center justify-center py-8 text-[var(--color-text-placeholder)] text-sm flex-shrink-0">
|
|
381
|
+
<Loader2 className="w-4 h-4 animate-spin mr-2" />
|
|
382
|
+
Searching...
|
|
383
|
+
</div>
|
|
384
|
+
)}
|
|
385
|
+
|
|
386
|
+
{/* Results list */}
|
|
387
|
+
<div className="flex-1 overflow-y-auto">
|
|
388
|
+
{!hasSearched && !isSearching ? (
|
|
389
|
+
<div className="flex flex-col items-center justify-center h-full text-gray-600 text-xs px-4">
|
|
390
|
+
<Search className="w-8 h-8 mb-2 text-gray-700" />
|
|
391
|
+
<p>Type a search term and press Enter</p>
|
|
392
|
+
<p className="mt-1 text-gray-700">{modKey}+Shift+F to focus search</p>
|
|
393
|
+
</div>
|
|
394
|
+
) : !isSearching && totalMatches === 0 ? (
|
|
395
|
+
<div className="flex flex-col items-center justify-center h-full text-gray-600 text-xs px-4">
|
|
396
|
+
<FileText className="w-8 h-8 mb-2 text-gray-700" />
|
|
397
|
+
<p>No results for "{query}"</p>
|
|
398
|
+
<p className="mt-1 text-gray-700">Try different search terms</p>
|
|
399
|
+
</div>
|
|
400
|
+
) : (
|
|
401
|
+
<div className="py-1">
|
|
402
|
+
{results.map((group, gi) => (
|
|
403
|
+
<div key={group.filePath}>
|
|
404
|
+
{/* File group header */}
|
|
405
|
+
<button
|
|
406
|
+
onClick={() => toggleGroup(gi)}
|
|
407
|
+
className="w-full flex items-center gap-1 px-2 py-1 text-xs hover:bg-[var(--color-bg-secondary)] transition-colors text-left"
|
|
408
|
+
>
|
|
409
|
+
{group.expanded ? (
|
|
410
|
+
<ChevronDown className="w-3 h-3 text-[var(--color-text-placeholder)] flex-shrink-0" />
|
|
411
|
+
) : (
|
|
412
|
+
<ChevronRight className="w-3 h-3 text-[var(--color-text-placeholder)] flex-shrink-0" />
|
|
413
|
+
)}
|
|
414
|
+
<FileText className="w-3.5 h-3.5 text-[var(--color-text-placeholder)] flex-shrink-0" />
|
|
415
|
+
<span className="text-[var(--color-text-secondary)] truncate flex-1">
|
|
416
|
+
{group.relativePath}
|
|
417
|
+
</span>
|
|
418
|
+
<span className="text-[10px] text-gray-600 flex-shrink-0 ml-1">
|
|
419
|
+
{group.matches.length}
|
|
420
|
+
</span>
|
|
421
|
+
</button>
|
|
422
|
+
|
|
423
|
+
{/* Individual matches */}
|
|
424
|
+
{group.expanded && (
|
|
425
|
+
<div className="ml-4">
|
|
426
|
+
{group.matches.map((match, mi) => (
|
|
427
|
+
<button
|
|
428
|
+
key={mi}
|
|
429
|
+
onClick={() => handleOpenFile(match.file, match.line)}
|
|
430
|
+
className="w-full text-left px-2 py-0.5 text-[11px] hover:bg-[var(--color-bg-secondary)] transition-colors group flex items-start gap-2"
|
|
431
|
+
>
|
|
432
|
+
<span className="text-gray-600 w-6 text-right flex-shrink-0 select-none pt-px">
|
|
433
|
+
{match.line}
|
|
434
|
+
</span>
|
|
435
|
+
<span className="text-[var(--color-text-tertiary)] truncate font-mono leading-relaxed">
|
|
436
|
+
{match.text.slice(0, match.highlightStart)}
|
|
437
|
+
<span className="bg-[var(--color-accent)]/40 text-[var(--color-text-accent)]">
|
|
438
|
+
{match.text.slice(match.highlightStart, match.highlightEnd)}
|
|
439
|
+
</span>
|
|
440
|
+
{match.text.slice(match.highlightEnd)}
|
|
441
|
+
</span>
|
|
442
|
+
</button>
|
|
443
|
+
))}
|
|
444
|
+
</div>
|
|
445
|
+
)}
|
|
446
|
+
</div>
|
|
447
|
+
))}
|
|
448
|
+
</div>
|
|
449
|
+
)}
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
452
|
+
);
|
|
404
453
|
}
|