From 8d7c8e3a4a775541e9c4aa2fc94eab8ddcbd5269 Mon Sep 17 00:00:00 2001 From: Brennon Overton Date: Sat, 12 Jul 2025 14:59:18 -0500 Subject: [PATCH 1/2] fix: improve terminal paste functionality and clean up UI - Fix paste formatting by using xterm.js native paste() method - Remove custom keyboard paste handler to allow native handling - Remove debug button from main sessions view for cleaner UI --- client/src/views/HomeView.vue | 10 ---------- client/src/views/TerminalView.vue | 12 +----------- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/client/src/views/HomeView.vue b/client/src/views/HomeView.vue index aa9cac7..fe5bcc1 100644 --- a/client/src/views/HomeView.vue +++ b/client/src/views/HomeView.vue @@ -8,16 +8,6 @@

SSH Sessions

- - - - - Debug Tool -
+ + +
+
+ + +
+
+ + +
+
+ + +
+
@@ -238,7 +323,6 @@ type="text" class="w-full p-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-slate-700 dark:text-white" placeholder="https://your-api-endpoint.com/v1" - :disabled="formValues['llm_provider'] !== 'custom'" />

Enter the base URL for your OpenAI-compatible API (including /v1 if needed) @@ -254,7 +338,6 @@ :type="showSensitive[setting.id] ? 'text' : 'password'" class="w-full p-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-indigo-500 dark:bg-slate-700 dark:text-white" placeholder="Enter your API key" - :disabled="formValues['llm_provider'] !== 'custom'" />

+ + +
+ +

+ Choose the Gemini model to use for AI assistance. 2.5 Flash offers the best price-performance balance. +

+
@@ -377,7 +477,56 @@ const visibleCategories = computed(() => { // Computed property to get settings for the active category const activeCategorySettings = computed(() => { - return settingsStore.getSettingsByCategory(activeCategory.value); + const allCategorySettings = settingsStore.getSettingsByCategory(activeCategory.value); + + // If not in LLM category, return all settings + if (activeCategory.value !== 'llm') { + return allCategorySettings; + } + + // Filter LLM settings based on selected provider + const selectedProvider = formValues.value['llm_provider'] || 'openai'; + + const filteredSettings = allCategorySettings.filter(setting => { + // Always show the provider selector + if (setting.id === 'llm_provider') { + return true; + } + + // Show settings based on selected provider + switch (selectedProvider) { + case 'openai': + return ['openai_api_key', 'openai_model'].includes(setting.id); + case 'gemini': + return ['gemini_api_key', 'gemini_model'].includes(setting.id); + case 'ollama': + return ['ollama_url', 'ollama_model'].includes(setting.id); + case 'custom': + return ['custom_api_url', 'custom_api_key', 'custom_model'].includes(setting.id); + default: + return false; + } + }); + + // Sort settings to ensure proper order: provider first, then API key, then model/url, then model + const settingOrder = { + 'llm_provider': 1, + 'openai_api_key': 2, + 'openai_model': 3, + 'gemini_api_key': 2, + 'gemini_model': 3, + 'ollama_url': 2, + 'ollama_model': 3, + 'custom_api_url': 2, + 'custom_api_key': 3, + 'custom_model': 4 + }; + + return filteredSettings.sort((a, b) => { + const orderA = settingOrder[a.id] || 999; + const orderB = settingOrder[b.id] || 999; + return orderA - orderB; + }); }); // Computed property to check if there are any changes @@ -416,7 +565,7 @@ const getPlaceholder = (setting) => { // Method to check if a setting has a special input type const isSpecialInput = (id) => { - return ['llm_provider', 'openai_model', 'ollama_model', 'custom_api_url', 'custom_api_key', 'custom_model'].includes(id); + return ['llm_provider', 'openai_model', 'ollama_model', 'custom_api_url', 'custom_api_key', 'custom_model', 'gemini_api_key', 'gemini_model'].includes(id); }; // Method to toggle visibility of sensitive values diff --git a/server/src/db/migration.js b/server/src/db/migration.js index d5f00a1..02fbb43 100644 --- a/server/src/db/migration.js +++ b/server/src/db/migration.js @@ -190,6 +190,8 @@ async function insertDefaultSettings() { { id: 'custom_api_url', name: 'Custom API URL', value: '', category: 'llm', description: 'Base URL for custom OpenAI-compatible API', is_sensitive: 0 }, { id: 'custom_api_key', name: 'Custom API Key', value: '', category: 'llm', description: 'API key for custom OpenAI-compatible API', is_sensitive: 1 }, { id: 'custom_model', name: 'Custom Model', value: 'gpt-3.5-turbo', category: 'llm', description: 'Model name for custom API', is_sensitive: 0 }, + { id: 'gemini_api_key', name: 'Gemini API Key', value: '', category: 'llm', description: 'API key for Google Gemini', is_sensitive: 1 }, + { id: 'gemini_model', name: 'Gemini Model', value: 'gemini-2.5-flash', category: 'llm', description: 'Model name for Gemini', is_sensitive: 0 }, // Encryption settings { id: 'encryption_key', name: 'Encryption Key', value: '736f4149702aae82ab6e45e64d977e3c6c1e9f7b29b368f61cafab1b9c2cc3b2', category: 'security', description: 'Encryption key for sensitive data', is_sensitive: 1 }, diff --git a/server/src/services/llmService.js b/server/src/services/llmService.js index b77e719..0524884 100644 --- a/server/src/services/llmService.js +++ b/server/src/services/llmService.js @@ -33,6 +33,10 @@ class LLMService { this.baseUrl = await settingsService.getSettingValue('custom_api_url', userId); this.apiKey = await settingsService.getSettingValue('custom_api_key', userId); this.model = await settingsService.getSettingValue('custom_model', userId); + } else if (this.provider === 'gemini') { + this.baseUrl = 'https://generativelanguage.googleapis.com/v1beta'; + this.apiKey = await settingsService.getSettingValue('gemini_api_key', userId); + this.model = await settingsService.getSettingValue('gemini_model', userId); } else { // Ollama this.baseUrl = await settingsService.getSettingValue('ollama_url', userId); @@ -85,6 +89,8 @@ class LLMService { let response; if (this.provider === 'openai' || this.provider === 'custom') { response = await this.callOpenAI(history); + } else if (this.provider === 'gemini') { + response = await this.callGemini(history); } else { response = await this.callOllama(history); } @@ -409,6 +415,108 @@ Please provide your response in JSON format with the following structure: }; } } + + async callGemini(messages) { + // Validate Gemini configuration + if (!this.apiKey || this.apiKey.trim() === '') { + throw new Error('Gemini API key not configured'); + } + + // Convert chat messages to Gemini format + const contents = []; + + for (const msg of messages) { + if (msg.role === 'system') { + // Gemini doesn't have a system role, prepend to first user message + continue; + } + + contents.push({ + role: msg.role === 'assistant' ? 'model' : 'user', + parts: [{ text: msg.content }] + }); + } + + // Add system message as first user message if present + const systemMessage = messages.find(msg => msg.role === 'system'); + if (systemMessage && contents.length > 0) { + contents[0].parts[0].text = `${systemMessage.content}\n\n${contents[0].parts[0].text}`; + } + + // Add structured output instructions + const structuredInstructions = ` + +Please provide your response in JSON format with the following structure: +{ + "chat_msg": "Your explanation or answer to the query", + "command_suggestion": "command to execute (if needed, leave empty if none)" +}`; + + if (contents.length > 0) { + contents[contents.length - 1].parts[0].text += structuredInstructions; + } + + const requestBody = { + contents: contents, + generationConfig: { + temperature: 0.7, + maxOutputTokens: 1024, + responseMimeType: "application/json" + } + }; + + const response = await fetch(`${this.baseUrl}/models/${this.model}:generateContent?key=${this.apiKey}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(requestBody) + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Gemini API error: ${error || response.statusText}`); + } + + const data = await response.json(); + const content = data.candidates?.[0]?.content?.parts?.[0]?.text || ''; + + console.log("Raw Gemini response:", content); + + // Try to parse the response as JSON + try { + const jsonResponse = JSON.parse(content); + console.log("Parsed Gemini JSON response:", jsonResponse); + + const chatMsg = jsonResponse.chat_msg || "No explanation provided"; + let commandSuggestion = jsonResponse.command_suggestion || ""; + + // If there's a command suggestion, return it in our special format + if (commandSuggestion && commandSuggestion.trim() !== "") { + console.log(`Gemini command detected: "${commandSuggestion}"`); + return { + message: chatMsg, + shouldExecuteCommand: false, + command: commandSuggestion, + reasoning: chatMsg, + requiresApproval: true + }; + } else { + // No command suggestion + return { + message: chatMsg, + shouldExecuteCommand: false + }; + } + } catch (error) { + console.error("Error parsing Gemini JSON response:", error); + // Fall back to the original response + return { + message: content, + shouldExecuteCommand: false + }; + } + } processResponse(response) { // First check if response is already an object (from our newer structured JSON format)