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,1023 @@
1
+ /**
2
+ * AI Chat WebView Provider
3
+ *
4
+ * Provides the AI Chat panel as a webview in the sidebar.
5
+ * Integrates with the PromptArchitect API for chat functionality.
6
+ */
7
+
8
+ import * as vscode from 'vscode';
9
+ import { PromptArchitectAPI } from '../api';
10
+
11
+ interface ChatMessage {
12
+ id: string;
13
+ role: 'user' | 'assistant';
14
+ content: string;
15
+ timestamp: number;
16
+ refined?: boolean;
17
+ }
18
+
19
+ export class AIChatViewProvider implements vscode.WebviewViewProvider {
20
+ public static readonly viewType = 'promptarchitect.aiChat';
21
+ private _view?: vscode.WebviewView;
22
+ private chatHistory: ChatMessage[] = [];
23
+
24
+ constructor(
25
+ private readonly context: vscode.ExtensionContext,
26
+ private readonly api: PromptArchitectAPI
27
+ ) {}
28
+
29
+ public resolveWebviewView(
30
+ webviewView: vscode.WebviewView,
31
+ _context: vscode.WebviewViewResolveContext,
32
+ _token: vscode.CancellationToken
33
+ ): void {
34
+ this._view = webviewView;
35
+
36
+ webviewView.webview.options = {
37
+ enableScripts: true,
38
+ localResourceRoots: [this.context.extensionUri],
39
+ };
40
+
41
+ webviewView.webview.html = this.getHtmlContent();
42
+
43
+ // Handle messages from webview
44
+ webviewView.webview.onDidReceiveMessage(async (message) => {
45
+ switch (message.command) {
46
+ case 'sendMessage':
47
+ await this.handleSendMessage(message.text, message.refined);
48
+ break;
49
+ case 'refine':
50
+ await this.handleRefine(message.text, message.mode);
51
+ break;
52
+ case 'copyToClipboard':
53
+ await vscode.env.clipboard.writeText(message.text);
54
+ vscode.window.showInformationMessage('📋 Copied!');
55
+ break;
56
+ case 'insertInEditor':
57
+ await this.insertInEditor(message.text);
58
+ break;
59
+ case 'clearChat':
60
+ this.chatHistory = [];
61
+ await this.context.workspaceState.update('aiChatHistory', []);
62
+ this.syncHistory();
63
+ vscode.window.showInformationMessage('🗒️ Chat cleared');
64
+ break;
65
+ case 'addContext':
66
+ await this.addEditorContext();
67
+ break;
68
+ case 'attachFile':
69
+ await this.attachFile();
70
+ break;
71
+ }
72
+ });
73
+
74
+ // Load any existing chat history
75
+ this.chatHistory = this.context.workspaceState.get('aiChatHistory', []);
76
+ this.syncHistory();
77
+ }
78
+
79
+ private async attachFile(): Promise<void> {
80
+ const uris = await vscode.window.showOpenDialog({
81
+ canSelectMany: false,
82
+ openLabel: 'Attach File',
83
+ filters: {
84
+ 'All Files': ['*'],
85
+ 'Code': ['ts', 'js', 'py', 'java', 'cpp', 'c', 'go', 'rs', 'rb', 'php'],
86
+ 'Config': ['json', 'yaml', 'yml', 'toml', 'xml'],
87
+ 'Text': ['txt', 'md', 'markdown'],
88
+ },
89
+ });
90
+
91
+ if (uris && uris.length > 0) {
92
+ const uri = uris[0];
93
+ const document = await vscode.workspace.openTextDocument(uri);
94
+ const content = document.getText();
95
+ const fileName = uri.fsPath.split(/[\\/]/).pop() || 'file';
96
+ const language = document.languageId;
97
+
98
+ // Limit content size
99
+ const maxChars = 10000;
100
+ const truncated = content.length > maxChars
101
+ ? content.substring(0, maxChars) + '\n\n... [truncated]'
102
+ : content;
103
+
104
+ this._view?.webview.postMessage({
105
+ command: 'addFile',
106
+ fileName,
107
+ language,
108
+ content: truncated,
109
+ });
110
+ }
111
+ }
112
+
113
+ private async handleSendMessage(text: string, wasRefined: boolean = false): Promise<void> {
114
+ if (!text.trim()) return;
115
+
116
+ // Add user message
117
+ const userMessage: ChatMessage = {
118
+ id: this.generateId(),
119
+ role: 'user',
120
+ content: text,
121
+ timestamp: Date.now(),
122
+ refined: wasRefined,
123
+ };
124
+ this.chatHistory.push(userMessage);
125
+ this.syncHistory();
126
+
127
+ // Show loading
128
+ this._view?.webview.postMessage({ command: 'loading', loading: true });
129
+
130
+ try {
131
+ const response = await this.api.chat({
132
+ message: text,
133
+ history: this.chatHistory.slice(0, -1).map(m => ({
134
+ role: m.role,
135
+ content: m.content,
136
+ })),
137
+ });
138
+
139
+ // Add assistant response
140
+ const assistantMessage: ChatMessage = {
141
+ id: this.generateId(),
142
+ role: 'assistant',
143
+ content: response.reply,
144
+ timestamp: Date.now(),
145
+ };
146
+ this.chatHistory.push(assistantMessage);
147
+
148
+ // Save to workspace state
149
+ await this.context.workspaceState.update('aiChatHistory', this.chatHistory);
150
+
151
+ this.syncHistory();
152
+ } catch (error) {
153
+ const errorMessage: ChatMessage = {
154
+ id: this.generateId(),
155
+ role: 'assistant',
156
+ content: `⚠️ Error: ${error}. Please try again.`,
157
+ timestamp: Date.now(),
158
+ };
159
+ this.chatHistory.push(errorMessage);
160
+ this.syncHistory();
161
+ } finally {
162
+ this._view?.webview.postMessage({ command: 'loading', loading: false });
163
+ }
164
+ }
165
+
166
+ private async handleRefine(text: string, mode: string): Promise<void> {
167
+ if (!text.trim()) return;
168
+
169
+ this._view?.webview.postMessage({ command: 'refining', refining: true });
170
+
171
+ try {
172
+ const config = vscode.workspace.getConfiguration('promptarchitect');
173
+ const targetModel = config.get<string>('targetModel') || 'general';
174
+
175
+ const feedbackMap: Record<string, string> = {
176
+ clarity: 'Make this prompt clearer and more specific.',
177
+ detailed: 'Add more context and details.',
178
+ concise: 'Make this more concise.',
179
+ technical: 'Make this technically precise.',
180
+ };
181
+
182
+ const result = await this.api.refinePrompt({
183
+ prompt: text,
184
+ feedback: feedbackMap[mode] || feedbackMap.clarity,
185
+ targetModel,
186
+ });
187
+
188
+ this._view?.webview.postMessage({
189
+ command: 'refined',
190
+ text: result.refinedPrompt,
191
+ });
192
+ } catch (error) {
193
+ vscode.window.showErrorMessage(`Refinement failed: ${error}`);
194
+ } finally {
195
+ this._view?.webview.postMessage({ command: 'refining', refining: false });
196
+ }
197
+ }
198
+
199
+ private async insertInEditor(text: string): Promise<void> {
200
+ const editor = vscode.window.activeTextEditor;
201
+ if (editor) {
202
+ await editor.edit((editBuilder) => {
203
+ if (editor.selection.isEmpty) {
204
+ editBuilder.insert(editor.selection.active, text);
205
+ } else {
206
+ editBuilder.replace(editor.selection, text);
207
+ }
208
+ });
209
+ } else {
210
+ const doc = await vscode.workspace.openTextDocument({ content: text });
211
+ await vscode.window.showTextDocument(doc);
212
+ }
213
+ }
214
+
215
+ private async addEditorContext(): Promise<void> {
216
+ const editor = vscode.window.activeTextEditor;
217
+ if (!editor) {
218
+ vscode.window.showWarningMessage('No active editor');
219
+ return;
220
+ }
221
+
222
+ let contextText = '';
223
+ if (!editor.selection.isEmpty) {
224
+ contextText = editor.document.getText(editor.selection);
225
+ } else {
226
+ const visibleRanges = editor.visibleRanges;
227
+ if (visibleRanges.length > 0) {
228
+ contextText = editor.document.getText(visibleRanges[0]);
229
+ }
230
+ }
231
+
232
+ if (contextText) {
233
+ const fileName = editor.document.fileName.split(/[\\/]/).pop();
234
+ const language = editor.document.languageId;
235
+
236
+ this._view?.webview.postMessage({
237
+ command: 'addContext',
238
+ context: { fileName, language, code: contextText },
239
+ });
240
+ }
241
+ }
242
+
243
+ private syncHistory(): void {
244
+ this._view?.webview.postMessage({
245
+ command: 'syncHistory',
246
+ history: this.chatHistory,
247
+ });
248
+ }
249
+
250
+ private generateId(): string {
251
+ return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
252
+ }
253
+
254
+ public prefillPrompt(text: string): void {
255
+ this._view?.webview.postMessage({
256
+ command: 'prefill',
257
+ text,
258
+ });
259
+ }
260
+
261
+ private getHtmlContent(): string {
262
+ return /*html*/ `
263
+ <!DOCTYPE html>
264
+ <html lang="en">
265
+ <head>
266
+ <meta charset="UTF-8">
267
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
268
+ <style>
269
+ :root {
270
+ --radius-sm: 4px;
271
+ --radius-md: 8px;
272
+ --radius-lg: 12px;
273
+ }
274
+
275
+ * { box-sizing: border-box; margin: 0; padding: 0; }
276
+
277
+ html, body {
278
+ font-family: var(--vscode-font-family);
279
+ font-size: var(--vscode-font-size);
280
+ background: var(--vscode-sideBar-background);
281
+ color: var(--vscode-foreground);
282
+ height: 100%;
283
+ overflow: hidden;
284
+ }
285
+
286
+ body {
287
+ display: flex;
288
+ flex-direction: column;
289
+ }
290
+
291
+ /* Header toolbar */
292
+ .header {
293
+ flex-shrink: 0;
294
+ display: flex;
295
+ align-items: center;
296
+ justify-content: space-between;
297
+ padding: 8px 12px;
298
+ border-bottom: 1px solid var(--vscode-widget-border);
299
+ background: var(--vscode-sideBarSectionHeader-background);
300
+ }
301
+
302
+ .header-title {
303
+ font-size: 11px;
304
+ font-weight: 600;
305
+ text-transform: uppercase;
306
+ letter-spacing: 0.5px;
307
+ opacity: 0.8;
308
+ display: flex;
309
+ align-items: center;
310
+ gap: 6px;
311
+ }
312
+
313
+ .header-actions {
314
+ display: flex;
315
+ gap: 4px;
316
+ }
317
+
318
+ .icon-btn {
319
+ background: transparent;
320
+ border: none;
321
+ color: var(--vscode-foreground);
322
+ cursor: pointer;
323
+ padding: 4px;
324
+ border-radius: var(--radius-sm);
325
+ opacity: 0.7;
326
+ transition: all 0.2s;
327
+ display: flex;
328
+ align-items: center;
329
+ justify-content: center;
330
+ }
331
+
332
+ .icon-btn:hover {
333
+ opacity: 1;
334
+ background: var(--vscode-toolbar-hoverBackground);
335
+ }
336
+
337
+ /* Chat container */
338
+ .chat-container {
339
+ flex: 1;
340
+ overflow-y: auto;
341
+ padding: 12px;
342
+ display: flex;
343
+ flex-direction: column;
344
+ gap: 12px;
345
+ scroll-behavior: smooth;
346
+ }
347
+
348
+ .empty-state {
349
+ flex: 1;
350
+ display: flex;
351
+ flex-direction: column;
352
+ align-items: center;
353
+ justify-content: center;
354
+ text-align: center;
355
+ opacity: 0.8;
356
+ padding: 20px;
357
+ }
358
+
359
+ .empty-state .icon { font-size: 48px; margin-bottom: 16px; opacity: 0.5; }
360
+ .empty-state h3 { font-size: 16px; margin-bottom: 8px; font-weight: 600; }
361
+ .empty-state p { font-size: 13px; line-height: 1.5; color: var(--vscode-descriptionForeground); max-width: 240px; margin-bottom: 24px; }
362
+
363
+ .quick-actions {
364
+ display: flex;
365
+ flex-wrap: wrap;
366
+ gap: 8px;
367
+ justify-content: center;
368
+ max-width: 300px;
369
+ }
370
+
371
+ .quick-action {
372
+ padding: 6px 12px;
373
+ background: var(--vscode-button-secondaryBackground);
374
+ color: var(--vscode-button-secondaryForeground);
375
+ border: 1px solid transparent;
376
+ border-radius: 16px;
377
+ font-size: 11px;
378
+ cursor: pointer;
379
+ transition: all 0.2s;
380
+ }
381
+
382
+ .quick-action:hover {
383
+ background: var(--vscode-button-secondaryHoverBackground);
384
+ transform: translateY(-1px);
385
+ }
386
+
387
+ /* Messages */
388
+ .message {
389
+ display: flex;
390
+ flex-direction: column;
391
+ gap: 4px;
392
+ animation: slideIn 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
393
+ }
394
+
395
+ @keyframes slideIn {
396
+ from { opacity: 0; transform: translateY(10px); }
397
+ to { opacity: 1; transform: translateY(0); }
398
+ }
399
+
400
+ .message.user { align-items: flex-end; }
401
+ .message.assistant { align-items: flex-start; }
402
+
403
+ .bubble {
404
+ max-width: 90%;
405
+ padding: 10px 14px;
406
+ font-size: 13px;
407
+ line-height: 1.5;
408
+ position: relative;
409
+ }
410
+
411
+ .message.user .bubble {
412
+ background: var(--vscode-button-background);
413
+ color: var(--vscode-button-foreground);
414
+ border-radius: 16px 16px 4px 16px;
415
+ }
416
+
417
+ .message.assistant .bubble {
418
+ background: var(--vscode-editor-background);
419
+ border: 1px solid var(--vscode-widget-border);
420
+ border-radius: 16px 16px 16px 4px;
421
+ }
422
+
423
+ .bubble pre {
424
+ background: var(--vscode-textCodeBlock-background);
425
+ padding: 12px;
426
+ border-radius: 6px;
427
+ overflow-x: auto;
428
+ margin: 8px 0;
429
+ font-family: var(--vscode-editor-font-family);
430
+ font-size: 12px;
431
+ }
432
+
433
+ .bubble code {
434
+ background: rgba(0,0,0,0.1);
435
+ padding: 2px 4px;
436
+ border-radius: 4px;
437
+ font-family: var(--vscode-editor-font-family);
438
+ font-size: 12px;
439
+ }
440
+
441
+ .message.assistant .bubble code {
442
+ background: var(--vscode-textCodeBlock-background);
443
+ }
444
+
445
+ .message-meta {
446
+ font-size: 10px;
447
+ color: var(--vscode-descriptionForeground);
448
+ display: flex;
449
+ align-items: center;
450
+ gap: 8px;
451
+ padding: 0 4px;
452
+ min-height: 16px;
453
+ }
454
+
455
+ .msg-actions {
456
+ display: flex;
457
+ gap: 4px;
458
+ opacity: 0;
459
+ transition: opacity 0.2s;
460
+ }
461
+
462
+ .message:hover .msg-actions { opacity: 1; }
463
+
464
+ .msg-action {
465
+ background: transparent;
466
+ border: none;
467
+ color: var(--vscode-descriptionForeground);
468
+ cursor: pointer;
469
+ padding: 2px;
470
+ font-size: 14px;
471
+ }
472
+
473
+ .msg-action:hover { color: var(--vscode-foreground); }
474
+
475
+ /* FLOATING COMPOSER */
476
+ .composer {
477
+ flex-shrink: 0;
478
+ padding: 16px;
479
+ background: var(--vscode-sideBar-background);
480
+ border-top: 1px solid var(--vscode-widget-border);
481
+ display: flex;
482
+ flex-direction: column;
483
+ gap: 12px;
484
+ }
485
+
486
+ .input-container {
487
+ background: var(--vscode-input-background);
488
+ border: 1px solid var(--vscode-input-border);
489
+ border-radius: var(--radius-md);
490
+ padding: 0;
491
+ transition: border-color 0.2s, box-shadow 0.2s;
492
+ display: flex;
493
+ flex-direction: column;
494
+ }
495
+
496
+ .input-container:focus-within {
497
+ border-color: var(--vscode-focusBorder);
498
+ box-shadow: 0 0 0 1px var(--vscode-focusBorder);
499
+ }
500
+
501
+ .chat-input {
502
+ width: 100%;
503
+ min-height: 100px;
504
+ max-height: 300px;
505
+ padding: 14px 16px;
506
+ border: none;
507
+ background: transparent;
508
+ color: var(--vscode-input-foreground);
509
+ font-family: var(--vscode-font-family);
510
+ font-size: 13px;
511
+ line-height: 1.6;
512
+ resize: none;
513
+ }
514
+
515
+ .chat-input:focus { outline: none; }
516
+
517
+ .chat-input::placeholder {
518
+ color: var(--vscode-input-placeholderForeground);
519
+ }
520
+
521
+ /* Attachments Bar */
522
+ .attachments-bar {
523
+ padding: 8px 12px;
524
+ border-top: 1px solid rgba(128, 128, 128, 0.1);
525
+ display: flex;
526
+ flex-wrap: wrap;
527
+ gap: 8px;
528
+ align-items: center;
529
+ }
530
+
531
+ .attach-btn {
532
+ background: transparent;
533
+ border: none;
534
+ color: var(--vscode-descriptionForeground);
535
+ font-size: 11px;
536
+ cursor: pointer;
537
+ display: flex;
538
+ align-items: center;
539
+ gap: 4px;
540
+ padding: 4px 8px;
541
+ border-radius: 4px;
542
+ transition: all 0.2s;
543
+ }
544
+
545
+ .attach-btn:hover {
546
+ background: var(--vscode-toolbar-hoverBackground);
547
+ color: var(--vscode-foreground);
548
+ }
549
+
550
+ .attachment-chip {
551
+ background: var(--vscode-badge-background);
552
+ color: var(--vscode-badge-foreground);
553
+ font-size: 11px;
554
+ padding: 2px 8px;
555
+ border-radius: 12px;
556
+ display: flex;
557
+ align-items: center;
558
+ gap: 4px;
559
+ animation: popIn 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
560
+ }
561
+
562
+ @keyframes popIn {
563
+ from { transform: scale(0.8); opacity: 0; }
564
+ to { transform: scale(1); opacity: 1; }
565
+ }
566
+
567
+ .chip-remove {
568
+ background: none;
569
+ border: none;
570
+ color: inherit;
571
+ cursor: pointer;
572
+ opacity: 0.7;
573
+ padding: 0;
574
+ font-size: 12px;
575
+ display: flex;
576
+ }
577
+
578
+ .chip-remove:hover { opacity: 1; }
579
+
580
+ /* Polish Toolbar - COLLAPSIBLE */
581
+ .polish-toolbar {
582
+ overflow: hidden;
583
+ transition: all 0.3s ease;
584
+ background: var(--vscode-editor-background);
585
+ border: 1px solid var(--vscode-widget-border);
586
+ border-radius: var(--radius-md);
587
+ }
588
+
589
+ .polish-toolbar.collapsed .polish-content {
590
+ display: none;
591
+ }
592
+
593
+ .polish-header {
594
+ padding: 8px 12px;
595
+ display: flex;
596
+ align-items: center;
597
+ justify-content: space-between;
598
+ cursor: pointer;
599
+ font-size: 11px;
600
+ font-weight: 600;
601
+ user-select: none;
602
+ }
603
+
604
+ .polish-header:hover {
605
+ background: var(--vscode-toolbar-hoverBackground);
606
+ }
607
+
608
+ .polish-content {
609
+ padding: 8px 12px;
610
+ border-top: 1px solid var(--vscode-widget-border);
611
+ display: flex;
612
+ gap: 6px;
613
+ flex-wrap: wrap;
614
+ }
615
+
616
+ .refine-btn {
617
+ flex: 1;
618
+ min-width: 80px;
619
+ padding: 6px 10px;
620
+ background: var(--vscode-button-secondaryBackground);
621
+ color: var(--vscode-button-secondaryForeground);
622
+ border: 1px solid transparent;
623
+ border-radius: 4px;
624
+ font-size: 11px;
625
+ cursor: pointer;
626
+ display: flex;
627
+ align-items: center;
628
+ justify-content: center;
629
+ gap: 6px;
630
+ transition: all 0.2s;
631
+ }
632
+
633
+ .refine-btn:hover {
634
+ background: var(--vscode-button-secondaryHoverBackground);
635
+ }
636
+
637
+ .refine-btn.active {
638
+ background: var(--vscode-button-background);
639
+ color: var(--vscode-button-foreground);
640
+ }
641
+
642
+ /* Execute Button */
643
+ .execute-btn {
644
+ width: 100%;
645
+ padding: 12px;
646
+ background: var(--vscode-button-background);
647
+ color: var(--vscode-button-foreground);
648
+ border: none;
649
+ border-radius: var(--radius-md);
650
+ font-size: 13px;
651
+ font-weight: 600;
652
+ cursor: pointer;
653
+ transition: all 0.2s;
654
+ display: flex;
655
+ align-items: center;
656
+ justify-content: center;
657
+ gap: 8px;
658
+ }
659
+
660
+ .execute-btn:hover {
661
+ background: var(--vscode-button-hoverBackground);
662
+ transform: translateY(-1px);
663
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
664
+ }
665
+
666
+ .execute-btn:active {
667
+ transform: translateY(0);
668
+ }
669
+
670
+ .execute-btn:disabled {
671
+ opacity: 0.6;
672
+ cursor: not-allowed;
673
+ transform: none;
674
+ }
675
+
676
+ .keyboard-hints {
677
+ text-align: center;
678
+ font-size: 10px;
679
+ color: var(--vscode-descriptionForeground);
680
+ opacity: 0;
681
+ transition: opacity 0.3s;
682
+ margin-top: -4px;
683
+ }
684
+
685
+ .composer:focus-within .keyboard-hints {
686
+ opacity: 0.7;
687
+ }
688
+
689
+ .spinner {
690
+ width: 14px;
691
+ height: 14px;
692
+ border: 2px solid currentColor;
693
+ border-top-color: transparent;
694
+ border-radius: 50%;
695
+ animation: spin 0.8s linear infinite;
696
+ }
697
+
698
+ @keyframes spin { to { transform: rotate(360deg); } }
699
+
700
+ /* Narrow sidebar adaptations */
701
+ @media (max-width: 300px) {
702
+ .polish-content {
703
+ flex-direction: column;
704
+ }
705
+ .refine-btn {
706
+ width: 100%;
707
+ justify-content: flex-start;
708
+ }
709
+ }
710
+ </style>
711
+ </head>
712
+ <body>
713
+ <div class="header">
714
+ <div class="header-title">
715
+ <span>⚡ Architect</span>
716
+ </div>
717
+ <div class="header-actions">
718
+ <button class="icon-btn" id="newChatBtn" title="New Chat">🗒️</button>
719
+ <button class="icon-btn" id="exportBtn" title="Export Chat">📤</button>
720
+ </div>
721
+ </div>
722
+
723
+ <div class="chat-container" id="chatContainer">
724
+ <div class="empty-state" id="emptyState">
725
+ <div class="icon">⚡</div>
726
+ <h3>PromptArchitect</h3>
727
+ <p>Build, refine, and execute complex prompts with ease.</p>
728
+ <div class="quick-actions">
729
+ <button class="quick-action" data-prompt="Refactor this code to follow SOLID principles:">🔧 Refactor</button>
730
+ <button class="quick-action" data-prompt="Write unit tests for this file:">🧪 Test</button>
731
+ <button class="quick-action" data-prompt="Explain how this code works:">💡 Explain</button>
732
+ <button class="quick-action" data-prompt="Find potential bugs in:">🐛 Debug</button>
733
+ </div>
734
+ </div>
735
+ </div>
736
+
737
+ <div class="composer">
738
+ <!-- Polish Toolbar (Collapsed by default) -->
739
+ <div class="polish-toolbar collapsed" id="polishToolbar">
740
+ <div class="polish-header" id="polishToggle">
741
+ <span style="display: flex; align-items: center; gap: 6px;">✨ Polish Prompt <span id="refineStatus" style="font-weight: 400; color: var(--vscode-descriptionForeground); font-size: 10px;">(Optional)</span></span>
742
+ <span class="chevron" id="polishChevron">▼</span>
743
+ </div>
744
+ <div class="polish-content">
745
+ <button class="refine-btn" data-mode="clarity">✨ Clarity</button>
746
+ <button class="refine-btn" data-mode="detailed">📝 Detail</button>
747
+ <button class="refine-btn" data-mode="concise">✂️ Concise</button>
748
+ <button class="refine-btn" data-mode="technical">⚙️ Technical</button>
749
+ </div>
750
+ </div>
751
+
752
+ <!-- Main Input Area -->
753
+ <div class="input-container">
754
+ <textarea class="chat-input" id="chatInput" placeholder="Describe what you want to build..." rows="4"></textarea>
755
+
756
+ <!-- Attachments Bar -->
757
+ <div class="attachments-bar" id="attachmentsBar">
758
+ <button class="attach-btn" id="attachCodeBtn">📎 Code</button>
759
+ <button class="attach-btn" id="attachFileBtn">📄 File</button>
760
+ <!-- Dynamic chips go here -->
761
+ <div id="attachmentChips" style="display:contents"></div>
762
+ </div>
763
+ </div>
764
+
765
+ <!-- Execute Button -->
766
+ <button class="execute-btn" id="sendBtn">
767
+ <span class="btn-icon">⚡</span>
768
+ <span class="btn-text">Execute Prompt</span>
769
+ </button>
770
+
771
+ <div class="keyboard-hints">
772
+ Enter to send · Shift+Enter for new line
773
+ </div>
774
+ </div>
775
+
776
+ <script>
777
+ const vscode = acquireVsCodeApi();
778
+
779
+ // UI Elements
780
+ const chatContainer = document.getElementById('chatContainer');
781
+ const emptyState = document.getElementById('emptyState');
782
+ const input = document.getElementById('chatInput');
783
+ const sendBtn = document.getElementById('sendBtn');
784
+ const polishToolbar = document.getElementById('polishToolbar');
785
+ const polishToggle = document.getElementById('polishToggle');
786
+ const polishChevron = document.getElementById('polishChevron');
787
+ const refineBtns = document.querySelectorAll('.refine-btn');
788
+ const attachmentChips = document.getElementById('attachmentChips');
789
+ const refineStatus = document.getElementById('refineStatus');
790
+
791
+ // State
792
+ let isLoading = false;
793
+ let wasRefined = false;
794
+ let attachedContexts = [];
795
+ let chatHistory = [];
796
+
797
+ // --- Autosize Textarea ---
798
+ function resizeInput() {
799
+ input.style.height = 'auto';
800
+ input.style.height = Math.min(input.scrollHeight, 300) + 'px';
801
+ }
802
+ input.addEventListener('input', resizeInput);
803
+
804
+ // --- Polish Toolbar ---
805
+ polishToggle.addEventListener('click', () => {
806
+ const isCollapsed = polishToolbar.classList.toggle('collapsed');
807
+ polishChevron.textContent = isCollapsed ? '▼' : '▲';
808
+ });
809
+
810
+ refineBtns.forEach(btn => {
811
+ btn.addEventListener('click', () => {
812
+ if (!input.value.trim()) return;
813
+
814
+ // Visual feedback
815
+ refineBtns.forEach(b => b.classList.remove('active'));
816
+ btn.classList.add('active');
817
+ refineStatus.textContent = '(Refining...)';
818
+
819
+ vscode.postMessage({
820
+ command: 'refine',
821
+ text: input.value,
822
+ mode: btn.dataset.mode
823
+ });
824
+ });
825
+ });
826
+
827
+ // --- Attachments ---
828
+ document.getElementById('attachCodeBtn').addEventListener('click', () => {
829
+ vscode.postMessage({ command: 'addContext' });
830
+ });
831
+
832
+ document.getElementById('attachFileBtn').addEventListener('click', () => {
833
+ vscode.postMessage({ command: 'attachFile' });
834
+ });
835
+
836
+ function renderAttachments() {
837
+ attachmentChips.innerHTML = attachedContexts.map((ctx, i) => \`
838
+ <div class="attachment-chip">
839
+ <span>\${ctx.type === 'file' ? '📄' : '✂️'} \${ctx.name}</span>
840
+ <button class="chip-remove" onclick="removeAttachment(\${i})">✕</button>
841
+ </div>
842
+ \`).join('');
843
+ }
844
+
845
+ window.removeAttachment = (index) => {
846
+ attachedContexts.splice(index, 1);
847
+ renderAttachments();
848
+ };
849
+
850
+ // --- Quick Actions ---
851
+ document.querySelectorAll('.quick-action').forEach(btn => {
852
+ btn.addEventListener('click', () => {
853
+ input.value = btn.dataset.prompt + ' ';
854
+ input.focus();
855
+ resizeInput();
856
+ });
857
+ });
858
+
859
+ // --- Messaging ---
860
+ sendBtn.addEventListener('click', sendMessage);
861
+
862
+ input.addEventListener('keydown', (e) => {
863
+ if (e.key === 'Enter' && !e.shiftKey) {
864
+ e.preventDefault();
865
+ sendMessage();
866
+ }
867
+ });
868
+
869
+ function sendMessage() {
870
+ if (isLoading || !input.value.trim()) return;
871
+
872
+ let text = input.value;
873
+
874
+ // Combine context
875
+ if (attachedContexts.length > 0) {
876
+ const contextStr = attachedContexts.map(ctx =>
877
+ \`**\${ctx.type}: \${ctx.name}**\\n\\\`\\\`\\\`\${ctx.language || ''}\\n\${ctx.content}\\n\\\`\\\`\\\`\`
878
+ ).join('\\n\\n');
879
+ text = contextStr + '\\n\\n' + text;
880
+ }
881
+
882
+ vscode.postMessage({ command: 'sendMessage', text, refined: wasRefined });
883
+
884
+ // Reset state
885
+ input.value = '';
886
+ resizeInput();
887
+ wasRefined = false;
888
+ attachedContexts = [];
889
+ renderAttachments();
890
+ refineBtns.forEach(b => b.classList.remove('active'));
891
+ polishToolbar.classList.add('collapsed');
892
+ polishChevron.textContent = '▼';
893
+ refineStatus.textContent = '(Optional)';
894
+ }
895
+
896
+ // --- Message Rendering ---
897
+ function renderMessage(msg) {
898
+ const div = document.createElement('div');
899
+ div.className = \`message \${msg.role}\`;
900
+
901
+ let content = msg.content
902
+ .replace(/\\\`\\\`\\\`(\\w*)\\n([\\s\\S]*?)\\\`\\\`\\\`/g, '<pre><code>$2</code></pre>')
903
+ .replace(/\\\`([^\\\`]+)\\\`/g, '<code>$1</code>')
904
+ .replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>')
905
+ .replace(/\\n/g, '<br>');
906
+
907
+ const time = new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
908
+
909
+ div.innerHTML = \`
910
+ <div class="bubble">
911
+ \${content}
912
+ </div>
913
+ <div class="message-meta">
914
+ <span>\${time}</span>
915
+ \${msg.refined ? '<span>✨ refined</span>' : ''}
916
+ <div class="msg-actions">
917
+ <button class="msg-action" onclick="copyMsg('\${msg.id}')" title="Copy">📋</button>
918
+ \${msg.role === 'assistant' ? \`<button class="msg-action" onclick="insertMsg('\${msg.id}')" title="Insert">📥</button>\` : ''}
919
+ </div>
920
+ </div>
921
+ \`;
922
+ return div;
923
+ }
924
+
925
+ // --- Extension Communication ---
926
+ window.addEventListener('message', event => {
927
+ const msg = event.data;
928
+
929
+ switch (msg.command) {
930
+ case 'loading':
931
+ isLoading = msg.loading;
932
+ sendBtn.disabled = msg.loading;
933
+ const btnText = sendBtn.querySelector('.btn-text');
934
+ const btnIcon = sendBtn.querySelector('.btn-icon');
935
+
936
+ if (msg.loading) {
937
+ btnIcon.innerHTML = '<div class="spinner"></div>';
938
+ btnText.textContent = 'Thinking...';
939
+ } else {
940
+ btnIcon.textContent = '⚡';
941
+ btnText.textContent = 'Execute Prompt';
942
+ }
943
+ break;
944
+
945
+ case 'refined':
946
+ input.value = msg.text;
947
+ wasRefined = true;
948
+ resizeInput();
949
+ refineStatus.textContent = '✓ Refined';
950
+ break;
951
+
952
+ case 'refining':
953
+ refineBtns.forEach(b => b.disabled = msg.refining);
954
+ break;
955
+
956
+ case 'syncHistory':
957
+ chatHistory = msg.history;
958
+ chatContainer.innerHTML = '';
959
+ if (chatHistory.length === 0) {
960
+ chatContainer.appendChild(emptyState);
961
+ } else {
962
+ chatHistory.forEach(m => chatContainer.appendChild(renderMessage(m)));
963
+ }
964
+ chatContainer.scrollTop = chatContainer.scrollHeight;
965
+ break;
966
+
967
+ case 'addContext':
968
+ case 'addFile':
969
+ attachedContexts.push({
970
+ type: msg.command === 'addFile' ? 'file' : 'code',
971
+ name: msg.command === 'addFile' ? msg.fileName : msg.context.fileName,
972
+ language: msg.command === 'addFile' ? msg.language : msg.context.language,
973
+ content: msg.command === 'addFile' ? msg.content : msg.context.code
974
+ });
975
+ renderAttachments();
976
+ break;
977
+
978
+ case 'prefill':
979
+ input.value = msg.text;
980
+ input.focus();
981
+ resizeInput();
982
+ break;
983
+
984
+ case 'clearChat':
985
+ chatHistory = [];
986
+ chatContainer.innerHTML = '';
987
+ chatContainer.appendChild(emptyState);
988
+ break;
989
+ }
990
+ });
991
+
992
+ // Helper functions exposed to window
993
+ window.copyMsg = (id) => {
994
+ const msg = chatHistory.find(m => m.id === id);
995
+ if (msg) vscode.postMessage({ command: 'copyToClipboard', text: msg.content });
996
+ };
997
+
998
+ window.insertMsg = (id) => {
999
+ const msg = chatHistory.find(m => m.id === id);
1000
+ if (msg) vscode.postMessage({ command: 'insertInEditor', text: msg.content });
1001
+ };
1002
+
1003
+ // Header Actions
1004
+ document.getElementById('newChatBtn').addEventListener('click', () => {
1005
+ vscode.postMessage({ command: 'clearChat' });
1006
+ });
1007
+
1008
+ document.getElementById('exportBtn').addEventListener('click', () => {
1009
+ const exportText = chatHistory.map(m =>
1010
+ \`[\${m.role.toUpperCase()}]\\n\${m.content}\`
1011
+ ).join('\\n\\n---\\n\\n');
1012
+ vscode.postMessage({ command: 'copyToClipboard', text: exportText });
1013
+ });
1014
+
1015
+ // Init
1016
+ resizeInput();
1017
+ input.focus();
1018
+ </script>
1019
+ </body>
1020
+ </html>
1021
+ `;
1022
+ }
1023
+ }