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.
@@ -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
+ }