promptarchitect 0.6.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/.vscodeignore +7 -0
- package/CHANGELOG.md +28 -0
- package/LICENSE +44 -0
- package/README.md +200 -0
- package/docs/CHAT_UI_REDESIGN_PLAN.md +371 -0
- package/images/hub-icon.svg +6 -0
- package/images/prompt-lab-icon.svg +11 -0
- package/package.json +519 -0
- package/src/agentPrompts.ts +278 -0
- package/src/agentService.ts +630 -0
- package/src/api.ts +223 -0
- package/src/authService.ts +556 -0
- package/src/chatPanel.ts +979 -0
- package/src/extension.ts +822 -0
- package/src/providers/aiChatViewProvider.ts +1023 -0
- package/src/providers/environmentTreeProvider.ts +311 -0
- package/src/providers/index.ts +9 -0
- package/src/providers/notesTreeProvider.ts +301 -0
- package/src/providers/quickAccessTreeProvider.ts +328 -0
- package/src/providers/scriptsTreeProvider.ts +324 -0
- package/src/refinerPanel.ts +620 -0
- package/src/templates.ts +61 -0
- package/src/workspaceIndexer.ts +766 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace Indexer
|
|
3
|
+
*
|
|
4
|
+
* Scans and indexes the workspace to provide context for prompt refinement.
|
|
5
|
+
* Creates a lightweight summary of the codebase structure, key files, and patterns.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as vscode from 'vscode';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
|
|
11
|
+
export interface WorkspaceIndex {
|
|
12
|
+
timestamp: number;
|
|
13
|
+
workspaceName: string;
|
|
14
|
+
workspacePath: string;
|
|
15
|
+
summary: string;
|
|
16
|
+
structure: FileStructure[];
|
|
17
|
+
languages: LanguageStats[];
|
|
18
|
+
keyFiles: KeyFile[];
|
|
19
|
+
patterns: string[];
|
|
20
|
+
totalFiles: number;
|
|
21
|
+
totalLines: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface FileStructure {
|
|
25
|
+
path: string;
|
|
26
|
+
type: 'file' | 'directory';
|
|
27
|
+
language?: string;
|
|
28
|
+
lineCount?: number;
|
|
29
|
+
hasTests?: boolean;
|
|
30
|
+
hasTypes?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface LanguageStats {
|
|
34
|
+
language: string;
|
|
35
|
+
fileCount: number;
|
|
36
|
+
lineCount: number;
|
|
37
|
+
percentage: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface KeyFile {
|
|
41
|
+
path: string;
|
|
42
|
+
type: 'config' | 'entry' | 'readme' | 'test' | 'types' | 'api' | 'component';
|
|
43
|
+
summary: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// File patterns to identify key files
|
|
47
|
+
const KEY_FILE_PATTERNS: Record<string, RegExp[]> = {
|
|
48
|
+
config: [
|
|
49
|
+
/^package\.json$/,
|
|
50
|
+
/^tsconfig.*\.json$/,
|
|
51
|
+
/^\.env(\..*)?$/,
|
|
52
|
+
/^vite\.config\.[jt]s$/,
|
|
53
|
+
/^webpack\.config\.[jt]s$/,
|
|
54
|
+
/^next\.config\.[jt]s$/,
|
|
55
|
+
/^tailwind\.config\.[jt]s$/,
|
|
56
|
+
/^firebase\.json$/,
|
|
57
|
+
/^docker-compose\.ya?ml$/,
|
|
58
|
+
/^Dockerfile$/,
|
|
59
|
+
],
|
|
60
|
+
entry: [
|
|
61
|
+
/^(index|main|app)\.[jt]sx?$/,
|
|
62
|
+
/^src\/(index|main|app)\.[jt]sx?$/,
|
|
63
|
+
],
|
|
64
|
+
readme: [/^readme\.md$/i, /^docs\/.*\.md$/i],
|
|
65
|
+
test: [/\.(test|spec)\.[jt]sx?$/, /__tests__\//],
|
|
66
|
+
types: [/\.d\.ts$/, /types?\.[jt]s$/],
|
|
67
|
+
api: [/api\/.*\.[jt]s$/, /routes?\/.*\.[jt]s$/, /controllers?\/.*\.[jt]s$/],
|
|
68
|
+
component: [/components?\/.*\.[jt]sx?$/, /views?\/.*\.[jt]sx?$/],
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Directories to skip
|
|
72
|
+
const SKIP_DIRECTORIES = new Set([
|
|
73
|
+
'node_modules',
|
|
74
|
+
'.git',
|
|
75
|
+
'dist',
|
|
76
|
+
'build',
|
|
77
|
+
'out',
|
|
78
|
+
'.next',
|
|
79
|
+
'.nuxt',
|
|
80
|
+
'coverage',
|
|
81
|
+
'.vscode',
|
|
82
|
+
'.idea',
|
|
83
|
+
'__pycache__',
|
|
84
|
+
'venv',
|
|
85
|
+
'.env',
|
|
86
|
+
'vendor',
|
|
87
|
+
'target',
|
|
88
|
+
'bin',
|
|
89
|
+
'obj',
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
// File extensions to include
|
|
93
|
+
const INCLUDE_EXTENSIONS = new Set([
|
|
94
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
95
|
+
'.py', '.pyw',
|
|
96
|
+
'.java', '.kt', '.scala',
|
|
97
|
+
'.go',
|
|
98
|
+
'.rs',
|
|
99
|
+
'.c', '.cpp', '.h', '.hpp',
|
|
100
|
+
'.cs',
|
|
101
|
+
'.rb',
|
|
102
|
+
'.php',
|
|
103
|
+
'.swift',
|
|
104
|
+
'.vue', '.svelte',
|
|
105
|
+
'.html', '.css', '.scss', '.less',
|
|
106
|
+
'.json', '.yaml', '.yml', '.toml',
|
|
107
|
+
'.md', '.mdx',
|
|
108
|
+
'.sql',
|
|
109
|
+
'.sh', '.bash', '.zsh',
|
|
110
|
+
'.dockerfile',
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
// Language mapping
|
|
114
|
+
const EXTENSION_TO_LANGUAGE: Record<string, string> = {
|
|
115
|
+
'.ts': 'TypeScript',
|
|
116
|
+
'.tsx': 'TypeScript (React)',
|
|
117
|
+
'.js': 'JavaScript',
|
|
118
|
+
'.jsx': 'JavaScript (React)',
|
|
119
|
+
'.py': 'Python',
|
|
120
|
+
'.java': 'Java',
|
|
121
|
+
'.go': 'Go',
|
|
122
|
+
'.rs': 'Rust',
|
|
123
|
+
'.c': 'C',
|
|
124
|
+
'.cpp': 'C++',
|
|
125
|
+
'.cs': 'C#',
|
|
126
|
+
'.rb': 'Ruby',
|
|
127
|
+
'.php': 'PHP',
|
|
128
|
+
'.swift': 'Swift',
|
|
129
|
+
'.kt': 'Kotlin',
|
|
130
|
+
'.vue': 'Vue',
|
|
131
|
+
'.svelte': 'Svelte',
|
|
132
|
+
'.html': 'HTML',
|
|
133
|
+
'.css': 'CSS',
|
|
134
|
+
'.scss': 'SCSS',
|
|
135
|
+
'.json': 'JSON',
|
|
136
|
+
'.yaml': 'YAML',
|
|
137
|
+
'.yml': 'YAML',
|
|
138
|
+
'.md': 'Markdown',
|
|
139
|
+
'.sql': 'SQL',
|
|
140
|
+
'.sh': 'Shell',
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export class WorkspaceIndexer {
|
|
144
|
+
private context: vscode.ExtensionContext;
|
|
145
|
+
private statusBarItem: vscode.StatusBarItem;
|
|
146
|
+
private isIndexing = false;
|
|
147
|
+
|
|
148
|
+
constructor(context: vscode.ExtensionContext) {
|
|
149
|
+
this.context = context;
|
|
150
|
+
this.statusBarItem = vscode.window.createStatusBarItem(
|
|
151
|
+
vscode.StatusBarAlignment.Left,
|
|
152
|
+
50
|
|
153
|
+
);
|
|
154
|
+
this.statusBarItem.command = 'promptarchitect.indexWorkspace';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get the current workspace index
|
|
159
|
+
*/
|
|
160
|
+
getIndex(): WorkspaceIndex | undefined {
|
|
161
|
+
return this.context.workspaceState.get<WorkspaceIndex>('workspaceIndex');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Check if index exists and is recent (less than 1 hour old)
|
|
166
|
+
*/
|
|
167
|
+
hasValidIndex(): boolean {
|
|
168
|
+
const index = this.getIndex();
|
|
169
|
+
if (!index) return false;
|
|
170
|
+
const oneHourAgo = Date.now() - 60 * 60 * 1000;
|
|
171
|
+
return index.timestamp > oneHourAgo;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get a compact context string for prompt refinement
|
|
176
|
+
*/
|
|
177
|
+
getContextForPrompt(): string {
|
|
178
|
+
const index = this.getIndex();
|
|
179
|
+
if (!index) {
|
|
180
|
+
return '';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const parts: string[] = [];
|
|
184
|
+
|
|
185
|
+
parts.push(`## Workspace Context: ${index.workspaceName}`);
|
|
186
|
+
parts.push('');
|
|
187
|
+
parts.push(`**Project Summary:** ${index.summary}`);
|
|
188
|
+
parts.push('');
|
|
189
|
+
|
|
190
|
+
// Languages
|
|
191
|
+
if (index.languages.length > 0) {
|
|
192
|
+
parts.push('**Tech Stack:**');
|
|
193
|
+
const topLanguages = index.languages.slice(0, 5);
|
|
194
|
+
parts.push(topLanguages.map(l => `- ${l.language} (${l.percentage.toFixed(1)}%)`).join('\n'));
|
|
195
|
+
parts.push('');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Key files summary
|
|
199
|
+
if (index.keyFiles.length > 0) {
|
|
200
|
+
parts.push('**Key Files:**');
|
|
201
|
+
const keyFilesSummary = index.keyFiles.slice(0, 10).map(f => `- ${f.path}: ${f.summary}`);
|
|
202
|
+
parts.push(keyFilesSummary.join('\n'));
|
|
203
|
+
parts.push('');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Project structure (top-level only)
|
|
207
|
+
const topLevelDirs = index.structure
|
|
208
|
+
.filter(f => f.type === 'directory' && !f.path.includes('/'))
|
|
209
|
+
.map(f => f.path);
|
|
210
|
+
|
|
211
|
+
if (topLevelDirs.length > 0) {
|
|
212
|
+
parts.push('**Project Structure:**');
|
|
213
|
+
parts.push('```');
|
|
214
|
+
parts.push(topLevelDirs.map(d => `├── ${d}/`).join('\n'));
|
|
215
|
+
parts.push('```');
|
|
216
|
+
parts.push('');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Detected patterns
|
|
220
|
+
if (index.patterns.length > 0) {
|
|
221
|
+
parts.push('**Detected Patterns:**');
|
|
222
|
+
parts.push(index.patterns.map(p => `- ${p}`).join('\n'));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return parts.join('\n');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Automatically index workspace if not already indexed (no user prompt)
|
|
230
|
+
* This runs silently in the background for autonomous operation
|
|
231
|
+
*/
|
|
232
|
+
async ensureIndexed(): Promise<boolean> {
|
|
233
|
+
// If already indexed and valid, return immediately
|
|
234
|
+
if (this.hasValidIndex()) {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// If not indexed, automatically index without user interaction
|
|
239
|
+
const index = await this.indexWorkspaceSilent();
|
|
240
|
+
return index !== undefined;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Show prompt to index workspace (legacy method for manual triggering)
|
|
245
|
+
*/
|
|
246
|
+
async promptToIndex(): Promise<boolean> {
|
|
247
|
+
const index = this.getIndex();
|
|
248
|
+
|
|
249
|
+
let message: string;
|
|
250
|
+
let yesButton: string;
|
|
251
|
+
|
|
252
|
+
if (!index) {
|
|
253
|
+
message = 'Index your workspace for smarter prompt refinement? This scans your project structure to provide better context.';
|
|
254
|
+
yesButton = 'Index Workspace';
|
|
255
|
+
} else {
|
|
256
|
+
const age = Math.round((Date.now() - index.timestamp) / (1000 * 60));
|
|
257
|
+
message = `Workspace was indexed ${age} minutes ago. Re-index for updated context?`;
|
|
258
|
+
yesButton = 'Re-index';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const choice = await vscode.window.showInformationMessage(
|
|
262
|
+
message,
|
|
263
|
+
{ modal: false },
|
|
264
|
+
yesButton,
|
|
265
|
+
'Not Now'
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
if (choice === yesButton) {
|
|
269
|
+
await this.indexWorkspace();
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Index the workspace silently (no UI notifications)
|
|
278
|
+
* Used for autonomous background indexing
|
|
279
|
+
*/
|
|
280
|
+
async indexWorkspaceSilent(): Promise<WorkspaceIndex | undefined> {
|
|
281
|
+
const workspaceFolders = vscode.workspace.workspaceFolders;
|
|
282
|
+
if (!workspaceFolders || workspaceFolders.length === 0) {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (this.isIndexing) {
|
|
287
|
+
// Wait for current indexing to complete
|
|
288
|
+
return new Promise((resolve) => {
|
|
289
|
+
const checkInterval = setInterval(() => {
|
|
290
|
+
if (!this.isIndexing) {
|
|
291
|
+
clearInterval(checkInterval);
|
|
292
|
+
resolve(this.getIndex());
|
|
293
|
+
}
|
|
294
|
+
}, 100);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return this.performIndexing(workspaceFolders[0], false);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Index the workspace with progress UI
|
|
303
|
+
*/
|
|
304
|
+
async indexWorkspace(): Promise<WorkspaceIndex | undefined> {
|
|
305
|
+
const workspaceFolders = vscode.workspace.workspaceFolders;
|
|
306
|
+
if (!workspaceFolders || workspaceFolders.length === 0) {
|
|
307
|
+
vscode.window.showWarningMessage('No workspace folder open');
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (this.isIndexing) {
|
|
312
|
+
vscode.window.showInformationMessage('Indexing already in progress...');
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return this.performIndexing(workspaceFolders[0], true);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Core indexing logic
|
|
321
|
+
*/
|
|
322
|
+
private async performIndexing(
|
|
323
|
+
workspaceFolder: vscode.WorkspaceFolder,
|
|
324
|
+
showProgress: boolean
|
|
325
|
+
): Promise<WorkspaceIndex | undefined> {
|
|
326
|
+
if (this.isIndexing) {
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
this.isIndexing = true;
|
|
331
|
+
const workspacePath = workspaceFolder.uri.fsPath;
|
|
332
|
+
const workspaceName = workspaceFolder.name;
|
|
333
|
+
|
|
334
|
+
const indexingTask = async (
|
|
335
|
+
progress?: vscode.Progress<{ message?: string; increment?: number }>,
|
|
336
|
+
token?: vscode.CancellationToken
|
|
337
|
+
): Promise<WorkspaceIndex | undefined> => {
|
|
338
|
+
try {
|
|
339
|
+
progress?.report({ message: 'Scanning files...', increment: 0 });
|
|
340
|
+
|
|
341
|
+
const structure: FileStructure[] = [];
|
|
342
|
+
const keyFiles: KeyFile[] = [];
|
|
343
|
+
const languageMap = new Map<string, { fileCount: number; lineCount: number }>();
|
|
344
|
+
let totalFiles = 0;
|
|
345
|
+
let totalLines = 0;
|
|
346
|
+
|
|
347
|
+
// Create a dummy token if none provided (for silent mode)
|
|
348
|
+
const cancellationToken = token || { isCancellationRequested: false } as vscode.CancellationToken;
|
|
349
|
+
const dummyProgress = { report: () => {} };
|
|
350
|
+
|
|
351
|
+
// Scan workspace
|
|
352
|
+
const files = await this.scanDirectory(
|
|
353
|
+
workspacePath,
|
|
354
|
+
'',
|
|
355
|
+
progress || dummyProgress as any,
|
|
356
|
+
cancellationToken
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
if (token?.isCancellationRequested) {
|
|
360
|
+
this.isIndexing = false;
|
|
361
|
+
return undefined;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
progress?.report({ message: 'Analyzing files...', increment: 50 });
|
|
365
|
+
|
|
366
|
+
for (const file of files) {
|
|
367
|
+
if (token?.isCancellationRequested) break;
|
|
368
|
+
|
|
369
|
+
const ext = path.extname(file.path).toLowerCase();
|
|
370
|
+
const language = EXTENSION_TO_LANGUAGE[ext] || 'Other';
|
|
371
|
+
|
|
372
|
+
// Update language stats
|
|
373
|
+
const stats = languageMap.get(language) || { fileCount: 0, lineCount: 0 };
|
|
374
|
+
stats.fileCount++;
|
|
375
|
+
stats.lineCount += file.lineCount || 0;
|
|
376
|
+
languageMap.set(language, stats);
|
|
377
|
+
|
|
378
|
+
totalFiles++;
|
|
379
|
+
totalLines += file.lineCount || 0;
|
|
380
|
+
|
|
381
|
+
// Check if it's a key file
|
|
382
|
+
const keyFileType = this.identifyKeyFile(file.path);
|
|
383
|
+
if (keyFileType) {
|
|
384
|
+
keyFiles.push({
|
|
385
|
+
path: file.path,
|
|
386
|
+
type: keyFileType,
|
|
387
|
+
summary: await this.summarizeKeyFile(workspacePath, file.path, keyFileType),
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
structure.push(file);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
progress?.report({ message: 'Building index...', increment: 80 });
|
|
395
|
+
|
|
396
|
+
// Calculate language percentages
|
|
397
|
+
const languages: LanguageStats[] = Array.from(languageMap.entries())
|
|
398
|
+
.map(([language, stats]) => ({
|
|
399
|
+
language,
|
|
400
|
+
fileCount: stats.fileCount,
|
|
401
|
+
lineCount: stats.lineCount,
|
|
402
|
+
percentage: (stats.lineCount / Math.max(totalLines, 1)) * 100,
|
|
403
|
+
}))
|
|
404
|
+
.sort((a, b) => b.lineCount - a.lineCount);
|
|
405
|
+
|
|
406
|
+
// Detect patterns
|
|
407
|
+
const patterns = this.detectPatterns(structure, keyFiles);
|
|
408
|
+
|
|
409
|
+
// Generate summary
|
|
410
|
+
const summary = this.generateSummary(workspaceName, languages, keyFiles, patterns);
|
|
411
|
+
|
|
412
|
+
const index: WorkspaceIndex = {
|
|
413
|
+
timestamp: Date.now(),
|
|
414
|
+
workspaceName,
|
|
415
|
+
workspacePath,
|
|
416
|
+
summary,
|
|
417
|
+
structure,
|
|
418
|
+
languages,
|
|
419
|
+
keyFiles,
|
|
420
|
+
patterns,
|
|
421
|
+
totalFiles,
|
|
422
|
+
totalLines,
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// Save to workspace state
|
|
426
|
+
await this.context.workspaceState.update('workspaceIndex', index);
|
|
427
|
+
|
|
428
|
+
progress?.report({ message: 'Complete!', increment: 100 });
|
|
429
|
+
|
|
430
|
+
this.updateStatusBar(index);
|
|
431
|
+
|
|
432
|
+
if (showProgress) {
|
|
433
|
+
vscode.window.showInformationMessage(
|
|
434
|
+
`✅ Workspace indexed: ${totalFiles} files, ${languages.length} languages detected`
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
this.isIndexing = false;
|
|
439
|
+
return index;
|
|
440
|
+
} catch (error) {
|
|
441
|
+
this.isIndexing = false;
|
|
442
|
+
if (showProgress) {
|
|
443
|
+
vscode.window.showErrorMessage(`Indexing failed: ${error}`);
|
|
444
|
+
}
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
// Run with or without progress UI
|
|
450
|
+
if (showProgress) {
|
|
451
|
+
return vscode.window.withProgress(
|
|
452
|
+
{
|
|
453
|
+
location: vscode.ProgressLocation.Notification,
|
|
454
|
+
title: 'Indexing workspace',
|
|
455
|
+
cancellable: true,
|
|
456
|
+
},
|
|
457
|
+
indexingTask
|
|
458
|
+
);
|
|
459
|
+
} else {
|
|
460
|
+
// Silent mode - no progress UI
|
|
461
|
+
return indexingTask();
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Scan a directory recursively
|
|
467
|
+
*/
|
|
468
|
+
private async scanDirectory(
|
|
469
|
+
basePath: string,
|
|
470
|
+
relativePath: string,
|
|
471
|
+
progress: vscode.Progress<{ message?: string; increment?: number }>,
|
|
472
|
+
token: vscode.CancellationToken
|
|
473
|
+
): Promise<FileStructure[]> {
|
|
474
|
+
const results: FileStructure[] = [];
|
|
475
|
+
const fullPath = path.join(basePath, relativePath);
|
|
476
|
+
|
|
477
|
+
try {
|
|
478
|
+
const entries = await vscode.workspace.fs.readDirectory(vscode.Uri.file(fullPath));
|
|
479
|
+
|
|
480
|
+
for (const [name, type] of entries) {
|
|
481
|
+
if (token.isCancellationRequested) break;
|
|
482
|
+
|
|
483
|
+
const entryRelativePath = relativePath ? `${relativePath}/${name}` : name;
|
|
484
|
+
|
|
485
|
+
if (type === vscode.FileType.Directory) {
|
|
486
|
+
// Skip ignored directories
|
|
487
|
+
if (SKIP_DIRECTORIES.has(name)) continue;
|
|
488
|
+
|
|
489
|
+
results.push({
|
|
490
|
+
path: entryRelativePath,
|
|
491
|
+
type: 'directory',
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// Recurse into subdirectory (limit depth to 5)
|
|
495
|
+
const depth = entryRelativePath.split('/').length;
|
|
496
|
+
if (depth < 5) {
|
|
497
|
+
const subResults = await this.scanDirectory(basePath, entryRelativePath, progress, token);
|
|
498
|
+
results.push(...subResults);
|
|
499
|
+
}
|
|
500
|
+
} else if (type === vscode.FileType.File) {
|
|
501
|
+
const ext = path.extname(name).toLowerCase();
|
|
502
|
+
|
|
503
|
+
// Only include relevant file types
|
|
504
|
+
if (!INCLUDE_EXTENSIONS.has(ext) && !name.startsWith('.')) continue;
|
|
505
|
+
|
|
506
|
+
// Get line count (approximate for large files)
|
|
507
|
+
let lineCount = 0;
|
|
508
|
+
try {
|
|
509
|
+
const uri = vscode.Uri.file(path.join(fullPath, name));
|
|
510
|
+
const stat = await vscode.workspace.fs.stat(uri);
|
|
511
|
+
|
|
512
|
+
// Only read small files for line count
|
|
513
|
+
if (stat.size < 500000) { // 500KB limit
|
|
514
|
+
const content = await vscode.workspace.fs.readFile(uri);
|
|
515
|
+
lineCount = Buffer.from(content).toString('utf-8').split('\n').length;
|
|
516
|
+
} else {
|
|
517
|
+
// Estimate for large files
|
|
518
|
+
lineCount = Math.round(stat.size / 50); // ~50 bytes per line estimate
|
|
519
|
+
}
|
|
520
|
+
} catch {
|
|
521
|
+
// Ignore read errors
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
results.push({
|
|
525
|
+
path: entryRelativePath,
|
|
526
|
+
type: 'file',
|
|
527
|
+
language: EXTENSION_TO_LANGUAGE[ext],
|
|
528
|
+
lineCount,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
} catch {
|
|
533
|
+
// Ignore directory read errors
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return results;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Identify if a file is a key file
|
|
541
|
+
*/
|
|
542
|
+
private identifyKeyFile(filePath: string): KeyFile['type'] | null {
|
|
543
|
+
const fileName = path.basename(filePath).toLowerCase();
|
|
544
|
+
|
|
545
|
+
for (const [type, patterns] of Object.entries(KEY_FILE_PATTERNS)) {
|
|
546
|
+
for (const pattern of patterns) {
|
|
547
|
+
if (pattern.test(fileName) || pattern.test(filePath)) {
|
|
548
|
+
return type as KeyFile['type'];
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Generate a brief summary of a key file
|
|
558
|
+
*/
|
|
559
|
+
private async summarizeKeyFile(
|
|
560
|
+
basePath: string,
|
|
561
|
+
relativePath: string,
|
|
562
|
+
type: KeyFile['type']
|
|
563
|
+
): Promise<string> {
|
|
564
|
+
const fullPath = path.join(basePath, relativePath);
|
|
565
|
+
|
|
566
|
+
try {
|
|
567
|
+
const uri = vscode.Uri.file(fullPath);
|
|
568
|
+
const stat = await vscode.workspace.fs.stat(uri);
|
|
569
|
+
|
|
570
|
+
// Only read small files
|
|
571
|
+
if (stat.size > 100000) {
|
|
572
|
+
return `Large ${type} file`;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const content = await vscode.workspace.fs.readFile(uri);
|
|
576
|
+
const text = Buffer.from(content).toString('utf-8');
|
|
577
|
+
|
|
578
|
+
switch (type) {
|
|
579
|
+
case 'config':
|
|
580
|
+
return this.summarizeConfig(relativePath, text);
|
|
581
|
+
case 'readme':
|
|
582
|
+
return this.summarizeReadme(text);
|
|
583
|
+
case 'entry':
|
|
584
|
+
return 'Application entry point';
|
|
585
|
+
case 'test':
|
|
586
|
+
return 'Test file';
|
|
587
|
+
case 'types':
|
|
588
|
+
return 'Type definitions';
|
|
589
|
+
case 'api':
|
|
590
|
+
return 'API endpoint/route';
|
|
591
|
+
case 'component':
|
|
592
|
+
return 'UI component';
|
|
593
|
+
default:
|
|
594
|
+
return type;
|
|
595
|
+
}
|
|
596
|
+
} catch {
|
|
597
|
+
return type;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
private summarizeConfig(fileName: string, content: string): string {
|
|
602
|
+
try {
|
|
603
|
+
if (fileName.endsWith('package.json')) {
|
|
604
|
+
const pkg = JSON.parse(content);
|
|
605
|
+
const deps = Object.keys(pkg.dependencies || {}).length;
|
|
606
|
+
const devDeps = Object.keys(pkg.devDependencies || {}).length;
|
|
607
|
+
return `${pkg.name || 'npm package'} - ${deps} deps, ${devDeps} devDeps`;
|
|
608
|
+
}
|
|
609
|
+
if (fileName.includes('tsconfig')) {
|
|
610
|
+
return 'TypeScript configuration';
|
|
611
|
+
}
|
|
612
|
+
if (fileName.includes('dockerfile')) {
|
|
613
|
+
return 'Docker container config';
|
|
614
|
+
}
|
|
615
|
+
} catch {
|
|
616
|
+
// Ignore parse errors
|
|
617
|
+
}
|
|
618
|
+
return 'Configuration file';
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
private summarizeReadme(content: string): string {
|
|
622
|
+
// Get first line that's not empty or a header marker
|
|
623
|
+
const lines = content.split('\n');
|
|
624
|
+
for (const line of lines) {
|
|
625
|
+
const trimmed = line.trim();
|
|
626
|
+
if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('!')) {
|
|
627
|
+
return trimmed.slice(0, 100) + (trimmed.length > 100 ? '...' : '');
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return 'Project documentation';
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Detect project patterns
|
|
635
|
+
*/
|
|
636
|
+
private detectPatterns(structure: FileStructure[], keyFiles: KeyFile[]): string[] {
|
|
637
|
+
const patterns: string[] = [];
|
|
638
|
+
const dirs = new Set(structure.filter(f => f.type === 'directory').map(f => f.path.split('/')[0]));
|
|
639
|
+
const files = structure.filter(f => f.type === 'file').map(f => f.path);
|
|
640
|
+
|
|
641
|
+
// Framework detection
|
|
642
|
+
if (files.some(f => f.includes('next.config'))) patterns.push('Next.js framework');
|
|
643
|
+
if (files.some(f => f.includes('nuxt.config'))) patterns.push('Nuxt.js framework');
|
|
644
|
+
if (files.some(f => f.includes('vite.config'))) patterns.push('Vite build tool');
|
|
645
|
+
if (dirs.has('pages') && files.some(f => f.endsWith('.tsx') || f.endsWith('.jsx'))) {
|
|
646
|
+
patterns.push('Pages-based routing');
|
|
647
|
+
}
|
|
648
|
+
if (dirs.has('app') && files.some(f => f.includes('app/') && f.endsWith('page.tsx'))) {
|
|
649
|
+
patterns.push('App Router (Next.js 13+)');
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Structure patterns
|
|
653
|
+
if (dirs.has('src')) patterns.push('src/ directory structure');
|
|
654
|
+
if (dirs.has('components')) patterns.push('Component-based architecture');
|
|
655
|
+
if (dirs.has('api') || dirs.has('routes')) patterns.push('API routes');
|
|
656
|
+
if (dirs.has('hooks') || files.some(f => f.includes('/use') && f.endsWith('.ts'))) {
|
|
657
|
+
patterns.push('Custom React hooks');
|
|
658
|
+
}
|
|
659
|
+
if (dirs.has('services')) patterns.push('Service layer pattern');
|
|
660
|
+
if (dirs.has('utils') || dirs.has('helpers')) patterns.push('Utility functions');
|
|
661
|
+
if (dirs.has('contexts') || dirs.has('context')) patterns.push('React Context API');
|
|
662
|
+
if (dirs.has('store') || dirs.has('redux') || files.some(f => f.includes('store.'))) {
|
|
663
|
+
patterns.push('State management');
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Testing
|
|
667
|
+
if (dirs.has('__tests__') || dirs.has('tests') || files.some(f => f.includes('.test.') || f.includes('.spec.'))) {
|
|
668
|
+
patterns.push('Test suite present');
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// TypeScript
|
|
672
|
+
if (files.some(f => f.endsWith('.ts') || f.endsWith('.tsx'))) {
|
|
673
|
+
patterns.push('TypeScript');
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Styling
|
|
677
|
+
if (files.some(f => f.includes('tailwind'))) patterns.push('Tailwind CSS');
|
|
678
|
+
if (files.some(f => f.endsWith('.scss'))) patterns.push('SCSS styling');
|
|
679
|
+
if (files.some(f => f.endsWith('.module.css'))) patterns.push('CSS Modules');
|
|
680
|
+
|
|
681
|
+
// Backend
|
|
682
|
+
if (files.some(f => f.includes('firebase'))) patterns.push('Firebase integration');
|
|
683
|
+
if (files.some(f => f.includes('prisma'))) patterns.push('Prisma ORM');
|
|
684
|
+
if (dirs.has('functions')) patterns.push('Serverless functions');
|
|
685
|
+
|
|
686
|
+
return patterns;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Generate a summary of the workspace
|
|
691
|
+
*/
|
|
692
|
+
private generateSummary(
|
|
693
|
+
name: string,
|
|
694
|
+
languages: LanguageStats[],
|
|
695
|
+
keyFiles: KeyFile[],
|
|
696
|
+
patterns: string[]
|
|
697
|
+
): string {
|
|
698
|
+
const parts: string[] = [];
|
|
699
|
+
|
|
700
|
+
// Primary language
|
|
701
|
+
if (languages.length > 0) {
|
|
702
|
+
const primary = languages[0];
|
|
703
|
+
parts.push(`${primary.language} project`);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Framework
|
|
707
|
+
const framework = patterns.find(p => p.includes('framework') || p.includes('Next.js') || p.includes('Nuxt'));
|
|
708
|
+
if (framework) {
|
|
709
|
+
parts.push(`using ${framework.replace(' framework', '')}`);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Key patterns
|
|
713
|
+
const relevantPatterns = patterns
|
|
714
|
+
.filter(p => !p.includes('framework'))
|
|
715
|
+
.slice(0, 3);
|
|
716
|
+
if (relevantPatterns.length > 0) {
|
|
717
|
+
parts.push(`with ${relevantPatterns.join(', ')}`);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return parts.join(' ') || `${name} workspace`;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Update status bar with index info
|
|
725
|
+
*/
|
|
726
|
+
private updateStatusBar(index: WorkspaceIndex) {
|
|
727
|
+
const age = Math.round((Date.now() - index.timestamp) / (1000 * 60));
|
|
728
|
+
this.statusBarItem.text = `$(database) Indexed (${age}m ago)`;
|
|
729
|
+
this.statusBarItem.tooltip = `Workspace: ${index.workspaceName}\nFiles: ${index.totalFiles}\nLines: ${index.totalLines}\nClick to re-index`;
|
|
730
|
+
this.statusBarItem.show();
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Show status bar item
|
|
735
|
+
*/
|
|
736
|
+
showStatusBar() {
|
|
737
|
+
const index = this.getIndex();
|
|
738
|
+
if (index) {
|
|
739
|
+
this.updateStatusBar(index);
|
|
740
|
+
} else {
|
|
741
|
+
this.statusBarItem.text = '$(database) Not indexed';
|
|
742
|
+
this.statusBarItem.tooltip = 'Click to index workspace for better prompt context';
|
|
743
|
+
this.statusBarItem.show();
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Hide status bar item
|
|
749
|
+
*/
|
|
750
|
+
hideStatusBar() {
|
|
751
|
+
this.statusBarItem.hide();
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Clear the index
|
|
756
|
+
*/
|
|
757
|
+
async clearIndex() {
|
|
758
|
+
await this.context.workspaceState.update('workspaceIndex', undefined);
|
|
759
|
+
this.statusBarItem.text = '$(database) Not indexed';
|
|
760
|
+
this.statusBarItem.tooltip = 'Click to index workspace';
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
dispose() {
|
|
764
|
+
this.statusBarItem.dispose();
|
|
765
|
+
}
|
|
766
|
+
}
|