sam-coder-cli 1.0.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.
Files changed (43) hide show
  1. package/README.md +59 -0
  2. package/ai-assistant-0.0.1.vsix +0 -0
  3. package/bin/agi-cli.js +815 -0
  4. package/bin/agi-cli.js.bak +352 -0
  5. package/bin/agi-cli.js.new +328 -0
  6. package/bin/config.json +3 -0
  7. package/bin/ui.js +42 -0
  8. package/dist/agentUtils.js +539 -0
  9. package/dist/agentUtils.js.map +1 -0
  10. package/dist/aiAssistantViewProvider.js +2098 -0
  11. package/dist/aiAssistantViewProvider.js.map +1 -0
  12. package/dist/extension.js +117 -0
  13. package/dist/extension.js.map +1 -0
  14. package/dist/fetch-polyfill.js +9 -0
  15. package/dist/fetch-polyfill.js.map +1 -0
  16. package/foldersnake/snake_game.py +125 -0
  17. package/media/ai-icon.png +0 -0
  18. package/media/ai-icon.svg +5 -0
  19. package/media/infinity-icon.svg +4 -0
  20. package/out/agentUtils.d.ts +28 -0
  21. package/out/agentUtils.d.ts.map +1 -0
  22. package/out/agentUtils.js +539 -0
  23. package/out/agentUtils.js.map +1 -0
  24. package/out/aiAssistantViewProvider.d.ts +58 -0
  25. package/out/aiAssistantViewProvider.d.ts.map +1 -0
  26. package/out/aiAssistantViewProvider.js +2098 -0
  27. package/out/aiAssistantViewProvider.js.map +1 -0
  28. package/out/extension.d.ts +4 -0
  29. package/out/extension.d.ts.map +1 -0
  30. package/out/extension.js +117 -0
  31. package/out/extension.js.map +1 -0
  32. package/out/fetch-polyfill.d.ts +11 -0
  33. package/out/fetch-polyfill.d.ts.map +1 -0
  34. package/out/fetch-polyfill.js +9 -0
  35. package/out/fetch-polyfill.js.map +1 -0
  36. package/package.json +31 -0
  37. package/src/agentUtils.ts +583 -0
  38. package/src/aiAssistantViewProvider.ts +2264 -0
  39. package/src/cliAgentUtils.js +73 -0
  40. package/src/extension.ts +112 -0
  41. package/src/fetch-polyfill.ts +11 -0
  42. package/tsconfig.json +24 -0
  43. package/webpack.config.js +45 -0
@@ -0,0 +1,2264 @@
1
+ import * as vscode from 'vscode';
2
+ import { fetch } from './fetch-polyfill';
3
+ import { AgentUtils, AgentAction } from './agentUtils';
4
+
5
+ interface Message {
6
+ role: 'user' | 'assistant' | 'system';
7
+ content: string;
8
+ timestamp: string;
9
+ }
10
+
11
+ // Default API key to use if none is provided in settings
12
+ const DEFAULT_API_KEY = 'fw_3ZM5QnSBpeAvHmRG6qB1FWCm';
13
+
14
+ // Translations for UI elements
15
+ interface Translations {
16
+ welcomeMessage: string;
17
+ workingMessage: string;
18
+ taskCompletedMessage: string;
19
+ errorMessage: string;
20
+ inputPlaceholder: string;
21
+ sendButton: string;
22
+ clearButton: string;
23
+ changeLanguageButton: string;
24
+ loadingText: string;
25
+ }
26
+
27
+ const translations: Record<string, Translations> = {
28
+ 'en': {
29
+ welcomeMessage: "Hello! I'm Samantha Coder, your AI assistant. Ask me anything or request help with your code. I can analyze and modify files in your workspace.",
30
+ workingMessage: "🤖 I'm working on this task...",
31
+ taskCompletedMessage: "✅ Task completed.",
32
+ errorMessage: "Sorry, I encountered an error. Please check your API key and connection.",
33
+ inputPlaceholder: "Ask something...",
34
+ sendButton: "Send",
35
+ clearButton: "Clear",
36
+ changeLanguageButton: "PT-BR",
37
+ loadingText: "Working on it..."
38
+ },
39
+ 'pt-br': {
40
+ welcomeMessage: "Olá! Eu sou Samantha Coder, sua assistente de IA. Pergunte qualquer coisa ou peça ajuda com seu código. Posso analisar e modificar arquivos no seu espaço de trabalho.",
41
+ workingMessage: "🤖 Estou trabalhando nesta tarefa...",
42
+ taskCompletedMessage: "✅ Tarefa concluída.",
43
+ errorMessage: "Desculpe, encontrei um erro. Por favor, verifique sua chave de API e conexão.",
44
+ inputPlaceholder: "Pergunte algo...",
45
+ sendButton: "Enviar",
46
+ clearButton: "Limpar",
47
+ changeLanguageButton: "EN",
48
+ loadingText: "Trabalhando nisso..."
49
+ }
50
+ };
51
+
52
+ // Add Chat interface to store chat information
53
+ interface Chat {
54
+ id: string;
55
+ title: string;
56
+ messages: Message[];
57
+ createdAt: string;
58
+ updatedAt: string;
59
+ }
60
+
61
+ // Add this interface near the top of the file, alongside the other interfaces
62
+ interface FileSnapshot {
63
+ exists: boolean;
64
+ content: string;
65
+ version: number;
66
+ }
67
+
68
+ interface StateSnapshot {
69
+ files: { [path: string]: FileSnapshot };
70
+ timestamp: string;
71
+ chatId: string | null;
72
+ primaryFile?: string;
73
+ trackedFiles: string[];
74
+ }
75
+
76
+ export class AIAssistantViewProvider implements vscode.WebviewViewProvider {
77
+ private _view?: vscode.WebviewView;
78
+ private _chats: Chat[] = [];
79
+ private _currentChatId: string | null = null;
80
+ private _agentUtils: AgentUtils;
81
+ private _isProcessingAgentActions = false;
82
+ private _lastEditedFile: string | null = null;
83
+ private _lastEditTime: number = 0;
84
+ private _language: string = 'en';
85
+ private _conversationHistory: { [key: string]: any } = {}; // Store state snapshots
86
+ private _trackedFiles: Set<string> = new Set();
87
+ private _abortController: AbortController | null = null; // For canceling fetch requests
88
+ private _isGeneratingResponse = false; // Flag to track if a response is being generated
89
+
90
+ constructor(
91
+ private readonly _extensionUri: vscode.Uri
92
+ ) {
93
+ this._agentUtils = new AgentUtils();
94
+
95
+ // Load language setting
96
+ const config = vscode.workspace.getConfiguration('aiAssistant');
97
+ this._language = config.get('language') || 'en';
98
+
99
+ // Track language setting changes
100
+ vscode.workspace.onDidChangeConfiguration(e => {
101
+ if (e.affectsConfiguration('aiAssistant.language')) {
102
+ const config = vscode.workspace.getConfiguration('aiAssistant');
103
+ this._language = config.get('language') || 'en';
104
+ this._updateWebviewContent();
105
+ }
106
+ });
107
+
108
+ // Track file edits
109
+ vscode.workspace.onDidChangeTextDocument(e => {
110
+ this._lastEditedFile = e.document.uri.fsPath;
111
+ this._lastEditTime = Date.now();
112
+ this._trackedFiles.add(e.document.uri.fsPath);
113
+ });
114
+
115
+ // Track file system changes
116
+ vscode.workspace.onDidCreateFiles(e => {
117
+ e.files.forEach(uri => {
118
+ this._trackedFiles.add(uri.fsPath);
119
+ });
120
+ });
121
+
122
+ // Initialize with default chat
123
+ this._ensureDefaultChat();
124
+ }
125
+
126
+ // Ensure there's at least one chat
127
+ private _ensureDefaultChat(): void {
128
+ if (this._chats.length === 0) {
129
+ this._createNewChat();
130
+ }
131
+ }
132
+
133
+ // Create a new chat
134
+ private _createNewChat(title?: string): string {
135
+ const id = Date.now().toString();
136
+ const now = new Date().toISOString();
137
+
138
+ const newChat: Chat = {
139
+ id,
140
+ title: title || `Chat ${this._chats.length + 1}`,
141
+ messages: [],
142
+ createdAt: now,
143
+ updatedAt: now
144
+ };
145
+
146
+ this._chats.push(newChat);
147
+ this._currentChatId = id;
148
+
149
+ // Update the webview if it exists
150
+ if (this._view) {
151
+ this._postMessageToWebview({
152
+ type: 'updateChats',
153
+ chats: this._chats,
154
+ currentChatId: this._currentChatId
155
+ });
156
+ }
157
+
158
+ return id;
159
+ }
160
+
161
+ // Get the current chat
162
+ private _getCurrentChat(): Chat | undefined {
163
+ // Ensure we have at least one chat
164
+ this._ensureDefaultChat();
165
+
166
+ if (!this._currentChatId && this._chats.length > 0) {
167
+ this._currentChatId = this._chats[0].id;
168
+ }
169
+
170
+ return this._chats.find(chat => chat.id === this._currentChatId);
171
+ }
172
+
173
+ // Filter out system messages for display
174
+ private _getMessagesForDisplay(messages: Message[]): Message[] {
175
+ return messages.filter(msg => msg.role !== 'system');
176
+ }
177
+
178
+ // Switch to a different chat
179
+ private _switchChat(chatId: string): void {
180
+ const chat = this._chats.find(c => c.id === chatId);
181
+ if (chat) {
182
+ this._currentChatId = chatId;
183
+
184
+ // Update the webview with visible messages only
185
+ this._postMessageToWebview({
186
+ type: 'switchChat',
187
+ chatId,
188
+ messages: this._getMessagesForDisplay(chat.messages)
189
+ });
190
+
191
+ // Update chat list to reflect new active chat
192
+ this._postMessageToWebview({
193
+ type: 'updateChats',
194
+ chats: this._chats,
195
+ currentChatId: this._currentChatId
196
+ });
197
+ }
198
+ }
199
+
200
+ // Rename a chat
201
+ private _renameChat(chatId: string, newTitle: string): void {
202
+ const chat = this._chats.find(c => c.id === chatId);
203
+ if (chat) {
204
+ chat.title = newTitle;
205
+ chat.updatedAt = new Date().toISOString();
206
+
207
+ // Update the webview
208
+ this._postMessageToWebview({
209
+ type: 'updateChats',
210
+ chats: this._chats,
211
+ currentChatId: this._currentChatId
212
+ });
213
+ }
214
+ }
215
+
216
+ // Delete a chat
217
+ private _deleteChat(chatId: string): void {
218
+ const index = this._chats.findIndex(c => c.id === chatId);
219
+ if (index !== -1) {
220
+ // Store reference to the chat we're deleting
221
+ const deletedChat = this._chats[index];
222
+
223
+ // Remove the chat
224
+ this._chats.splice(index, 1);
225
+
226
+ // Remove all state snapshots associated with this chat's messages
227
+ if (deletedChat && deletedChat.messages) {
228
+ deletedChat.messages.forEach(msg => {
229
+ if (msg.timestamp && this._conversationHistory[msg.timestamp]) {
230
+ delete this._conversationHistory[msg.timestamp];
231
+ }
232
+ });
233
+ }
234
+
235
+ // If we deleted the current chat, switch to another one or create a new one
236
+ if (this._currentChatId === chatId) {
237
+ if (this._chats.length > 0) {
238
+ this._currentChatId = this._chats[0].id;
239
+
240
+ // Get the new current chat
241
+ const newCurrentChat = this._getCurrentChat();
242
+
243
+ // Update the webview with the new chat's messages
244
+ if (newCurrentChat && this._view) {
245
+ this._postMessageToWebview({
246
+ type: 'switchChat',
247
+ chatId: this._currentChatId,
248
+ messages: this._getMessagesForDisplay(newCurrentChat.messages)
249
+ });
250
+ }
251
+ } else {
252
+ // Create a new chat if no chats remain
253
+ this._createNewChat();
254
+
255
+ // Get the new chat
256
+ const newChat = this._getCurrentChat();
257
+
258
+ // Update the webview with the new chat
259
+ if (newChat && this._view) {
260
+ this._postMessageToWebview({
261
+ type: 'switchChat',
262
+ chatId: this._currentChatId,
263
+ messages: this._getMessagesForDisplay(newChat.messages)
264
+ });
265
+ }
266
+ }
267
+ }
268
+
269
+ // Update the webview's chat list
270
+ this._postMessageToWebview({
271
+ type: 'updateChats',
272
+ chats: this._chats,
273
+ currentChatId: this._currentChatId
274
+ });
275
+ }
276
+ }
277
+
278
+ private _updateSystemMessage(chat: Chat) {
279
+ // Remove existing system message
280
+ chat.messages = chat.messages.filter(msg => msg.role !== 'system');
281
+
282
+ // Get workspace info
283
+ const workspaceFolders = vscode.workspace.workspaceFolders || [];
284
+ const workspaceInfo = workspaceFolders.map(folder => folder.uri.fsPath).join(', ');
285
+
286
+ // Add updated system message
287
+ chat.messages.push({
288
+ role: 'system',
289
+ content: `You are a VS Code AI Assistant with agency capabilities. You can perform actions on the user's workspace.
290
+
291
+ ENVIRONMENT CONTEXT:
292
+ - OS: ${process.platform}
293
+ - Workspace: ${workspaceInfo || 'No workspace open'}
294
+ - Last edited file: ${this._lastEditedFile || 'None'}
295
+ ${this._lastEditedFile ? `- Last edit: ${new Date(this._lastEditTime).toLocaleTimeString()}` : ''}
296
+
297
+ When you need to perform actions, respond with JSON in the following format:
298
+ \`\`\`json
299
+ {
300
+ "thoughts": "Your reasoning about what needs to be done",
301
+ "actions": [
302
+ {
303
+ "type": "read|write|search|command|analyze|execute|stop",
304
+ "data": { ... action specific data ... }
305
+ }
306
+ ]
307
+ }
308
+ \`\`\`
309
+
310
+ Action types and their data:
311
+ - read: { "path": "relative/or/absolute/path" }
312
+ - write: { "path": "relative/or/absolute/path", "content": "file content" }
313
+ - search: { "type": "files", "pattern": "glob pattern" } or { "type": "text", "text": "search text" }
314
+ - command: { "command": "command string to execute in terminal" }
315
+ - execute: { "language": "js|python|bash|...", "code": "code to execute" }
316
+ - analyze: { "code": "code to analyze", "question": "what you want to analyze" }
317
+ - browse: { "query": "search query", "numResults": 5 } (free web search using DuckDuckGo, optional numResults)
318
+ - edit: {
319
+ "path": "relative/or/absolute/path",
320
+ "edits": {
321
+ "operations": [
322
+ { "type": "replace", "startLine": 10, "endLine": 15, "newText": "new code here" },
323
+ { "type": "replace", "pattern": "oldFunction\\(\\)", "replacement": "newFunction()", "flags": "g" },
324
+ { "type": "insert", "line": 20, "text": "new line of code here" },
325
+ { "type": "insert", "position": "start", "text": "// Header comment" },
326
+ { "type": "insert", "position": "end", "text": "// Footer comment" },
327
+ { "type": "delete", "startLine": 25, "endLine": 30 }
328
+ ]
329
+ }
330
+ } (edit specific parts of an existing file)
331
+ - stop: {} (use this to indicate you're done with the task and no more actions are needed)
332
+
333
+ CONTEXTUAL UNDERSTANDING:
334
+ Before every response, I will automatically gather information about the user's current context, including:
335
+ - Currently open files
336
+ - Current editor selection
337
+ - Recent edits
338
+ - Terminal output (if available)
339
+
340
+ By default, you will continue to take actions in a loop until you decide to stop with the 'stop' action type.
341
+ Always wrap your JSON in markdown code blocks with the json language specifier.
342
+ When executing code or commands that might be potentially harmful, explain what the code does before executing it.
343
+ `,
344
+ timestamp: new Date().toISOString()
345
+ });
346
+ }
347
+
348
+ public async gatherUserContext(): Promise<string> {
349
+ try {
350
+ let context = "";
351
+
352
+ // Get active editor and document info
353
+ const editor = vscode.window.activeTextEditor;
354
+ if (editor) {
355
+ const document = editor.document;
356
+ const selection = editor.selection;
357
+ const selectedText = document.getText(selection);
358
+
359
+ context += `ACTIVE EDITOR:\n`;
360
+ context += `- File: ${document.uri.fsPath}\n`;
361
+ context += `- Language: ${document.languageId}\n`;
362
+
363
+ if (!selection.isEmpty) {
364
+ context += `- Selection (Lines ${selection.start.line + 1}-${selection.end.line + 1}):\n`;
365
+ context += "```\n" + selectedText + "\n```\n";
366
+ } else {
367
+ const lineCount = document.lineCount;
368
+ const visibleRange = editor.visibleRanges[0];
369
+ context += `- Visible Range: Lines ${visibleRange.start.line + 1}-${visibleRange.end.line + 1} of ${lineCount}\n`;
370
+
371
+ // Get a few lines around the cursor to provide context
372
+ const cursorPos = selection.active;
373
+ const startLine = Math.max(0, cursorPos.line - 3);
374
+ const endLine = Math.min(lineCount - 1, cursorPos.line + 3);
375
+ const contextLines: string[] = [];
376
+
377
+ for (let i = startLine; i <= endLine; i++) {
378
+ const line = document.lineAt(i);
379
+ contextLines.push(`${i === cursorPos.line ? '> ' : ' '}${line.text}`);
380
+ }
381
+
382
+ context += `- Code around cursor (Line ${cursorPos.line + 1}):\n`;
383
+ context += "```\n" + contextLines.join('\n') + "\n```\n";
384
+ }
385
+ } else {
386
+ context += "No active editor\n";
387
+ }
388
+
389
+ // Get information about open editors
390
+ const openEditors = vscode.window.visibleTextEditors.map(editor => {
391
+ return {
392
+ path: editor.document.uri.fsPath,
393
+ viewColumn: editor.viewColumn
394
+ };
395
+ });
396
+
397
+ if (openEditors.length > 0) {
398
+ context += `\nOPEN EDITORS:\n`;
399
+ openEditors.forEach(editor => {
400
+ context += `- ${editor.path}\n`;
401
+ });
402
+ }
403
+
404
+ // Recent edits
405
+ if (this._lastEditedFile) {
406
+ context += `\nRECENT EDITS:\n`;
407
+ context += `- Last modified: ${this._lastEditedFile} at ${new Date(this._lastEditTime).toLocaleTimeString()}\n`;
408
+ }
409
+
410
+ // Terminal output
411
+ try {
412
+ const terminalOutput = await this._agentUtils.getTerminalOutput();
413
+ if (terminalOutput) {
414
+ context += `\nTERMINAL:\n${terminalOutput}\n`;
415
+ }
416
+ } catch (error) {
417
+ console.error('Error getting terminal output:', error);
418
+ }
419
+
420
+ // Current extensions
421
+ try {
422
+ const extensions = vscode.extensions.all
423
+ .filter(ext => ext.isActive && !ext.packageJSON.isBuiltin)
424
+ .map(ext => `${ext.packageJSON.displayName || ext.packageJSON.name} (${ext.packageJSON.version})`);
425
+
426
+ if (extensions.length > 0) {
427
+ context += `\nACTIVE EXTENSIONS (top 5):\n`;
428
+ extensions.slice(0, 5).forEach(ext => {
429
+ context += `- ${ext}\n`;
430
+ });
431
+ }
432
+ } catch (error) {
433
+ console.error('Error getting extensions:', error);
434
+ }
435
+
436
+ return context;
437
+ } catch (error) {
438
+ console.error('Error gathering context:', error);
439
+ return "Error gathering context";
440
+ }
441
+ }
442
+
443
+ public ensureWebviewIsVisible() {
444
+ try {
445
+ if (!this._view) {
446
+ // Try to show the view if not already visible
447
+ vscode.commands.executeCommand('workbench.view.extension.ai-assistant-view');
448
+ vscode.commands.executeCommand('aiAssistantView.focus');
449
+ } else {
450
+ // If view exists but might not be visible, try to show it
451
+ this._view.show(true);
452
+ }
453
+ } catch (error) {
454
+ console.error('Error ensuring webview is visible:', error);
455
+ }
456
+ }
457
+
458
+ public resolveWebviewView(
459
+ webviewView: vscode.WebviewView,
460
+ _context: vscode.WebviewViewResolveContext,
461
+ _token: vscode.CancellationToken,
462
+ ) {
463
+ this._view = webviewView;
464
+
465
+ if (!this._extensionUri) {
466
+ console.error('Extension URI is undefined in resolveWebviewView');
467
+ return;
468
+ }
469
+
470
+ // Ensure default chat exists
471
+ this._ensureDefaultChat();
472
+
473
+ webviewView.webview.options = {
474
+ enableScripts: true,
475
+ localResourceRoots: [
476
+ vscode.Uri.joinPath(this._extensionUri, 'media')
477
+ ]
478
+ };
479
+
480
+ webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
481
+
482
+ webviewView.webview.onDidReceiveMessage(async data => {
483
+ switch (data.type) {
484
+ case 'sendQuery':
485
+ await this.sendQueryToAI(data.value);
486
+ break;
487
+ case 'clearConversation':
488
+ this.clearConversation();
489
+ break;
490
+ case 'toggleLanguage':
491
+ this.toggleLanguage();
492
+ break;
493
+ case 'restoreState':
494
+ await this.restoreToState(data.messageId);
495
+ break;
496
+ case 'createNewChat':
497
+ this._createNewChat(data.title);
498
+ break;
499
+ case 'switchChat':
500
+ this._switchChat(data.chatId);
501
+ break;
502
+ case 'renameChat':
503
+ this._renameChat(data.chatId, data.newTitle);
504
+ break;
505
+ case 'deleteChat':
506
+ this._deleteChat(data.chatId);
507
+ break;
508
+ case 'getConfiguration':
509
+ this._getConfiguration();
510
+ break;
511
+ case 'saveConfiguration':
512
+ this._saveConfiguration(data.modelName, data.apiBaseUrl, data.apiKey, data.maxTokens);
513
+ break;
514
+ }
515
+ });
516
+
517
+ // Ensure a default chat exists
518
+ this._ensureDefaultChat();
519
+
520
+ // Send initial chat data to the webview
521
+ this._postMessageToWebview({
522
+ type: 'updateChats',
523
+ chats: this._chats,
524
+ currentChatId: this._currentChatId
525
+ });
526
+
527
+ // Get current chat messages (if any)
528
+ const currentChat = this._getCurrentChat();
529
+ if (currentChat) {
530
+ this._postMessageToWebview({
531
+ type: 'restoreConversation',
532
+ messages: this._getMessagesForDisplay(currentChat.messages)
533
+ });
534
+ }
535
+
536
+ // Show the view by focusing on it
537
+ if (!webviewView.visible) {
538
+ webviewView.show();
539
+ }
540
+ }
541
+
542
+ public async sendQueryToAI(query: string) {
543
+ if (!this._view) {
544
+ this.ensureWebviewIsVisible();
545
+ return;
546
+ }
547
+
548
+ // Don't start a new query if we're already generating a response
549
+ if (this._isGeneratingResponse) {
550
+ vscode.window.showInformationMessage('Already processing a request. Please wait or clear the conversation.');
551
+ return;
552
+ }
553
+
554
+ // Get the current chat or create a new one if none exists
555
+ let currentChat = this._getCurrentChat();
556
+ if (!currentChat) {
557
+ this._createNewChat();
558
+ currentChat = this._getCurrentChat();
559
+ }
560
+
561
+ if (!currentChat) {
562
+ console.error('Failed to get or create a chat');
563
+ return;
564
+ }
565
+
566
+ // Add user message to conversation
567
+ const userMessage: Message = {
568
+ role: 'user',
569
+ content: query,
570
+ timestamp: new Date().toISOString()
571
+ };
572
+ currentChat.messages.push(userMessage);
573
+ currentChat.updatedAt = new Date().toISOString();
574
+
575
+ // Take a snapshot of the current state before processing
576
+ await this._takeStateSnapshot(userMessage.timestamp);
577
+
578
+ // Update the webview with the new message
579
+ await this._postMessageToWebview({
580
+ type: 'addMessage',
581
+ message: userMessage
582
+ });
583
+
584
+ try {
585
+ // Gather current context before sending query
586
+ const userContext = await this.gatherUserContext();
587
+
588
+ // Update the system message with fresh context
589
+ this._updateSystemMessage(currentChat);
590
+
591
+ // Update the system message to include a timestamp
592
+ currentChat.messages.push({
593
+ role: 'system',
594
+ content: `Current user context:\n${userContext}`,
595
+ timestamp: new Date().toISOString()
596
+ });
597
+
598
+ // Show loading indicator
599
+ await this._postMessageToWebview({ type: 'setLoading', isLoading: true });
600
+
601
+ // Get AI response
602
+ await this._getAIResponse(currentChat);
603
+ } catch (error) {
604
+ console.error('Error in sendQueryToAI:', error);
605
+ vscode.window.showErrorMessage(`Error: ${error instanceof Error ? error.message : String(error)}`);
606
+
607
+ await this._postMessageToWebview({
608
+ type: 'addMessage',
609
+ message: {
610
+ role: 'assistant',
611
+ content: translations[this._language].errorMessage,
612
+ timestamp: new Date().toISOString()
613
+ }
614
+ });
615
+ } finally {
616
+ // Hide loading indicator
617
+ await this._postMessageToWebview({ type: 'setLoading', isLoading: false });
618
+ }
619
+ }
620
+
621
+ // Helper method to safely post messages to the webview
622
+ private async _postMessageToWebview(message: any): Promise<void> {
623
+ if (this._view?.webview) {
624
+ try {
625
+ await this._view.webview.postMessage(message);
626
+ } catch (error) {
627
+ console.error('Error posting message to webview:', error);
628
+ }
629
+ }
630
+ }
631
+
632
+ private async _getAIResponse(chat: Chat) {
633
+ try {
634
+ if (!this._view) {
635
+ return;
636
+ }
637
+
638
+ // Set the generating flag
639
+ this._isGeneratingResponse = true;
640
+
641
+ // Create a new AbortController for this request
642
+ this._abortController = new AbortController();
643
+
644
+ // Get API key from settings or use default
645
+ const config = vscode.workspace.getConfiguration('aiAssistant');
646
+ let apiKey = config.get<string>('apiKey');
647
+
648
+ // If no API key in settings, use the default one
649
+ if (!apiKey || apiKey.trim() === '') {
650
+ apiKey = DEFAULT_API_KEY;
651
+ }
652
+
653
+ const model = config.get<string>('model') || "accounts/fireworks/models/deepseek-v3-0324";
654
+ const apiBaseUrl = config.get<string>('apiBaseUrl') || "https://api.fireworks.ai/inference/v1";
655
+ const maxTokens = config.get<number>('maxTokens') || 120000;
656
+
657
+ // Strip out timestamp field from messages before sending to API
658
+ const apiMessages = chat.messages.map(msg => ({
659
+ role: msg.role,
660
+ content: msg.content
661
+ }));
662
+
663
+ // Create request options
664
+ const options: any = {
665
+ method: "POST",
666
+ headers: {
667
+ "Accept": "application/json",
668
+ "Content-Type": "application/json",
669
+ "Authorization": `Bearer ${apiKey}`
670
+ },
671
+ body: JSON.stringify({
672
+ model: model,
673
+ max_tokens: maxTokens,
674
+ top_p: 1,
675
+ top_k: 40,
676
+ presence_penalty: 0,
677
+ frequency_penalty: 0,
678
+ temperature: 0.6,
679
+ messages: apiMessages
680
+ })
681
+ };
682
+
683
+ // Add abort signal if available
684
+ if (this._abortController) {
685
+ options.signal = this._abortController.signal;
686
+ }
687
+
688
+ const response = await fetch(`${apiBaseUrl}/chat/completions`, options);
689
+
690
+ // Check if the request was aborted
691
+ if (this._abortController && this._abortController.signal.aborted) {
692
+ this._isGeneratingResponse = false;
693
+ return;
694
+ }
695
+
696
+ if (!response.ok) {
697
+ const errorData = await response.json().catch(() => null);
698
+ let errorMessage = `API request failed: ${response.statusText}`;
699
+ if (errorData?.error?.message) {
700
+ errorMessage = `API Error: ${errorData.error.message}`;
701
+ } else if (response.status === 401) {
702
+ errorMessage = 'Invalid API key. Please check your API key in settings.';
703
+ } else if (response.status === 429) {
704
+ errorMessage = 'Rate limit exceeded. Please try again later.';
705
+ } else if (response.status >= 500) {
706
+ errorMessage = 'Server error. Please try again later.';
707
+ }
708
+ throw new Error(errorMessage);
709
+ }
710
+
711
+ const data = await response.json();
712
+ if (!data?.choices?.[0]?.message?.content) {
713
+ throw new Error('Invalid response format from API');
714
+ }
715
+
716
+ const assistantMessage = data.choices[0].message.content;
717
+
718
+ // Update the agent working message to include a timestamp
719
+ const timestamp = new Date().toISOString();
720
+ await this._postMessageToWebview({
721
+ type: 'addMessage',
722
+ message: {
723
+ role: 'assistant',
724
+ content: 'Thinking...',
725
+ timestamp
726
+ }
727
+ });
728
+
729
+ // Update the agent response message to include a timestamp
730
+ chat.messages.push({
731
+ role: 'assistant',
732
+ content: assistantMessage,
733
+ timestamp
734
+ });
735
+
736
+ // Update UI with agent response
737
+ await this._postMessageToWebview({
738
+ type: 'addMessage',
739
+ message: {
740
+ role: 'assistant',
741
+ content: assistantMessage,
742
+ timestamp
743
+ }
744
+ });
745
+
746
+ // Check if the message contains agent actions
747
+ await this._processAgentActions(assistantMessage, chat);
748
+ } catch (error) {
749
+ console.error('Error calling Fireworks API:', error);
750
+
751
+ // Show a more specific error message in the chat
752
+ const errorMessage = error instanceof Error ? error.message : String(error);
753
+ await this._postMessageToWebview({
754
+ type: 'addMessage',
755
+ message: {
756
+ role: 'assistant',
757
+ content: `❌ ${errorMessage}`,
758
+ timestamp: new Date().toISOString()
759
+ }
760
+ });
761
+
762
+ throw error; // Rethrow to be handled by caller
763
+ } finally {
764
+ this._isGeneratingResponse = false;
765
+ if (this._view) {
766
+ this._view.webview.postMessage({ type: 'setLoading', isLoading: false });
767
+ }
768
+ }
769
+ }
770
+
771
+ private _updateWebviewContent() {
772
+ if (this._view) {
773
+ this._view.webview.html = this._getHtmlForWebview(this._view.webview);
774
+ // Update welcome message
775
+ this._postMessageToWebview({
776
+ type: 'updateLanguage',
777
+ translations: translations[this._language]
778
+ });
779
+ }
780
+ }
781
+
782
+ public async _processAgentActions(message: string, chat: Chat) {
783
+ // If already processing actions, don't start another process
784
+ if (this._isProcessingAgentActions) {
785
+ return;
786
+ }
787
+
788
+ try {
789
+ this._isProcessingAgentActions = true;
790
+
791
+ // First try to extract JSON from markdown code blocks
792
+ const jsonMatch = message.match(/```json\s*([\s\S]*?)\s*```/);
793
+
794
+ if (jsonMatch) {
795
+ // Process JSON from code block
796
+ const jsonContent = jsonMatch[1].trim();
797
+ this._agentUtils.log(`Extracted JSON from markdown: ${jsonContent ? jsonContent.substring(0, 100) + '...' : 'empty content'}`);
798
+
799
+ try {
800
+ const actionData = JSON.parse(jsonContent);
801
+ this._agentUtils.log(`Successfully parsed JSON from code block`);
802
+
803
+ // Check if processing was cancelled
804
+ if (!this._isProcessingAgentActions) {
805
+ return;
806
+ }
807
+
808
+ await this._processJsonActions(actionData, chat);
809
+ } catch (parseError) {
810
+ console.error('Error parsing JSON from code block:', parseError);
811
+ this._agentUtils.log(`Error parsing JSON from code block: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
812
+ }
813
+ } else {
814
+ // Try extracting without code block markers
815
+ try {
816
+ const jsonData = JSON.parse(message);
817
+ if (jsonData && jsonData.actions && Array.isArray(jsonData.actions)) {
818
+ this._agentUtils.log(`Found JSON without code block markers`);
819
+
820
+ // Check if processing was cancelled
821
+ if (!this._isProcessingAgentActions) {
822
+ return;
823
+ }
824
+
825
+ await this._processJsonActions(jsonData, chat);
826
+ } else {
827
+ this._agentUtils.log(`Parsed JSON but no valid actions array found`);
828
+ }
829
+ } catch (e) {
830
+ // Not valid JSON
831
+ this._agentUtils.log(`No JSON code blocks found and content is not valid JSON`);
832
+ }
833
+ }
834
+ } catch (error) {
835
+ console.error('Error processing agent actions:', error);
836
+ this._agentUtils.log(`Error processing agent actions: ${error instanceof Error ? error.message : String(error)}`);
837
+
838
+ if (this._view) {
839
+ this._view.webview.postMessage({
840
+ type: 'addMessage',
841
+ message: {
842
+ role: 'assistant',
843
+ content: `❌ ${error instanceof Error ? error.message : String(error)}`,
844
+ timestamp: new Date().toISOString()
845
+ }
846
+ });
847
+ }
848
+ } finally {
849
+ this._isProcessingAgentActions = false;
850
+ if (this._view) {
851
+ this._view.webview.postMessage({ type: 'setLoading', isLoading: false });
852
+ }
853
+ }
854
+ }
855
+
856
+ private async _processJsonActions(actionData: any, chat: Chat) {
857
+ if (!actionData.actions || !Array.isArray(actionData.actions)) {
858
+ this._agentUtils.log(`No valid actions array found in JSON`);
859
+ return; // No valid actions array
860
+ }
861
+
862
+ // Check if there's a "stop" action which should halt the agent after executing all actions
863
+ const hasStopAction = actionData.actions.some((action: AgentAction) => action.type === 'stop');
864
+
865
+ // Show that the agent is working
866
+ this._agentUtils.showOutputChannel();
867
+ this._agentUtils.log(`Processing ${actionData.actions.length} agent actions...`);
868
+ this._agentUtils.log(`Thoughts: ${actionData.thoughts || 'No thoughts provided'}`);
869
+
870
+ // Log each action for debugging
871
+ actionData.actions.forEach((action: AgentAction, index: number) => {
872
+ const actionDataStr = action.data ?
873
+ (typeof action.data === 'string' ?
874
+ action.data.substring(0, 150) :
875
+ JSON.stringify(action.data || {}).substring(0, 150)) :
876
+ 'undefined';
877
+ this._agentUtils.log(`Action ${index + 1}: type=${action.type}, data=${actionDataStr}${actionDataStr && actionDataStr.length > 149 ? '...' : ''}`);
878
+ });
879
+
880
+ if (this._view) {
881
+ this._view.webview.postMessage({
882
+ type: 'addMessage',
883
+ message: {
884
+ role: 'assistant',
885
+ content: translations[this._language].workingMessage,
886
+ timestamp: new Date().toISOString()
887
+ }
888
+ });
889
+ this._view.webview.postMessage({ type: 'setLoading', isLoading: true });
890
+ }
891
+
892
+ // Execute actions
893
+ try {
894
+ // Check if processing was cancelled before executing actions
895
+ if (!this._isProcessingAgentActions) {
896
+ return;
897
+ }
898
+
899
+ const actionResults = await this._agentUtils.executeActions(actionData.actions);
900
+
901
+ // Check again if processing was cancelled after executing actions
902
+ if (!this._isProcessingAgentActions) {
903
+ return;
904
+ }
905
+
906
+ this._agentUtils.log(`Actions executed successfully`);
907
+
908
+ // Log results for debugging
909
+ actionResults.forEach((result: AgentAction, index: number) => {
910
+ let resultStr = 'undefined';
911
+ if (result.result) {
912
+ if (typeof result.result === 'string') {
913
+ resultStr = result.result.substring(0, 100);
914
+ } else {
915
+ try {
916
+ resultStr = JSON.stringify(result.result).substring(0, 100);
917
+ } catch (e) {
918
+ resultStr = '[Object cannot be stringified]';
919
+ }
920
+ }
921
+ }
922
+ this._agentUtils.log(`Result ${index + 1}: ${resultStr}${resultStr.length >= 100 ? '...' : ''}`);
923
+ });
924
+
925
+ // If this batch included a stop action, stop after all actions complete
926
+ if (hasStopAction) {
927
+ this._agentUtils.log(`Stop action detected, halting after execution`);
928
+ if (this._view) {
929
+ this._view.webview.postMessage({
930
+ type: 'addMessage',
931
+ message: {
932
+ role: 'assistant',
933
+ content: translations[this._language].taskCompletedMessage,
934
+ timestamp: new Date().toISOString()
935
+ }
936
+ });
937
+ }
938
+ return;
939
+ }
940
+
941
+ // Check if processing was cancelled before continuing
942
+ if (!this._isProcessingAgentActions) {
943
+ return;
944
+ }
945
+
946
+ // Format results to send back to AI
947
+ const resultsForAI = JSON.stringify(actionResults, null, 2);
948
+
949
+ // Update context after actions completed
950
+ const updatedContext = await this.gatherUserContext();
951
+
952
+ // First add context as a system message
953
+ chat.messages.push({
954
+ role: 'system',
955
+ content: `Updated context after actions:\n${updatedContext}`,
956
+ timestamp: new Date().toISOString()
957
+ });
958
+
959
+ // Add action results as a system message to the conversation (not shown in UI)
960
+ chat.messages.push({
961
+ role: 'system',
962
+ content: `The assistant has completed the actions. Here are the results:\n\`\`\`json\n${resultsForAI}\n\`\`\`\n
963
+ Based on these results, determine what to do next. You can:
964
+ 1. Continue with more actions by returning a new JSON with "actions" array
965
+ 2. Stop the iteration by including an action with "type": "stop" if the task is completed
966
+ 3. Provide a final response to the user with your findings
967
+
968
+ Please analyze these results and respond appropriately.`,
969
+ timestamp: new Date().toISOString()
970
+ });
971
+
972
+ // Check if processing was cancelled before calling API again
973
+ if (!this._isProcessingAgentActions) {
974
+ return;
975
+ }
976
+
977
+ // Call API again with updated conversation
978
+ await this._getAIResponse(chat);
979
+ } catch (error) {
980
+ console.error('Error executing actions:', error);
981
+ this._agentUtils.log(`Error executing actions: ${error instanceof Error ? error.message : String(error)}`);
982
+ throw error;
983
+ }
984
+ }
985
+
986
+ public clearConversation() {
987
+ // Abort any ongoing API request
988
+ if (this._abortController && this._isGeneratingResponse) {
989
+ this._abortController.abort();
990
+ this._abortController = null;
991
+ this._isGeneratingResponse = false;
992
+ }
993
+
994
+ // Stop any agent actions processing
995
+ this._isProcessingAgentActions = false;
996
+
997
+ // Get the current chat
998
+ const currentChat = this._getCurrentChat();
999
+ if (!currentChat) {
1000
+ return;
1001
+ }
1002
+
1003
+ // Keep system message but clear the rest
1004
+ currentChat.messages = currentChat.messages.filter(msg => msg.role === 'system');
1005
+
1006
+ // Update the webview
1007
+ this._postMessageToWebview({ type: 'clearConversation' });
1008
+
1009
+ // Hide loading indicator if it's showing
1010
+ this._postMessageToWebview({ type: 'setLoading', isLoading: false });
1011
+ }
1012
+
1013
+ private _getHtmlForWebview(webview: vscode.Webview) {
1014
+ // Create URLs for images
1015
+ let mediaPath: vscode.Uri | string = '';
1016
+ let infinityIconPath: string = '';
1017
+
1018
+ if (this._extensionUri) {
1019
+ try {
1020
+ mediaPath = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'media'));
1021
+ infinityIconPath = `${mediaPath}/infinity-icon.svg`;
1022
+ } catch (error) {
1023
+ console.error('Error creating webview URIs:', error);
1024
+ }
1025
+ } else {
1026
+ console.error('Extension URI is undefined');
1027
+ }
1028
+
1029
+ return `<!DOCTYPE html>
1030
+ <html lang="${this._language}">
1031
+ <head>
1032
+ <meta charset="UTF-8">
1033
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1034
+ <title>AI Assistant</title>
1035
+ <style>
1036
+ :root {
1037
+ --primary-color: #ff3333;
1038
+ --primary-gradient: linear-gradient(135deg, #ff3333, #cc0000);
1039
+ --secondary-gradient: linear-gradient(135deg, #ff6666, #ff3333);
1040
+ --accent-color: #ff6666;
1041
+ --text-on-primary: white;
1042
+ --message-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
1043
+ --hover-transform: translateY(-2px);
1044
+ }
1045
+
1046
+ body {
1047
+ font-family: var(--vscode-font-family);
1048
+ padding: 0;
1049
+ margin: 0;
1050
+ color: var(--vscode-foreground);
1051
+ background-color: var(--vscode-editor-background);
1052
+ background-image:
1053
+ radial-gradient(circle at 0% 0%, rgba(255, 51, 51, 0.03) 0%, transparent 50%),
1054
+ radial-gradient(circle at 100% 100%, rgba(255, 102, 102, 0.03) 0%, transparent 50%);
1055
+ }
1056
+
1057
+ .container {
1058
+ display: flex;
1059
+ flex-direction: column;
1060
+ height: 100vh;
1061
+ max-width: 100%;
1062
+ box-sizing: border-box;
1063
+ }
1064
+
1065
+ .header {
1066
+ background: var(--primary-gradient);
1067
+ color: var(--text-on-primary);
1068
+ padding: 12px 16px;
1069
+ display: flex;
1070
+ justify-content: space-between;
1071
+ align-items: center;
1072
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
1073
+ position: relative;
1074
+ overflow: hidden;
1075
+ }
1076
+
1077
+ .header::before {
1078
+ content: '';
1079
+ position: absolute;
1080
+ top: 0;
1081
+ left: 0;
1082
+ right: 0;
1083
+ bottom: 0;
1084
+ background: linear-gradient(45deg, transparent 0%, rgba(255, 255, 255, 0.1) 50%, transparent 100%);
1085
+ animation: shimmer 3s infinite;
1086
+ }
1087
+
1088
+ @keyframes shimmer {
1089
+ 0% { transform: translateX(-100%); }
1090
+ 100% { transform: translateX(100%); }
1091
+ }
1092
+
1093
+ .header-title {
1094
+ display: flex;
1095
+ align-items: center;
1096
+ font-size: 16px;
1097
+ font-weight: bold;
1098
+ position: relative;
1099
+ z-index: 1;
1100
+ }
1101
+
1102
+ .header-actions {
1103
+ display: flex;
1104
+ gap: 8px;
1105
+ }
1106
+
1107
+ .profile-icon {
1108
+ width: 32px;
1109
+ height: 32px;
1110
+ border-radius: 50%;
1111
+ background: var(--secondary-gradient);
1112
+ margin-right: 12px;
1113
+ padding: 4px;
1114
+ box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.2);
1115
+ display: flex;
1116
+ align-items: center;
1117
+ justify-content: center;
1118
+ transition: transform 0.3s ease;
1119
+ }
1120
+
1121
+ .profile-icon:hover {
1122
+ transform: scale(1.1);
1123
+ }
1124
+
1125
+ .profile-icon img {
1126
+ width: 24px;
1127
+ height: 24px;
1128
+ filter: brightness(0) invert(1);
1129
+ }
1130
+
1131
+ .conversation {
1132
+ flex: 1;
1133
+ overflow-y: auto;
1134
+ padding: 20px;
1135
+ background-color: var(--vscode-editor-background);
1136
+ background-image:
1137
+ radial-gradient(circle at 10% 20%, rgba(255, 51, 51, 0.03) 0%, transparent 20%),
1138
+ radial-gradient(circle at 90% 80%, rgba(255, 102, 102, 0.03) 0%, transparent 20%);
1139
+ }
1140
+
1141
+ .message {
1142
+ margin-bottom: 20px;
1143
+ padding: 14px 18px;
1144
+ border-radius: 12px;
1145
+ max-width: 85%;
1146
+ word-wrap: break-word;
1147
+ animation: fadeIn 0.4s ease;
1148
+ box-shadow: var(--message-shadow);
1149
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
1150
+ }
1151
+
1152
+ .message:hover {
1153
+ transform: var(--hover-transform);
1154
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
1155
+ }
1156
+
1157
+ @keyframes fadeIn {
1158
+ from {
1159
+ opacity: 0;
1160
+ transform: translateY(20px);
1161
+ }
1162
+ to {
1163
+ opacity: 1;
1164
+ transform: translateY(0);
1165
+ }
1166
+ }
1167
+
1168
+ .user {
1169
+ background: var(--primary-gradient);
1170
+ color: var(--text-on-primary);
1171
+ align-self: flex-end;
1172
+ margin-left: auto;
1173
+ border-top-right-radius: 4px;
1174
+ }
1175
+
1176
+ .assistant {
1177
+ background-color: var(--vscode-editor-inactiveSelectionBackground);
1178
+ color: var(--vscode-foreground);
1179
+ align-self: flex-start;
1180
+ margin-right: auto;
1181
+ border-top-left-radius: 4px;
1182
+ position: relative;
1183
+ padding-left: 24px;
1184
+ }
1185
+
1186
+ .assistant::before {
1187
+ content: '';
1188
+ display: block;
1189
+ position: absolute;
1190
+ left: 0;
1191
+ top: 0;
1192
+ bottom: 0;
1193
+ width: 4px;
1194
+ background: var(--primary-gradient);
1195
+ border-radius: 4px 0 0 4px;
1196
+ }
1197
+
1198
+ .input-area {
1199
+ display: flex;
1200
+ padding: 16px;
1201
+ border-top: 1px solid var(--vscode-panel-border);
1202
+ background-color: var(--vscode-editor-background);
1203
+ position: relative;
1204
+ }
1205
+
1206
+ #query-input {
1207
+ flex: 1;
1208
+ padding: 14px;
1209
+ border: 2px solid var(--vscode-input-border);
1210
+ background-color: var(--vscode-input-background);
1211
+ color: var(--vscode-input-foreground);
1212
+ border-radius: 12px;
1213
+ outline: none;
1214
+ transition: all 0.3s ease;
1215
+ font-size: 14px;
1216
+ }
1217
+
1218
+ #query-input:focus {
1219
+ border-color: var(--primary-color);
1220
+ box-shadow: 0 0 0 3px rgba(255, 51, 51, 0.1);
1221
+ }
1222
+
1223
+ .send-button {
1224
+ margin-left: 10px;
1225
+ padding: 10px 16px;
1226
+ background: var(--primary-gradient);
1227
+ color: var(--text-on-primary);
1228
+ border: none;
1229
+ border-radius: 12px;
1230
+ cursor: pointer;
1231
+ font-weight: bold;
1232
+ transition: all 0.3s ease;
1233
+ box-shadow: 0 2px 6px rgba(255, 51, 51, 0.3);
1234
+ font-size: 13px;
1235
+ position: relative;
1236
+ overflow: hidden;
1237
+ }
1238
+
1239
+ .send-button::before {
1240
+ content: '';
1241
+ position: absolute;
1242
+ top: 0;
1243
+ left: 0;
1244
+ width: 100%;
1245
+ height: 100%;
1246
+ background: linear-gradient(45deg, transparent 0%, rgba(255, 255, 255, 0.2) 50%, transparent 100%);
1247
+ transform: translateX(-100%);
1248
+ transition: transform 0.6s ease;
1249
+ }
1250
+
1251
+ .send-button:hover {
1252
+ transform: var(--hover-transform);
1253
+ box-shadow: 0 4px 12px rgba(255, 51, 51, 0.4);
1254
+ }
1255
+
1256
+ .send-button:hover::before {
1257
+ transform: translateX(100%);
1258
+ }
1259
+
1260
+ .clear-button {
1261
+ margin-left: 8px;
1262
+ padding: 10px 16px;
1263
+ background-color: transparent;
1264
+ color: var(--vscode-errorForeground);
1265
+ border: 2px solid var(--vscode-errorForeground);
1266
+ border-radius: 12px;
1267
+ cursor: pointer;
1268
+ transition: all 0.3s ease;
1269
+ font-size: 13px;
1270
+ font-weight: 500;
1271
+ }
1272
+
1273
+ .clear-button:hover {
1274
+ background-color: var(--vscode-errorForeground);
1275
+ color: white;
1276
+ transform: var(--hover-transform);
1277
+ }
1278
+
1279
+ .lang-button {
1280
+ background-color: rgba(255, 255, 255, 0.1);
1281
+ color: var(--text-on-primary);
1282
+ border: 1px solid rgba(255, 255, 255, 0.2);
1283
+ border-radius: 6px;
1284
+ cursor: pointer;
1285
+ padding: 6px 12px;
1286
+ font-size: 12px;
1287
+ transition: all 0.3s ease;
1288
+ backdrop-filter: blur(4px);
1289
+ }
1290
+
1291
+ .config-button {
1292
+ background-color: rgba(255, 255, 255, 0.1);
1293
+ color: var(--text-on-primary);
1294
+ border: 1px solid rgba(255, 255, 255, 0.2);
1295
+ border-radius: 6px;
1296
+ cursor: pointer;
1297
+ padding: 6px 12px;
1298
+ font-size: 12px;
1299
+ transition: all 0.3s ease;
1300
+ backdrop-filter: blur(4px);
1301
+ }
1302
+
1303
+ .lang-button:hover, .config-button:hover {
1304
+ background-color: rgba(255, 255, 255, 0.2);
1305
+ transform: var(--hover-transform);
1306
+ }
1307
+
1308
+ .config-modal {
1309
+ display: none;
1310
+ position: fixed;
1311
+ top: 0;
1312
+ left: 0;
1313
+ width: 100%;
1314
+ height: 100%;
1315
+ background-color: rgba(0, 0, 0, 0.5);
1316
+ z-index: 1000;
1317
+ justify-content: center;
1318
+ align-items: center;
1319
+ }
1320
+
1321
+ .config-modal-content {
1322
+ background-color: var(--vscode-editor-background);
1323
+ border-radius: 8px;
1324
+ padding: 24px;
1325
+ width: 90%;
1326
+ max-width: 450px;
1327
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
1328
+ margin: 0 auto;
1329
+ }
1330
+
1331
+ .config-form {
1332
+ display: flex;
1333
+ flex-direction: column;
1334
+ align-items: stretch;
1335
+ width: 100%;
1336
+ }
1337
+
1338
+ .config-modal-header {
1339
+ display: flex;
1340
+ justify-content: space-between;
1341
+ align-items: center;
1342
+ margin-bottom: 20px;
1343
+ }
1344
+
1345
+ .config-modal-title {
1346
+ font-size: 18px;
1347
+ font-weight: bold;
1348
+ }
1349
+
1350
+ .config-modal-close {
1351
+ background: none;
1352
+ border: none;
1353
+ font-size: 20px;
1354
+ cursor: pointer;
1355
+ color: var(--vscode-foreground);
1356
+ }
1357
+
1358
+ .config-form-group {
1359
+ margin-bottom: 18px;
1360
+ width: 100%;
1361
+ }
1362
+
1363
+ .config-label {
1364
+ display: block;
1365
+ margin-bottom: 8px;
1366
+ font-weight: 500;
1367
+ }
1368
+
1369
+ .config-input {
1370
+ width: 100%;
1371
+ padding: 10px 12px;
1372
+ border-radius: 6px;
1373
+ border: 1px solid var(--vscode-input-border);
1374
+ background-color: var(--vscode-input-background);
1375
+ color: var(--vscode-input-foreground);
1376
+ box-sizing: border-box;
1377
+ }
1378
+
1379
+ .config-save-button {
1380
+ background: var(--primary-gradient);
1381
+ color: var(--text-on-primary);
1382
+ border: none;
1383
+ border-radius: 6px;
1384
+ padding: 10px 16px;
1385
+ font-weight: bold;
1386
+ cursor: pointer;
1387
+ margin-top: 12px;
1388
+ align-self: center;
1389
+ width: 80%;
1390
+ }
1391
+
1392
+ .loading {
1393
+ display: none;
1394
+ text-align: center;
1395
+ margin: 24px 0;
1396
+ padding: 24px;
1397
+ border-radius: 12px;
1398
+ background-color: rgba(0, 0, 0, 0.03);
1399
+ animation: pulse 2s infinite;
1400
+ }
1401
+
1402
+ @keyframes pulse {
1403
+ 0% { opacity: 0.6; }
1404
+ 50% { opacity: 1; }
1405
+ 100% { opacity: 0.6; }
1406
+ }
1407
+
1408
+ .spinner {
1409
+ display: inline-block;
1410
+ position: relative;
1411
+ width: 48px;
1412
+ height: 48px;
1413
+ }
1414
+
1415
+ .spinner::before {
1416
+ content: "";
1417
+ display: block;
1418
+ position: absolute;
1419
+ top: 0;
1420
+ left: 0;
1421
+ width: 100%;
1422
+ height: 100%;
1423
+ border-radius: 50%;
1424
+ border: 4px solid rgba(255, 51, 51, 0.1);
1425
+ border-top-color: var(--primary-color);
1426
+ animation: spin 1s cubic-bezier(0.68, -0.55, 0.265, 1.55) infinite;
1427
+ }
1428
+
1429
+ @keyframes spin {
1430
+ to { transform: rotate(360deg); }
1431
+ }
1432
+
1433
+ .loading-text {
1434
+ margin-top: 12px;
1435
+ color: var(--primary-color);
1436
+ font-weight: 500;
1437
+ font-size: 14px;
1438
+ }
1439
+
1440
+ pre {
1441
+ background-color: var(--vscode-textCodeBlock-background);
1442
+ padding: 16px;
1443
+ border-radius: 12px;
1444
+ overflow-x: auto;
1445
+ border-left: 4px solid var(--primary-color);
1446
+ margin: 12px 0;
1447
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
1448
+ }
1449
+
1450
+ code {
1451
+ font-family: var(--vscode-editor-font-family);
1452
+ font-size: 13px;
1453
+ }
1454
+
1455
+ .agent-working {
1456
+ font-style: italic;
1457
+ opacity: 0.9;
1458
+ background-color: rgba(255, 51, 51, 0.05);
1459
+ border-left: 4px solid var(--primary-color);
1460
+ padding: 16px;
1461
+ border-radius: 12px;
1462
+ margin: 12px 0;
1463
+ animation: pulse 2s infinite;
1464
+ }
1465
+
1466
+ .message-actions {
1467
+ display: flex;
1468
+ justify-content: flex-end;
1469
+ margin-top: 5px;
1470
+ }
1471
+
1472
+ .restore-button {
1473
+ background-color: var(--vscode-button-secondaryBackground);
1474
+ color: var(--vscode-button-secondaryForeground);
1475
+ border: none;
1476
+ border-radius: 3px;
1477
+ padding: 2px 8px;
1478
+ font-size: 11px;
1479
+ cursor: pointer;
1480
+ margin-left: 5px;
1481
+ display: flex;
1482
+ align-items: center;
1483
+ transition: all 0.2s ease;
1484
+ }
1485
+
1486
+ .restore-button:hover {
1487
+ background-color: var(--vscode-button-secondaryHoverBackground);
1488
+ transform: translateY(-1px);
1489
+ }
1490
+
1491
+ .restore-button::before {
1492
+ content: "↺";
1493
+ margin-right: 4px;
1494
+ font-size: 12px;
1495
+ }
1496
+
1497
+ /* Chat sidebar styles */
1498
+ .app-container {
1499
+ display: flex;
1500
+ height: 100vh;
1501
+ width: 100%;
1502
+ }
1503
+
1504
+ .chat-sidebar {
1505
+ width: 250px;
1506
+ background-color: var(--vscode-sideBar-background);
1507
+ border-right: 1px solid var(--vscode-sideBar-border);
1508
+ display: flex;
1509
+ flex-direction: column;
1510
+ overflow: hidden;
1511
+ }
1512
+
1513
+ .chat-sidebar-header {
1514
+ padding: 10px;
1515
+ display: flex;
1516
+ justify-content: space-between;
1517
+ align-items: center;
1518
+ border-bottom: 1px solid var(--vscode-sideBar-border);
1519
+ }
1520
+
1521
+ .new-chat-button {
1522
+ background-color: var(--primary-color);
1523
+ color: var(--text-on-primary);
1524
+ border: none;
1525
+ border-radius: 3px;
1526
+ padding: 5px 10px;
1527
+ cursor: pointer;
1528
+ font-size: 12px;
1529
+ display: flex;
1530
+ align-items: center;
1531
+ }
1532
+
1533
+ .new-chat-button:hover {
1534
+ background-color: #cc0000;
1535
+ }
1536
+
1537
+ .new-chat-button svg {
1538
+ margin-right: 5px;
1539
+ width: 14px;
1540
+ height: 14px;
1541
+ }
1542
+
1543
+ .chat-list {
1544
+ flex: 1;
1545
+ overflow-y: auto;
1546
+ padding: 5px;
1547
+ }
1548
+
1549
+ .chat-item {
1550
+ padding: 8px 10px;
1551
+ margin-bottom: 2px;
1552
+ border-radius: 4px;
1553
+ cursor: pointer;
1554
+ display: flex;
1555
+ align-items: center;
1556
+ justify-content: space-between;
1557
+ transition: background-color 0.2s;
1558
+ }
1559
+
1560
+ .chat-item:hover {
1561
+ background-color: var(--vscode-list-hoverBackground);
1562
+ }
1563
+
1564
+ .chat-item.active {
1565
+ background-color: var(--primary-color);
1566
+ color: var(--text-on-primary);
1567
+ }
1568
+
1569
+ .chat-title {
1570
+ flex: 1;
1571
+ white-space: nowrap;
1572
+ overflow: hidden;
1573
+ text-overflow: ellipsis;
1574
+ font-size: 13px;
1575
+ }
1576
+
1577
+ .chat-actions {
1578
+ display: none;
1579
+ margin-left: 5px;
1580
+ }
1581
+
1582
+ .chat-item:hover .chat-actions {
1583
+ display: flex;
1584
+ }
1585
+
1586
+ .chat-action-button {
1587
+ background: none;
1588
+ border: none;
1589
+ color: var(--vscode-foreground);
1590
+ cursor: pointer;
1591
+ padding: 2px 5px;
1592
+ font-size: 12px;
1593
+ opacity: 0.7;
1594
+ }
1595
+
1596
+ .chat-action-button:hover {
1597
+ opacity: 1;
1598
+ }
1599
+
1600
+ .main-content {
1601
+ flex: 1;
1602
+ display: flex;
1603
+ flex-direction: column;
1604
+ overflow: hidden;
1605
+ }
1606
+ </style>
1607
+ </head>
1608
+ <body>
1609
+ <div class="app-container">
1610
+ <div class="chat-sidebar">
1611
+ <div class="chat-sidebar-header">
1612
+ <h3>Chats</h3>
1613
+ <button class="new-chat-button" id="new-chat-button">
1614
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1615
+ <line x1="12" y1="5" x2="12" y2="19"></line>
1616
+ <line x1="5" y1="12" x2="19" y2="12"></line>
1617
+ </svg>
1618
+ New Chat
1619
+ </button>
1620
+ </div>
1621
+ <div class="chat-list" id="chat-list">
1622
+ <!-- Chat items will be added here dynamically -->
1623
+ </div>
1624
+ </div>
1625
+
1626
+ <div class="main-content">
1627
+ <div class="header">
1628
+ <div class="header-title">
1629
+ <div class="profile-icon">
1630
+ <img src="${infinityIconPath}" alt="AI" />
1631
+ </div>
1632
+ <span>Samantha Coder</span>
1633
+ </div>
1634
+ <div class="header-actions">
1635
+ <button class="config-button" id="config-button">⚙️ Config</button>
1636
+ <button class="lang-button" id="lang-button">${translations[this._language].changeLanguageButton}</button>
1637
+ </div>
1638
+ </div>
1639
+
1640
+ <div class="config-modal" id="config-modal">
1641
+ <div class="config-modal-content">
1642
+ <div class="config-modal-header">
1643
+ <div class="config-modal-title">Configuration</div>
1644
+ <button class="config-modal-close" id="config-modal-close">&times;</button>
1645
+ </div>
1646
+ <div class="config-form">
1647
+ <div class="config-form-group">
1648
+ <label class="config-label" for="model-input">Model Name</label>
1649
+ <input type="text" id="model-input" class="config-input" placeholder="Enter model name">
1650
+ </div>
1651
+ <div class="config-form-group">
1652
+ <label class="config-label" for="api-base-url-input">API Base URL</label>
1653
+ <input type="text" id="api-base-url-input" class="config-input" placeholder="Enter API base URL">
1654
+ </div>
1655
+ <div class="config-form-group">
1656
+ <label class="config-label" for="api-key-input">API Key (optional)</label>
1657
+ <input type="password" id="api-key-input" class="config-input" placeholder="Enter API key">
1658
+ </div>
1659
+ <div class="config-form-group">
1660
+ <label class="config-label" for="max-tokens-input">Max Tokens</label>
1661
+ <input type="number" id="max-tokens-input" class="config-input" placeholder="Enter max tokens">
1662
+ </div>
1663
+ <button class="config-save-button" id="config-save-button">Save Configuration</button>
1664
+ </div>
1665
+ </div>
1666
+ </div>
1667
+
1668
+ <div class="conversation" id="conversation">
1669
+ <div class="message assistant">
1670
+ ${translations[this._language].welcomeMessage}
1671
+ </div>
1672
+ </div>
1673
+
1674
+ <div class="loading" id="loading">
1675
+ <div class="spinner"></div>
1676
+ <div class="loading-text">${translations[this._language].loadingText}</div>
1677
+ </div>
1678
+
1679
+ <div class="input-area">
1680
+ <input type="text" id="query-input" placeholder="${translations[this._language].inputPlaceholder}" />
1681
+ <button class="send-button" id="send-button">${translations[this._language].sendButton}</button>
1682
+ <button class="clear-button" id="clear-button">${translations[this._language].clearButton}</button>
1683
+ </div>
1684
+ </div>
1685
+ </div>
1686
+
1687
+ <script>
1688
+ (function() {
1689
+ const vscode = acquireVsCodeApi();
1690
+ const conversationEl = document.getElementById('conversation');
1691
+ const queryInput = document.getElementById('query-input');
1692
+ const sendButton = document.getElementById('send-button');
1693
+ const clearButton = document.getElementById('clear-button');
1694
+ const langButton = document.getElementById('lang-button');
1695
+ const loadingEl = document.getElementById('loading');
1696
+ const chatListEl = document.getElementById('chat-list');
1697
+ const newChatButton = document.getElementById('new-chat-button');
1698
+ const configButton = document.getElementById('config-button');
1699
+ const configModal = document.getElementById('config-modal');
1700
+ const configModalClose = document.getElementById('config-modal-close');
1701
+ const configSaveButton = document.getElementById('config-save-button');
1702
+ const modelInput = document.getElementById('model-input');
1703
+ const apiBaseUrlInput = document.getElementById('api-base-url-input');
1704
+ const apiKeyInput = document.getElementById('api-key-input');
1705
+ const maxTokensInput = document.getElementById('max-tokens-input');
1706
+
1707
+ // Current translations
1708
+ let currentTranslations = ${JSON.stringify(translations[this._language])};
1709
+
1710
+ // Current chat ID
1711
+ let currentChatId = null;
1712
+
1713
+ // Format messages with markdown-like syntax
1714
+ function formatMessage(text) {
1715
+ // Handle code blocks
1716
+ text = text.replace(/\`\`\`([^\`]+)\`\`\`/g, '<pre><code>$1</code></pre>');
1717
+
1718
+ // Handle inline code
1719
+ text = text.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
1720
+
1721
+ // Handle line breaks
1722
+ text = text.replace(/\\n/g, '<br>');
1723
+
1724
+ return text;
1725
+ }
1726
+
1727
+ function addMessageToUI(message) {
1728
+ const messageEl = document.createElement('div');
1729
+ messageEl.className = \`message \${message.role}\`;
1730
+ messageEl.dataset.timestamp = message.timestamp;
1731
+
1732
+ // Add agent-working class if it's the agent working message
1733
+ if (message.role === 'assistant' && message.content.includes(currentTranslations.workingMessage)) {
1734
+ messageEl.classList.add('agent-working');
1735
+ }
1736
+
1737
+ const contentDiv = document.createElement('div');
1738
+ contentDiv.className = 'message-content';
1739
+ contentDiv.innerHTML = formatMessage(message.content);
1740
+
1741
+ // Simply add content to the message for all message types
1742
+ messageEl.appendChild(contentDiv);
1743
+
1744
+ conversationEl.appendChild(messageEl);
1745
+ conversationEl.scrollTop = conversationEl.scrollHeight;
1746
+ }
1747
+
1748
+ // Update UI text elements based on language
1749
+ function updateUILanguage(trans) {
1750
+ currentTranslations = trans;
1751
+ document.documentElement.lang = currentTranslations.changeLanguageButton === 'EN' ? 'pt-br' : 'en';
1752
+
1753
+ // Update placeholders and button texts
1754
+ queryInput.placeholder = currentTranslations.inputPlaceholder;
1755
+ sendButton.textContent = currentTranslations.sendButton;
1756
+ clearButton.textContent = currentTranslations.clearButton;
1757
+ langButton.textContent = currentTranslations.changeLanguageButton;
1758
+
1759
+ // Update loading text
1760
+ document.querySelector('.loading-text').textContent = currentTranslations.loadingText;
1761
+ }
1762
+
1763
+ // Handle sending query
1764
+ function sendQuery() {
1765
+ const query = queryInput.value.trim();
1766
+ if (query) {
1767
+ vscode.postMessage({
1768
+ type: 'sendQuery',
1769
+ value: query
1770
+ });
1771
+ queryInput.value = '';
1772
+ }
1773
+ }
1774
+
1775
+ // Create a chat item element
1776
+ function createChatItem(chat) {
1777
+ const chatItem = document.createElement('div');
1778
+ chatItem.className = 'chat-item';
1779
+ if (chat.id === currentChatId) {
1780
+ chatItem.classList.add('active');
1781
+ }
1782
+ chatItem.dataset.chatId = chat.id;
1783
+
1784
+ const titleSpan = document.createElement('span');
1785
+ titleSpan.className = 'chat-title';
1786
+ titleSpan.textContent = chat.title;
1787
+
1788
+ const actionsDiv = document.createElement('div');
1789
+ actionsDiv.className = 'chat-actions';
1790
+
1791
+ const renameButton = document.createElement('button');
1792
+ renameButton.className = 'chat-action-button';
1793
+ renameButton.innerHTML = '✏️';
1794
+ renameButton.title = 'Rename';
1795
+ renameButton.onclick = (e) => {
1796
+ e.stopPropagation();
1797
+ const newTitle = prompt('Enter new chat title:', chat.title);
1798
+ if (newTitle && newTitle.trim() !== '') {
1799
+ vscode.postMessage({
1800
+ type: 'renameChat',
1801
+ chatId: chat.id,
1802
+ newTitle: newTitle.trim()
1803
+ });
1804
+ }
1805
+ };
1806
+
1807
+ const deleteButton = document.createElement('button');
1808
+ deleteButton.className = 'chat-action-button';
1809
+ deleteButton.innerHTML = '🗑️';
1810
+ deleteButton.title = 'Delete';
1811
+ deleteButton.onclick = (e) => {
1812
+ e.stopPropagation();
1813
+ // Prevent deleting the only chat
1814
+ const chatCount = document.querySelectorAll('.chat-item').length;
1815
+ if (chatCount <= 1) {
1816
+ alert('Cannot delete the only chat. Create a new chat first.');
1817
+ return;
1818
+ }
1819
+
1820
+ if (confirm('Are you sure you want to delete this chat?')) {
1821
+ vscode.postMessage({
1822
+ type: 'deleteChat',
1823
+ chatId: chat.id
1824
+ });
1825
+ }
1826
+ };
1827
+
1828
+ actionsDiv.appendChild(renameButton);
1829
+ actionsDiv.appendChild(deleteButton);
1830
+
1831
+ chatItem.appendChild(titleSpan);
1832
+ chatItem.appendChild(actionsDiv);
1833
+
1834
+ chatItem.onclick = () => {
1835
+ // Don't switch if already active
1836
+ if (chat.id !== currentChatId) {
1837
+ vscode.postMessage({
1838
+ type: 'switchChat',
1839
+ chatId: chat.id
1840
+ });
1841
+ }
1842
+ };
1843
+
1844
+ return chatItem;
1845
+ }
1846
+
1847
+ // Update the chat list
1848
+ function updateChatList(chats, activeChatId) {
1849
+ chatListEl.innerHTML = '';
1850
+ currentChatId = activeChatId;
1851
+
1852
+ chats.forEach(chat => {
1853
+ chatListEl.appendChild(createChatItem(chat));
1854
+ });
1855
+ }
1856
+
1857
+ // Event listeners
1858
+ sendButton.addEventListener('click', sendQuery);
1859
+
1860
+ queryInput.addEventListener('keydown', (e) => {
1861
+ if (e.key === 'Enter') {
1862
+ sendQuery();
1863
+ }
1864
+ });
1865
+
1866
+ clearButton.addEventListener('click', () => {
1867
+ vscode.postMessage({
1868
+ type: 'clearConversation'
1869
+ });
1870
+ });
1871
+
1872
+ langButton.addEventListener('click', () => {
1873
+ vscode.postMessage({
1874
+ type: 'toggleLanguage'
1875
+ });
1876
+ });
1877
+
1878
+ newChatButton.addEventListener('click', () => {
1879
+ const title = prompt('Enter chat title:', 'New Chat');
1880
+ if (title && title.trim() !== '') {
1881
+ vscode.postMessage({
1882
+ type: 'createNewChat',
1883
+ title: title.trim()
1884
+ });
1885
+ } else {
1886
+ vscode.postMessage({
1887
+ type: 'createNewChat'
1888
+ });
1889
+ }
1890
+ });
1891
+
1892
+ // Config button event
1893
+ configButton.addEventListener('click', () => {
1894
+ // Request current configuration values
1895
+ vscode.postMessage({
1896
+ type: 'getConfiguration'
1897
+ });
1898
+
1899
+ // Show modal
1900
+ configModal.style.display = 'flex';
1901
+ });
1902
+
1903
+ // Close modal when clicking close button
1904
+ configModalClose.addEventListener('click', () => {
1905
+ configModal.style.display = 'none';
1906
+ });
1907
+
1908
+ // Close modal when clicking outside the modal content
1909
+ configModal.addEventListener('click', (e) => {
1910
+ if (e.target === configModal) {
1911
+ configModal.style.display = 'none';
1912
+ }
1913
+ });
1914
+
1915
+ // Save configuration
1916
+ configSaveButton.addEventListener('click', () => {
1917
+ const modelName = modelInput.value.trim();
1918
+ const apiBaseUrl = apiBaseUrlInput.value.trim();
1919
+ const apiKey = apiKeyInput.value.trim();
1920
+ const maxTokens = maxTokensInput.value.trim();
1921
+
1922
+ vscode.postMessage({
1923
+ type: 'saveConfiguration',
1924
+ modelName: modelName,
1925
+ apiBaseUrl: apiBaseUrl,
1926
+ apiKey: apiKey,
1927
+ maxTokens: maxTokens
1928
+ });
1929
+
1930
+ configModal.style.display = 'none';
1931
+ });
1932
+
1933
+ // Handle messages from the extension
1934
+ window.addEventListener('message', event => {
1935
+ const message = event.data;
1936
+ switch (message.type) {
1937
+ case 'addMessage':
1938
+ addMessageToUI(message.message);
1939
+ break;
1940
+ case 'clearConversation':
1941
+ conversationEl.innerHTML = '';
1942
+ addMessageToUI({
1943
+ role: 'assistant',
1944
+ content: currentTranslations.welcomeMessage,
1945
+ timestamp: new Date().toISOString()
1946
+ });
1947
+ break;
1948
+ case 'setLoading':
1949
+ loadingEl.style.display = message.isLoading ? 'block' : 'none';
1950
+ break;
1951
+ case 'updateLanguage':
1952
+ updateUILanguage(message.translations);
1953
+ break;
1954
+ case 'restoreConversation':
1955
+ conversationEl.innerHTML = '';
1956
+ if (Array.isArray(message.messages)) {
1957
+ message.messages.forEach(msg => {
1958
+ addMessageToUI(msg);
1959
+ });
1960
+ }
1961
+ break;
1962
+ case 'updateChats':
1963
+ updateChatList(message.chats, message.currentChatId);
1964
+ break;
1965
+ case 'switchChat':
1966
+ currentChatId = message.chatId;
1967
+ conversationEl.innerHTML = '';
1968
+ if (Array.isArray(message.messages)) {
1969
+ message.messages.forEach(msg => {
1970
+ addMessageToUI(msg);
1971
+ });
1972
+ }
1973
+ break;
1974
+ case 'configuration':
1975
+ // Update configuration form with values from extension
1976
+ modelInput.value = message.model || '';
1977
+ apiBaseUrlInput.value = message.apiBaseUrl || '';
1978
+ apiKeyInput.value = message.apiKey || '';
1979
+ maxTokensInput.value = message.maxTokens || '';
1980
+ break;
1981
+ }
1982
+ });
1983
+ })();
1984
+ </script>
1985
+ </body>
1986
+ </html>`;
1987
+ }
1988
+
1989
+ public getAgentUtils(): AgentUtils {
1990
+ return this._agentUtils;
1991
+ }
1992
+
1993
+ public toggleLanguage() {
1994
+ // Toggle between English and Portuguese
1995
+ this._language = this._language === 'en' ? 'pt-br' : 'en';
1996
+
1997
+ // Update the configuration
1998
+ const config = vscode.workspace.getConfiguration('aiAssistant');
1999
+ config.update('language', this._language, true);
2000
+
2001
+ // Update the webview content while preserving chat history
2002
+ if (this._view) {
2003
+ this._view.webview.html = this._getHtmlForWebview(this._view.webview);
2004
+
2005
+ // Send current chat state to webview
2006
+ this._postMessageToWebview({
2007
+ type: 'updateLanguage',
2008
+ translations: translations[this._language]
2009
+ });
2010
+
2011
+ // Restore current chat messages
2012
+ const currentChat = this._getCurrentChat();
2013
+ if (currentChat) {
2014
+ this._postMessageToWebview({
2015
+ type: 'restoreConversation',
2016
+ messages: this._getMessagesForDisplay(currentChat.messages)
2017
+ });
2018
+ }
2019
+
2020
+ // Update chat list
2021
+ this._postMessageToWebview({
2022
+ type: 'updateChats',
2023
+ chats: this._chats,
2024
+ currentChatId: this._currentChatId
2025
+ });
2026
+ }
2027
+ }
2028
+
2029
+ // Take a snapshot of the current state
2030
+ private async _takeStateSnapshot(messageId: string) {
2031
+ try {
2032
+ // Create a snapshot even if no editor is active
2033
+ const snapshot: StateSnapshot = {
2034
+ files: {},
2035
+ timestamp: new Date().toISOString(),
2036
+ chatId: this._currentChatId,
2037
+ trackedFiles: [...this._trackedFiles] // Store the set of tracked files at this point
2038
+ };
2039
+
2040
+ // Track open editors and their content
2041
+ for (const editor of vscode.window.visibleTextEditors) {
2042
+ const document = editor.document;
2043
+ snapshot.files[document.uri.fsPath] = {
2044
+ exists: true,
2045
+ content: document.getText(),
2046
+ version: document.version
2047
+ };
2048
+ }
2049
+
2050
+ // Store the snapshot
2051
+ this._conversationHistory[messageId] = snapshot;
2052
+
2053
+ // If there's an active editor, mark its file as the primary focus
2054
+ const activeEditor = vscode.window.activeTextEditor;
2055
+ if (activeEditor) {
2056
+ const document = activeEditor.document;
2057
+ if (snapshot.files[document.uri.fsPath]) {
2058
+ snapshot.primaryFile = document.uri.fsPath;
2059
+ }
2060
+ }
2061
+
2062
+ // Reset the tracked files set after taking a snapshot
2063
+ this._trackedFiles.clear();
2064
+
2065
+ // Re-add the currently open files to the tracked set
2066
+ vscode.window.visibleTextEditors.forEach(editor => {
2067
+ this._trackedFiles.add(editor.document.uri.fsPath);
2068
+ });
2069
+ } catch (error) {
2070
+ console.error('Error taking state snapshot:', error);
2071
+ }
2072
+ }
2073
+
2074
+ // Restore to the state before a specific message
2075
+ public async restoreToState(messageId: string) {
2076
+ try {
2077
+ const snapshot = this._conversationHistory[messageId] as StateSnapshot;
2078
+ if (!snapshot) {
2079
+ vscode.window.showErrorMessage('No previous state found for this message');
2080
+ return;
2081
+ }
2082
+
2083
+ // Get the current chat
2084
+ const currentChat = this._getCurrentChat();
2085
+ if (!currentChat) {
2086
+ return;
2087
+ }
2088
+
2089
+ // Find the message in the conversation
2090
+ const messageIndex = currentChat.messages.findIndex(msg => msg.timestamp === messageId);
2091
+ if (messageIndex === -1) {
2092
+ vscode.window.showErrorMessage('Message not found in conversation');
2093
+ return;
2094
+ }
2095
+
2096
+ // Get a list of files that were created or modified after this snapshot
2097
+ const filesCreatedAfter = new Set<string>(this._trackedFiles);
2098
+
2099
+ // If the snapshot has trackedFiles, remove them from the current set
2100
+ if (snapshot.trackedFiles) {
2101
+ snapshot.trackedFiles.forEach(file => {
2102
+ filesCreatedAfter.delete(file);
2103
+ });
2104
+ }
2105
+
2106
+ // Check if there are files that were created after this message
2107
+ if (filesCreatedAfter.size > 0) {
2108
+ // Ask user if they want to delete newly created files
2109
+ const deleteNewFiles = await vscode.window.showInformationMessage(
2110
+ `${filesCreatedAfter.size} file(s) were created after this message. Delete them?`,
2111
+ 'Yes', 'No'
2112
+ );
2113
+
2114
+ if (deleteNewFiles === 'Yes') {
2115
+ // Delete files created after the snapshot
2116
+ const deletedCount = await this._deleteFiles(filesCreatedAfter);
2117
+ if (deletedCount > 0) {
2118
+ vscode.window.showInformationMessage(`Deleted ${deletedCount} file(s) created after the message`);
2119
+ }
2120
+ }
2121
+ }
2122
+
2123
+ // Implement restoration logic
2124
+ if (snapshot.files) {
2125
+ // Keep track of successful restorations
2126
+ let restoredCount = 0;
2127
+ let primaryFileOpened = false;
2128
+
2129
+ // Process each file in the snapshot
2130
+ for (const [filePath, fileData] of Object.entries(snapshot.files)) {
2131
+ try {
2132
+ // Restore file content if it existed at the time of snapshot
2133
+ if (fileData.exists && fileData.content) {
2134
+ await this._agentUtils.writeFile(filePath, fileData.content);
2135
+ restoredCount++;
2136
+
2137
+ // Open the primary file (the one that was active when snapshot was taken)
2138
+ if (snapshot.primaryFile === filePath && !primaryFileOpened) {
2139
+ const uri = vscode.Uri.file(filePath);
2140
+ const document = await vscode.workspace.openTextDocument(uri);
2141
+ await vscode.window.showTextDocument(document);
2142
+ primaryFileOpened = true;
2143
+ }
2144
+ }
2145
+ } catch (fileError) {
2146
+ console.error(`Error restoring file ${filePath}:`, fileError);
2147
+ }
2148
+ }
2149
+
2150
+ // If we have a primary file but couldn't open it, try opening the first restored file
2151
+ if (!primaryFileOpened && restoredCount > 0 && snapshot.primaryFile) {
2152
+ try {
2153
+ const uri = vscode.Uri.file(snapshot.primaryFile);
2154
+ const document = await vscode.workspace.openTextDocument(uri);
2155
+ await vscode.window.showTextDocument(document);
2156
+ } catch (error) {
2157
+ console.error('Error opening primary file:', error);
2158
+ }
2159
+ }
2160
+
2161
+ // Show restoration success message
2162
+ if (restoredCount > 0) {
2163
+ vscode.window.showInformationMessage(`Restored ${restoredCount} file(s) to state before message`);
2164
+ } else {
2165
+ // If no files were in the snapshot, this was likely the start of the conversation
2166
+ vscode.window.showInformationMessage('Restored to initial state before any changes');
2167
+ }
2168
+ }
2169
+
2170
+ // Remove all messages after this one from the conversation
2171
+ currentChat.messages = currentChat.messages.slice(0, messageIndex + 1);
2172
+ currentChat.updatedAt = new Date().toISOString();
2173
+
2174
+ // Update the webview to reflect the changes
2175
+ await this._postMessageToWebview({
2176
+ type: 'restoreConversation',
2177
+ messages: this._getMessagesForDisplay(currentChat.messages)
2178
+ });
2179
+
2180
+ // Reset the tracked files to the state at the time of the snapshot
2181
+ this._trackedFiles.clear();
2182
+ if (snapshot.trackedFiles) {
2183
+ snapshot.trackedFiles.forEach(file => {
2184
+ this._trackedFiles.add(file);
2185
+ });
2186
+ }
2187
+ } catch (error) {
2188
+ console.error('Error restoring state:', error);
2189
+ vscode.window.showErrorMessage(`Failed to restore state: ${error instanceof Error ? error.message : String(error)}`);
2190
+ }
2191
+ }
2192
+
2193
+ // Helper method to delete multiple files
2194
+ private async _deleteFiles(filesToDelete: Set<string>): Promise<number> {
2195
+ let deletedCount = 0;
2196
+
2197
+ for (const filePath of filesToDelete) {
2198
+ try {
2199
+ const uri = vscode.Uri.file(filePath);
2200
+
2201
+ // Check if the file exists before attempting to delete
2202
+ try {
2203
+ await vscode.workspace.fs.stat(uri);
2204
+
2205
+ // File exists, delete it
2206
+ await vscode.workspace.fs.delete(uri, { useTrash: false });
2207
+ deletedCount++;
2208
+ } catch (statError) {
2209
+ // File doesn't exist, skip it
2210
+ console.log(`File ${filePath} doesn't exist, skipping deletion`);
2211
+ }
2212
+ } catch (error) {
2213
+ console.error(`Error deleting file ${filePath}:`, error);
2214
+ }
2215
+ }
2216
+
2217
+ return deletedCount;
2218
+ }
2219
+
2220
+ private async _getConfiguration() {
2221
+ if (!this._view) {
2222
+ return;
2223
+ }
2224
+
2225
+ const config = vscode.workspace.getConfiguration('aiAssistant');
2226
+ const model = config.get('model');
2227
+ const apiKey = config.get('apiKey');
2228
+ const apiBaseUrl = config.get('apiBaseUrl');
2229
+ const maxTokens = config.get('maxTokens');
2230
+
2231
+ this._postMessageToWebview({
2232
+ type: 'configuration',
2233
+ model,
2234
+ apiKey,
2235
+ apiBaseUrl,
2236
+ maxTokens
2237
+ });
2238
+ }
2239
+
2240
+ private async _saveConfiguration(modelName: string, apiBaseUrl: string, apiKey: string, maxTokens: string) {
2241
+ const config = vscode.workspace.getConfiguration('aiAssistant');
2242
+
2243
+ if (modelName) {
2244
+ await config.update('model', modelName, vscode.ConfigurationTarget.Global);
2245
+ }
2246
+
2247
+ if (apiBaseUrl) {
2248
+ await config.update('apiBaseUrl', apiBaseUrl, vscode.ConfigurationTarget.Global);
2249
+ }
2250
+
2251
+ if (apiKey) {
2252
+ await config.update('apiKey', apiKey, vscode.ConfigurationTarget.Global);
2253
+ }
2254
+
2255
+ if (maxTokens) {
2256
+ const maxTokensNum = parseInt(maxTokens, 10);
2257
+ if (!isNaN(maxTokensNum) && maxTokensNum > 0) {
2258
+ await config.update('maxTokens', maxTokensNum, vscode.ConfigurationTarget.Global);
2259
+ }
2260
+ }
2261
+
2262
+ vscode.window.showInformationMessage('AI Assistant configuration updated');
2263
+ }
2264
+ }