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,979 @@
1
+ /**
2
+ * PromptArchitect Chat Panel
3
+ *
4
+ * A full-featured AI chat interface within VS Code.
5
+ * Users can refine prompts and chat with AI models directly.
6
+ */
7
+
8
+ import * as vscode from 'vscode';
9
+ import { PromptArchitectAPI } from './api';
10
+
11
+ interface ChatMessage {
12
+ id: string;
13
+ role: 'user' | 'assistant' | 'system';
14
+ content: string;
15
+ timestamp: number;
16
+ refined?: boolean;
17
+ }
18
+
19
+ export class ChatPanel {
20
+ public static currentPanel: ChatPanel | undefined;
21
+ private readonly panel: vscode.WebviewPanel;
22
+ private readonly api: PromptArchitectAPI;
23
+ private disposables: vscode.Disposable[] = [];
24
+ private chatHistory: ChatMessage[] = [];
25
+
26
+ private constructor(panel: vscode.WebviewPanel, api: PromptArchitectAPI) {
27
+ this.panel = panel;
28
+ this.api = api;
29
+
30
+ // Set initial HTML content
31
+ this.updateWebview();
32
+
33
+ // Handle messages from webview
34
+ this.panel.webview.onDidReceiveMessage(
35
+ async (message) => {
36
+ switch (message.command) {
37
+ case 'sendMessage':
38
+ await this.handleSendMessage(message.text, message.refined);
39
+ break;
40
+ case 'refineBeforeSend':
41
+ await this.handleRefine(message.text, message.mode);
42
+ break;
43
+ case 'copyToClipboard':
44
+ await vscode.env.clipboard.writeText(message.text);
45
+ vscode.window.showInformationMessage('📋 Copied to clipboard!');
46
+ break;
47
+ case 'insertInEditor':
48
+ await this.insertInActiveEditor(message.text);
49
+ break;
50
+ case 'clearChat':
51
+ this.chatHistory = [];
52
+ this.syncChatHistory();
53
+ break;
54
+ case 'addContext':
55
+ await this.addEditorContext();
56
+ break;
57
+ case 'exportChat':
58
+ await this.exportChatHistory();
59
+ break;
60
+ }
61
+ },
62
+ null,
63
+ this.disposables
64
+ );
65
+
66
+ // Clean up on panel close
67
+ this.panel.onDidDispose(() => this.dispose(), null, this.disposables);
68
+ }
69
+
70
+ public static createOrShow(api: PromptArchitectAPI) {
71
+ const column = vscode.ViewColumn.Beside;
72
+
73
+ // If panel exists, show it
74
+ if (ChatPanel.currentPanel) {
75
+ ChatPanel.currentPanel.panel.reveal(column);
76
+ return;
77
+ }
78
+
79
+ // Create new panel
80
+ const panel = vscode.window.createWebviewPanel(
81
+ 'promptArchitectChat',
82
+ '💬 PromptArchitect Chat',
83
+ column,
84
+ {
85
+ enableScripts: true,
86
+ retainContextWhenHidden: true,
87
+ }
88
+ );
89
+
90
+ ChatPanel.currentPanel = new ChatPanel(panel, api);
91
+ }
92
+
93
+ public static prefillPrompt(text: string) {
94
+ if (ChatPanel.currentPanel) {
95
+ ChatPanel.currentPanel.panel.webview.postMessage({
96
+ command: 'prefill',
97
+ text,
98
+ });
99
+ }
100
+ }
101
+
102
+ private async handleSendMessage(text: string, wasRefined: boolean = false) {
103
+ if (!text.trim()) return;
104
+
105
+ // Add user message to history
106
+ const userMessage: ChatMessage = {
107
+ id: this.generateId(),
108
+ role: 'user',
109
+ content: text,
110
+ timestamp: Date.now(),
111
+ refined: wasRefined,
112
+ };
113
+ this.chatHistory.push(userMessage);
114
+ this.syncChatHistory();
115
+
116
+ // Show loading state
117
+ this.panel.webview.postMessage({ command: 'loading', loading: true });
118
+
119
+ try {
120
+ // Call the chat API
121
+ const response = await this.api.chat({
122
+ message: text,
123
+ history: this.chatHistory.slice(0, -1).map(m => ({
124
+ role: m.role,
125
+ content: m.content,
126
+ })),
127
+ });
128
+
129
+ // Add assistant response to history
130
+ const assistantMessage: ChatMessage = {
131
+ id: this.generateId(),
132
+ role: 'assistant',
133
+ content: response.reply,
134
+ timestamp: Date.now(),
135
+ };
136
+ this.chatHistory.push(assistantMessage);
137
+ this.syncChatHistory();
138
+
139
+ } catch (error) {
140
+ // Add error message
141
+ const errorMessage: ChatMessage = {
142
+ id: this.generateId(),
143
+ role: 'assistant',
144
+ content: `⚠️ Error: ${error}. Please try again.`,
145
+ timestamp: Date.now(),
146
+ };
147
+ this.chatHistory.push(errorMessage);
148
+ this.syncChatHistory();
149
+ } finally {
150
+ this.panel.webview.postMessage({ command: 'loading', loading: false });
151
+ }
152
+ }
153
+
154
+ private async handleRefine(text: string, mode: string) {
155
+ if (!text.trim()) {
156
+ this.panel.webview.postMessage({
157
+ command: 'refineError',
158
+ message: 'Please enter some text to refine.',
159
+ });
160
+ return;
161
+ }
162
+
163
+ this.panel.webview.postMessage({ command: 'refining', refining: true });
164
+
165
+ try {
166
+ const config = vscode.workspace.getConfiguration('promptarchitect');
167
+ const targetModel = config.get<string>('targetModel') || 'general';
168
+
169
+ const feedbackMap: Record<string, string> = {
170
+ clarity: 'Make this prompt clearer and more specific. Improve structure and reduce ambiguity.',
171
+ detailed: 'Expand this prompt with more context, requirements, and specific details.',
172
+ concise: 'Make this prompt more concise while keeping the core intent. Remove redundancy.',
173
+ technical: 'Make this prompt more technically precise with proper terminology.',
174
+ structured: 'Add clear structure with sections, bullet points, or numbered steps.',
175
+ };
176
+
177
+ const result = await this.api.refinePrompt({
178
+ prompt: text,
179
+ feedback: feedbackMap[mode] || feedbackMap.clarity,
180
+ targetModel,
181
+ });
182
+
183
+ this.panel.webview.postMessage({
184
+ command: 'refined',
185
+ text: result.refinedPrompt,
186
+ });
187
+ } catch (error) {
188
+ this.panel.webview.postMessage({
189
+ command: 'refineError',
190
+ message: `Refinement failed: ${error}`,
191
+ });
192
+ } finally {
193
+ this.panel.webview.postMessage({ command: 'refining', refining: false });
194
+ }
195
+ }
196
+
197
+ private async insertInActiveEditor(text: string) {
198
+ const editor = vscode.window.activeTextEditor;
199
+ if (editor) {
200
+ await editor.edit((editBuilder) => {
201
+ if (editor.selection.isEmpty) {
202
+ editBuilder.insert(editor.selection.active, text);
203
+ } else {
204
+ editBuilder.replace(editor.selection, text);
205
+ }
206
+ });
207
+ vscode.window.showInformationMessage('✅ Inserted into editor!');
208
+ } else {
209
+ const doc = await vscode.workspace.openTextDocument({ content: text });
210
+ await vscode.window.showTextDocument(doc);
211
+ }
212
+ }
213
+
214
+ private async addEditorContext() {
215
+ const editor = vscode.window.activeTextEditor;
216
+ if (!editor) {
217
+ vscode.window.showWarningMessage('No active editor to get context from.');
218
+ return;
219
+ }
220
+
221
+ let contextText = '';
222
+ const selection = editor.selection;
223
+
224
+ if (!selection.isEmpty) {
225
+ contextText = editor.document.getText(selection);
226
+ } else {
227
+ // Get visible text or first 100 lines
228
+ const visibleRanges = editor.visibleRanges;
229
+ if (visibleRanges.length > 0) {
230
+ contextText = editor.document.getText(visibleRanges[0]);
231
+ }
232
+ }
233
+
234
+ if (contextText) {
235
+ const fileName = editor.document.fileName.split(/[\\/]/).pop();
236
+ const language = editor.document.languageId;
237
+
238
+ this.panel.webview.postMessage({
239
+ command: 'addContext',
240
+ context: {
241
+ fileName,
242
+ language,
243
+ code: contextText,
244
+ },
245
+ });
246
+ }
247
+ }
248
+
249
+ private async exportChatHistory() {
250
+ const content = this.chatHistory.map(msg => {
251
+ const time = new Date(msg.timestamp).toLocaleTimeString();
252
+ const role = msg.role === 'user' ? '👤 You' : '🤖 Assistant';
253
+ return `### ${role} (${time})${msg.refined ? ' [Refined]' : ''}\n\n${msg.content}\n`;
254
+ }).join('\n---\n\n');
255
+
256
+ const doc = await vscode.workspace.openTextDocument({
257
+ content: `# PromptArchitect Chat Export\n\n${content}`,
258
+ language: 'markdown',
259
+ });
260
+ await vscode.window.showTextDocument(doc);
261
+ }
262
+
263
+ private syncChatHistory() {
264
+ this.panel.webview.postMessage({
265
+ command: 'syncHistory',
266
+ history: this.chatHistory,
267
+ });
268
+ }
269
+
270
+ private generateId(): string {
271
+ return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
272
+ }
273
+
274
+ private updateWebview() {
275
+ this.panel.webview.html = this.getHtmlContent();
276
+ }
277
+
278
+ private getHtmlContent(): string {
279
+ return /*html*/ `
280
+ <!DOCTYPE html>
281
+ <html lang="en">
282
+ <head>
283
+ <meta charset="UTF-8">
284
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
285
+ <title>PromptArchitect Chat</title>
286
+ <style>
287
+ * {
288
+ box-sizing: border-box;
289
+ margin: 0;
290
+ padding: 0;
291
+ }
292
+
293
+ body {
294
+ font-family: var(--vscode-font-family);
295
+ background: var(--vscode-editor-background);
296
+ color: var(--vscode-editor-foreground);
297
+ height: 100vh;
298
+ display: flex;
299
+ flex-direction: column;
300
+ overflow: hidden;
301
+ }
302
+
303
+ /* Header */
304
+ .header {
305
+ display: flex;
306
+ align-items: center;
307
+ justify-content: space-between;
308
+ padding: 12px 16px;
309
+ border-bottom: 1px solid var(--vscode-widget-border);
310
+ background: var(--vscode-sideBar-background);
311
+ }
312
+
313
+ .header-left {
314
+ display: flex;
315
+ align-items: center;
316
+ gap: 8px;
317
+ }
318
+
319
+ .header h1 {
320
+ font-size: 14px;
321
+ font-weight: 600;
322
+ }
323
+
324
+ .header-actions {
325
+ display: flex;
326
+ gap: 4px;
327
+ }
328
+
329
+ .icon-btn {
330
+ background: transparent;
331
+ border: none;
332
+ color: var(--vscode-foreground);
333
+ cursor: pointer;
334
+ padding: 6px;
335
+ border-radius: 4px;
336
+ font-size: 14px;
337
+ opacity: 0.7;
338
+ transition: all 0.15s;
339
+ }
340
+
341
+ .icon-btn:hover {
342
+ opacity: 1;
343
+ background: var(--vscode-toolbar-hoverBackground);
344
+ }
345
+
346
+ .icon-btn[title]:hover::after {
347
+ content: attr(title);
348
+ }
349
+
350
+ /* Chat Area */
351
+ .chat-container {
352
+ flex: 1;
353
+ overflow-y: auto;
354
+ padding: 16px;
355
+ display: flex;
356
+ flex-direction: column;
357
+ gap: 16px;
358
+ }
359
+
360
+ .empty-state {
361
+ flex: 1;
362
+ display: flex;
363
+ flex-direction: column;
364
+ align-items: center;
365
+ justify-content: center;
366
+ text-align: center;
367
+ opacity: 0.7;
368
+ padding: 40px;
369
+ }
370
+
371
+ .empty-state .icon {
372
+ font-size: 48px;
373
+ margin-bottom: 16px;
374
+ }
375
+
376
+ .empty-state h2 {
377
+ font-size: 16px;
378
+ margin-bottom: 8px;
379
+ }
380
+
381
+ .empty-state p {
382
+ font-size: 12px;
383
+ color: var(--vscode-descriptionForeground);
384
+ }
385
+
386
+ .message {
387
+ display: flex;
388
+ gap: 12px;
389
+ animation: fadeIn 0.2s ease;
390
+ }
391
+
392
+ @keyframes fadeIn {
393
+ from { opacity: 0; transform: translateY(8px); }
394
+ to { opacity: 1; transform: translateY(0); }
395
+ }
396
+
397
+ .message.user {
398
+ flex-direction: row-reverse;
399
+ }
400
+
401
+ .avatar {
402
+ width: 32px;
403
+ height: 32px;
404
+ border-radius: 50%;
405
+ display: flex;
406
+ align-items: center;
407
+ justify-content: center;
408
+ font-size: 16px;
409
+ flex-shrink: 0;
410
+ }
411
+
412
+ .message.user .avatar {
413
+ background: var(--vscode-button-background);
414
+ }
415
+
416
+ .message.assistant .avatar {
417
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
418
+ }
419
+
420
+ .message-content {
421
+ max-width: 80%;
422
+ padding: 12px 16px;
423
+ border-radius: 12px;
424
+ font-size: 13px;
425
+ line-height: 1.5;
426
+ }
427
+
428
+ .message.user .message-content {
429
+ background: var(--vscode-button-background);
430
+ color: var(--vscode-button-foreground);
431
+ border-bottom-right-radius: 4px;
432
+ }
433
+
434
+ .message.assistant .message-content {
435
+ background: var(--vscode-input-background);
436
+ border: 1px solid var(--vscode-widget-border);
437
+ border-bottom-left-radius: 4px;
438
+ }
439
+
440
+ .message-content pre {
441
+ background: var(--vscode-textCodeBlock-background);
442
+ padding: 12px;
443
+ border-radius: 6px;
444
+ overflow-x: auto;
445
+ margin: 8px 0;
446
+ font-family: var(--vscode-editor-font-family);
447
+ font-size: 12px;
448
+ }
449
+
450
+ .message-content code {
451
+ background: var(--vscode-textCodeBlock-background);
452
+ padding: 2px 6px;
453
+ border-radius: 4px;
454
+ font-family: var(--vscode-editor-font-family);
455
+ font-size: 12px;
456
+ }
457
+
458
+ .message-meta {
459
+ font-size: 10px;
460
+ color: var(--vscode-descriptionForeground);
461
+ margin-top: 4px;
462
+ display: flex;
463
+ gap: 8px;
464
+ align-items: center;
465
+ }
466
+
467
+ .refined-badge {
468
+ background: var(--vscode-badge-background);
469
+ color: var(--vscode-badge-foreground);
470
+ padding: 2px 6px;
471
+ border-radius: 10px;
472
+ font-size: 9px;
473
+ }
474
+
475
+ .message-actions {
476
+ display: flex;
477
+ gap: 4px;
478
+ margin-top: 8px;
479
+ opacity: 0;
480
+ transition: opacity 0.15s;
481
+ }
482
+
483
+ .message:hover .message-actions {
484
+ opacity: 1;
485
+ }
486
+
487
+ .msg-action-btn {
488
+ background: var(--vscode-button-secondaryBackground);
489
+ color: var(--vscode-button-secondaryForeground);
490
+ border: none;
491
+ padding: 4px 8px;
492
+ border-radius: 4px;
493
+ font-size: 11px;
494
+ cursor: pointer;
495
+ display: flex;
496
+ align-items: center;
497
+ gap: 4px;
498
+ }
499
+
500
+ .msg-action-btn:hover {
501
+ background: var(--vscode-button-secondaryHoverBackground);
502
+ }
503
+
504
+ /* Input Area */
505
+ .input-area {
506
+ border-top: 1px solid var(--vscode-widget-border);
507
+ padding: 16px;
508
+ background: var(--vscode-sideBar-background);
509
+ }
510
+
511
+ .refine-bar {
512
+ display: flex;
513
+ gap: 6px;
514
+ margin-bottom: 12px;
515
+ flex-wrap: wrap;
516
+ align-items: center;
517
+ }
518
+
519
+ .refine-label {
520
+ font-size: 11px;
521
+ color: var(--vscode-descriptionForeground);
522
+ margin-right: 4px;
523
+ }
524
+
525
+ .refine-btn {
526
+ padding: 4px 10px;
527
+ border: 1px solid var(--vscode-button-secondaryBackground);
528
+ background: var(--vscode-button-secondaryBackground);
529
+ color: var(--vscode-button-secondaryForeground);
530
+ border-radius: 12px;
531
+ font-size: 10px;
532
+ cursor: pointer;
533
+ transition: all 0.15s;
534
+ }
535
+
536
+ .refine-btn:hover {
537
+ background: var(--vscode-button-secondaryHoverBackground);
538
+ }
539
+
540
+ .refine-btn.active {
541
+ background: var(--vscode-button-background);
542
+ color: var(--vscode-button-foreground);
543
+ border-color: var(--vscode-button-background);
544
+ }
545
+
546
+ .input-wrapper {
547
+ display: flex;
548
+ gap: 8px;
549
+ align-items: flex-end;
550
+ }
551
+
552
+ .input-container {
553
+ flex: 1;
554
+ position: relative;
555
+ }
556
+
557
+ .prompt-input {
558
+ width: 100%;
559
+ min-height: 60px;
560
+ max-height: 200px;
561
+ padding: 12px;
562
+ padding-right: 40px;
563
+ border: 1px solid var(--vscode-input-border);
564
+ background: var(--vscode-input-background);
565
+ color: var(--vscode-input-foreground);
566
+ border-radius: 8px;
567
+ font-family: var(--vscode-font-family);
568
+ font-size: 13px;
569
+ line-height: 1.5;
570
+ resize: none;
571
+ }
572
+
573
+ .prompt-input:focus {
574
+ outline: none;
575
+ border-color: var(--vscode-focusBorder);
576
+ }
577
+
578
+ .context-indicator {
579
+ position: absolute;
580
+ top: 8px;
581
+ right: 8px;
582
+ font-size: 16px;
583
+ opacity: 0.5;
584
+ }
585
+
586
+ .send-btn {
587
+ padding: 12px 20px;
588
+ background: var(--vscode-button-background);
589
+ color: var(--vscode-button-foreground);
590
+ border: none;
591
+ border-radius: 8px;
592
+ font-size: 13px;
593
+ font-weight: 500;
594
+ cursor: pointer;
595
+ display: flex;
596
+ align-items: center;
597
+ gap: 6px;
598
+ transition: all 0.15s;
599
+ }
600
+
601
+ .send-btn:hover {
602
+ background: var(--vscode-button-hoverBackground);
603
+ }
604
+
605
+ .send-btn:disabled {
606
+ opacity: 0.6;
607
+ cursor: not-allowed;
608
+ }
609
+
610
+ .spinner {
611
+ display: inline-block;
612
+ width: 14px;
613
+ height: 14px;
614
+ border: 2px solid currentColor;
615
+ border-top-color: transparent;
616
+ border-radius: 50%;
617
+ animation: spin 0.8s linear infinite;
618
+ }
619
+
620
+ @keyframes spin {
621
+ to { transform: rotate(360deg); }
622
+ }
623
+
624
+ .shortcuts {
625
+ display: flex;
626
+ gap: 16px;
627
+ margin-top: 8px;
628
+ font-size: 10px;
629
+ color: var(--vscode-descriptionForeground);
630
+ }
631
+
632
+ .shortcut {
633
+ display: flex;
634
+ align-items: center;
635
+ gap: 4px;
636
+ }
637
+
638
+ kbd {
639
+ background: var(--vscode-keybindingLabel-background);
640
+ border: 1px solid var(--vscode-keybindingLabel-border);
641
+ padding: 2px 6px;
642
+ border-radius: 4px;
643
+ font-family: var(--vscode-font-family);
644
+ font-size: 10px;
645
+ }
646
+
647
+ /* Loading indicator */
648
+ .typing-indicator {
649
+ display: none;
650
+ padding: 16px;
651
+ gap: 12px;
652
+ }
653
+
654
+ .typing-indicator.visible {
655
+ display: flex;
656
+ }
657
+
658
+ .typing-dots {
659
+ display: flex;
660
+ gap: 4px;
661
+ padding: 12px 16px;
662
+ background: var(--vscode-input-background);
663
+ border: 1px solid var(--vscode-widget-border);
664
+ border-radius: 12px;
665
+ }
666
+
667
+ .typing-dot {
668
+ width: 8px;
669
+ height: 8px;
670
+ background: var(--vscode-foreground);
671
+ border-radius: 50%;
672
+ opacity: 0.4;
673
+ animation: typingBounce 1.4s ease-in-out infinite;
674
+ }
675
+
676
+ .typing-dot:nth-child(2) { animation-delay: 0.2s; }
677
+ .typing-dot:nth-child(3) { animation-delay: 0.4s; }
678
+
679
+ @keyframes typingBounce {
680
+ 0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
681
+ 30% { transform: translateY(-8px); opacity: 1; }
682
+ }
683
+ </style>
684
+ </head>
685
+ <body>
686
+ <!-- Header -->
687
+ <div class="header">
688
+ <div class="header-left">
689
+ <span style="font-size: 18px;">✨</span>
690
+ <h1>PromptArchitect Chat</h1>
691
+ </div>
692
+ <div class="header-actions">
693
+ <button class="icon-btn" id="addContextBtn" title="Add editor context">📎</button>
694
+ <button class="icon-btn" id="exportBtn" title="Export chat">📤</button>
695
+ <button class="icon-btn" id="clearBtn" title="Clear chat">🗑️</button>
696
+ </div>
697
+ </div>
698
+
699
+ <!-- Chat Messages -->
700
+ <div class="chat-container" id="chatContainer">
701
+ <div class="empty-state" id="emptyState">
702
+ <div class="icon">💬</div>
703
+ <h2>Start a conversation</h2>
704
+ <p>Type a message below. Use the refine buttons to improve your prompt before sending.</p>
705
+ </div>
706
+ </div>
707
+
708
+ <!-- Typing Indicator -->
709
+ <div class="typing-indicator" id="typingIndicator">
710
+ <div class="avatar" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">🤖</div>
711
+ <div class="typing-dots">
712
+ <div class="typing-dot"></div>
713
+ <div class="typing-dot"></div>
714
+ <div class="typing-dot"></div>
715
+ </div>
716
+ </div>
717
+
718
+ <!-- Input Area -->
719
+ <div class="input-area">
720
+ <div class="refine-bar">
721
+ <span class="refine-label">✨ Refine first:</span>
722
+ <button class="refine-btn" data-mode="clarity">🎯 Clarity</button>
723
+ <button class="refine-btn" data-mode="detailed">📝 Detailed</button>
724
+ <button class="refine-btn" data-mode="concise">✂️ Concise</button>
725
+ <button class="refine-btn" data-mode="technical">⚙️ Technical</button>
726
+ <button class="refine-btn" data-mode="structured">📋 Structured</button>
727
+ </div>
728
+
729
+ <div class="input-wrapper">
730
+ <div class="input-container">
731
+ <textarea
732
+ class="prompt-input"
733
+ id="promptInput"
734
+ placeholder="Type your message... (Ctrl+Enter to send)"
735
+ rows="2"
736
+ ></textarea>
737
+ <span class="context-indicator" id="contextIndicator" style="display: none;">📎</span>
738
+ </div>
739
+ <button class="send-btn" id="sendBtn">
740
+ <span id="sendBtnIcon">➤</span>
741
+ <span id="sendBtnText">Send</span>
742
+ </button>
743
+ </div>
744
+
745
+ <div class="shortcuts">
746
+ <span class="shortcut"><kbd>Ctrl</kbd>+<kbd>Enter</kbd> Send</span>
747
+ <span class="shortcut"><kbd>Shift</kbd>+<kbd>Enter</kbd> New line</span>
748
+ </div>
749
+ </div>
750
+
751
+ <script>
752
+ const vscode = acquireVsCodeApi();
753
+ const chatContainer = document.getElementById('chatContainer');
754
+ const emptyState = document.getElementById('emptyState');
755
+ const typingIndicator = document.getElementById('typingIndicator');
756
+ const input = document.getElementById('promptInput');
757
+ const sendBtn = document.getElementById('sendBtn');
758
+ const sendBtnIcon = document.getElementById('sendBtnIcon');
759
+ const sendBtnText = document.getElementById('sendBtnText');
760
+ const refineBtns = document.querySelectorAll('.refine-btn');
761
+ const addContextBtn = document.getElementById('addContextBtn');
762
+ const exportBtn = document.getElementById('exportBtn');
763
+ const clearBtn = document.getElementById('clearBtn');
764
+ const contextIndicator = document.getElementById('contextIndicator');
765
+
766
+ let isLoading = false;
767
+ let isRefining = false;
768
+ let wasRefined = false;
769
+ let attachedContext = null;
770
+
771
+ // Auto-resize textarea
772
+ input.addEventListener('input', () => {
773
+ input.style.height = 'auto';
774
+ input.style.height = Math.min(input.scrollHeight, 200) + 'px';
775
+ });
776
+
777
+ // Refine buttons
778
+ refineBtns.forEach(btn => {
779
+ btn.addEventListener('click', () => {
780
+ if (isRefining || !input.value.trim()) return;
781
+ vscode.postMessage({
782
+ command: 'refineBeforeSend',
783
+ text: input.value,
784
+ mode: btn.dataset.mode
785
+ });
786
+ });
787
+ });
788
+
789
+ // Send button
790
+ sendBtn.addEventListener('click', sendMessage);
791
+
792
+ function sendMessage() {
793
+ if (isLoading || isRefining || !input.value.trim()) return;
794
+
795
+ let messageText = input.value;
796
+
797
+ // If context is attached, prepend it
798
+ if (attachedContext) {
799
+ messageText = \`**Context from \${attachedContext.fileName} (\${attachedContext.language}):**
800
+ \\\`\\\`\\\`\${attachedContext.language}
801
+ \${attachedContext.code}
802
+ \\\`\\\`\\\`
803
+
804
+ \${messageText}\`;
805
+ attachedContext = null;
806
+ contextIndicator.style.display = 'none';
807
+ }
808
+
809
+ vscode.postMessage({
810
+ command: 'sendMessage',
811
+ text: messageText,
812
+ refined: wasRefined
813
+ });
814
+
815
+ input.value = '';
816
+ input.style.height = 'auto';
817
+ wasRefined = false;
818
+ }
819
+
820
+ // Keyboard shortcuts
821
+ input.addEventListener('keydown', (e) => {
822
+ if (e.ctrlKey && e.key === 'Enter') {
823
+ e.preventDefault();
824
+ sendMessage();
825
+ }
826
+ });
827
+
828
+ // Header actions
829
+ addContextBtn.addEventListener('click', () => {
830
+ vscode.postMessage({ command: 'addContext' });
831
+ });
832
+
833
+ exportBtn.addEventListener('click', () => {
834
+ vscode.postMessage({ command: 'exportChat' });
835
+ });
836
+
837
+ clearBtn.addEventListener('click', () => {
838
+ vscode.postMessage({ command: 'clearChat' });
839
+ });
840
+
841
+ // Render chat message
842
+ function renderMessage(msg) {
843
+ const div = document.createElement('div');
844
+ div.className = \`message \${msg.role}\`;
845
+ div.dataset.id = msg.id;
846
+
847
+ const avatar = msg.role === 'user' ? '👤' : '🤖';
848
+ const time = new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
849
+
850
+ // Simple markdown rendering
851
+ let content = msg.content
852
+ .replace(/\`\`\`(\w*)\n([\s\S]*?)\`\`\`/g, '<pre><code>$2</code></pre>')
853
+ .replace(/\`([^\`]+)\`/g, '<code>$1</code>')
854
+ .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
855
+ .replace(/\n/g, '<br>');
856
+
857
+ div.innerHTML = \`
858
+ <div class="avatar">\${avatar}</div>
859
+ <div class="message-body">
860
+ <div class="message-content">\${content}</div>
861
+ <div class="message-meta">
862
+ <span>\${time}</span>
863
+ \${msg.refined ? '<span class="refined-badge">✨ Refined</span>' : ''}
864
+ </div>
865
+ <div class="message-actions">
866
+ <button class="msg-action-btn" onclick="copyMessage('\${msg.id}')">📋 Copy</button>
867
+ \${msg.role === 'assistant' ? '<button class="msg-action-btn" onclick="insertMessage(\\''+msg.id+'\\')">📥 Insert</button>' : ''}
868
+ </div>
869
+ </div>
870
+ \`;
871
+
872
+ return div;
873
+ }
874
+
875
+ // Copy message
876
+ window.copyMessage = (id) => {
877
+ const msg = window.chatHistory?.find(m => m.id === id);
878
+ if (msg) {
879
+ vscode.postMessage({ command: 'copyToClipboard', text: msg.content });
880
+ }
881
+ };
882
+
883
+ // Insert message into editor
884
+ window.insertMessage = (id) => {
885
+ const msg = window.chatHistory?.find(m => m.id === id);
886
+ if (msg) {
887
+ vscode.postMessage({ command: 'insertInEditor', text: msg.content });
888
+ }
889
+ };
890
+
891
+ // Handle messages from extension
892
+ window.addEventListener('message', event => {
893
+ const message = event.data;
894
+
895
+ switch (message.command) {
896
+ case 'prefill':
897
+ input.value = message.text;
898
+ input.focus();
899
+ input.style.height = 'auto';
900
+ input.style.height = Math.min(input.scrollHeight, 200) + 'px';
901
+ break;
902
+
903
+ case 'loading':
904
+ isLoading = message.loading;
905
+ sendBtn.disabled = message.loading;
906
+ typingIndicator.classList.toggle('visible', message.loading);
907
+ if (message.loading) {
908
+ sendBtnIcon.innerHTML = '<span class="spinner"></span>';
909
+ sendBtnText.textContent = '';
910
+ } else {
911
+ sendBtnIcon.textContent = '➤';
912
+ sendBtnText.textContent = 'Send';
913
+ }
914
+ // Scroll to bottom
915
+ chatContainer.scrollTop = chatContainer.scrollHeight;
916
+ break;
917
+
918
+ case 'refining':
919
+ isRefining = message.refining;
920
+ refineBtns.forEach(btn => btn.disabled = message.refining);
921
+ break;
922
+
923
+ case 'refined':
924
+ input.value = message.text;
925
+ wasRefined = true;
926
+ input.style.height = 'auto';
927
+ input.style.height = Math.min(input.scrollHeight, 200) + 'px';
928
+ break;
929
+
930
+ case 'refineError':
931
+ // Could show inline error
932
+ break;
933
+
934
+ case 'syncHistory':
935
+ window.chatHistory = message.history;
936
+
937
+ // Clear and re-render
938
+ chatContainer.innerHTML = '';
939
+
940
+ if (message.history.length === 0) {
941
+ chatContainer.appendChild(emptyState);
942
+ } else {
943
+ emptyState.remove();
944
+ message.history.forEach(msg => {
945
+ chatContainer.appendChild(renderMessage(msg));
946
+ });
947
+ }
948
+
949
+ // Scroll to bottom
950
+ chatContainer.scrollTop = chatContainer.scrollHeight;
951
+ break;
952
+
953
+ case 'addContext':
954
+ attachedContext = message.context;
955
+ contextIndicator.style.display = 'block';
956
+ contextIndicator.title = \`Attached: \${message.context.fileName}\`;
957
+ break;
958
+ }
959
+ });
960
+
961
+ // Focus input on load
962
+ input.focus();
963
+ </script>
964
+ </body>
965
+ </html>
966
+ `;
967
+ }
968
+
969
+ private dispose() {
970
+ ChatPanel.currentPanel = undefined;
971
+ this.panel.dispose();
972
+ while (this.disposables.length) {
973
+ const disposable = this.disposables.pop();
974
+ if (disposable) {
975
+ disposable.dispose();
976
+ }
977
+ }
978
+ }
979
+ }