diff --git a/app/components/chat/ImportFolderButton.tsx b/app/components/chat/ImportFolderButton.tsx index 6cbfcac..c75200e 100644 --- a/app/components/chat/ImportFolderButton.tsx +++ b/app/components/chat/ImportFolderButton.tsx @@ -3,6 +3,7 @@ import type { Message } from 'ai'; import { toast } from 'react-toastify'; import { MAX_FILES, isBinaryFile, shouldIncludeFile } from '~/utils/fileUtils'; import { createChatFromFolder } from '~/utils/folderImport'; +import { logStore } from '~/lib/stores/logs'; // Assuming logStore is imported from this location interface ImportFolderButtonProps { className?: string; @@ -16,9 +17,15 @@ export const ImportFolderButton: React.FC = ({ classNam const allFiles = Array.from(e.target.files || []); if (allFiles.length > MAX_FILES) { + const error = new Error(`Too many files: ${allFiles.length}`); + logStore.logError('File import failed - too many files', error, { + fileCount: allFiles.length, + maxFiles: MAX_FILES, + }); toast.error( `This folder contains ${allFiles.length.toLocaleString()} files. This product is not yet optimized for very large projects. Please select a folder with fewer than ${MAX_FILES.toLocaleString()} files.`, ); + return; } @@ -31,7 +38,10 @@ export const ImportFolderButton: React.FC = ({ classNam const filteredFiles = allFiles.filter((file) => shouldIncludeFile(file.webkitRelativePath)); if (filteredFiles.length === 0) { + const error = new Error('No valid files found'); + logStore.logError('File import failed - no valid files', error, { folderName }); toast.error('No files found in the selected folder'); + return; } @@ -48,11 +58,18 @@ export const ImportFolderButton: React.FC = ({ classNam .map((f) => f.file.webkitRelativePath.split('/').slice(1).join('/')); if (textFiles.length === 0) { + const error = new Error('No text files found'); + logStore.logError('File import failed - no text files', error, { folderName }); toast.error('No text files found in the selected folder'); + return; } if (binaryFilePaths.length > 0) { + logStore.logWarning(`Skipping binary files during import`, { + folderName, + binaryCount: binaryFilePaths.length, + }); toast.info(`Skipping ${binaryFilePaths.length} binary files`); } @@ -62,8 +79,14 @@ export const ImportFolderButton: React.FC = ({ classNam await importChat(folderName, [...messages]); } + logStore.logSystem('Folder imported successfully', { + folderName, + textFileCount: textFiles.length, + binaryFileCount: binaryFilePaths.length, + }); toast.success('Folder imported successfully'); } catch (error) { + logStore.logError('Failed to import folder', error, { folderName }); console.error('Failed to import folder:', error); toast.error('Failed to import folder'); } finally { diff --git a/app/components/settings/SettingsWindow.tsx b/app/components/settings/SettingsWindow.tsx index b7b368d..aae0a86 100644 --- a/app/components/settings/SettingsWindow.tsx +++ b/app/components/settings/SettingsWindow.tsx @@ -10,6 +10,7 @@ import ProvidersTab from './providers/ProvidersTab'; import { useSettings } from '~/lib/hooks/useSettings'; import FeaturesTab from './features/FeaturesTab'; import DebugTab from './debug/DebugTab'; +import EventLogsTab from './event-logs/EventLogsTab'; import ConnectionsTab from './connections/ConnectionsTab'; interface SettingsProps { @@ -17,11 +18,10 @@ interface SettingsProps { onClose: () => void; } -type TabType = 'chat-history' | 'providers' | 'features' | 'debug' | 'connection'; +type TabType = 'chat-history' | 'providers' | 'features' | 'debug' | 'event-logs' | 'connection'; -// Providers that support base URL configuration export const SettingsWindow = ({ open, onClose }: SettingsProps) => { - const { debug } = useSettings(); + const { debug, eventLogs } = useSettings(); const [activeTab, setActiveTab] = useState('chat-history'); const tabs: { id: TabType; label: string; icon: string; component?: ReactElement }[] = [ @@ -39,6 +39,16 @@ export const SettingsWindow = ({ open, onClose }: SettingsProps) => { }, ] : []), + ...(eventLogs + ? [ + { + id: 'event-logs' as TabType, + label: 'Event Logs', + icon: 'i-ph:list-bullets', + component: , + }, + ] + : []), ]; return ( diff --git a/app/components/settings/chat-history/ChatHistoryTab.tsx b/app/components/settings/chat-history/ChatHistoryTab.tsx index e96f0d8..76a0f21 100644 --- a/app/components/settings/chat-history/ChatHistoryTab.tsx +++ b/app/components/settings/chat-history/ChatHistoryTab.tsx @@ -4,6 +4,7 @@ import { toast } from 'react-toastify'; import { db, deleteById, getAll } from '~/lib/persistence'; import { classNames } from '~/utils/classNames'; import styles from '~/components/settings/Settings.module.scss'; +import { logStore } from '~/lib/stores/logs'; // Import logStore for event logging export default function ChatHistoryTab() { const navigate = useNavigate(); @@ -22,7 +23,10 @@ export default function ChatHistoryTab() { const handleDeleteAllChats = async () => { if (!db) { + const error = new Error('Database is not available'); + logStore.logError('Failed to delete chats - DB unavailable', error); toast.error('Database is not available'); + return; } @@ -30,13 +34,12 @@ export default function ChatHistoryTab() { setIsDeleting(true); const allChats = await getAll(db); - - // Delete all chats one by one await Promise.all(allChats.map((chat) => deleteById(db!, chat.id))); - + logStore.logSystem('All chats deleted successfully', { count: allChats.length }); toast.success('All chats deleted successfully'); navigate('/', { replace: true }); } catch (error) { + logStore.logError('Failed to delete chats', error); toast.error('Failed to delete chats'); console.error(error); } finally { @@ -46,7 +49,10 @@ export default function ChatHistoryTab() { const handleExportAllChats = async () => { if (!db) { + const error = new Error('Database is not available'); + logStore.logError('Failed to export chats - DB unavailable', error); toast.error('Database is not available'); + return; } @@ -58,8 +64,10 @@ export default function ChatHistoryTab() { }; downloadAsJson(exportData, `all-chats-${new Date().toISOString()}.json`); + logStore.logSystem('Chats exported successfully', { count: allChats.length }); toast.success('Chats exported successfully'); } catch (error) { + logStore.logError('Failed to export chats', error); toast.error('Failed to export chats'); console.error(error); } diff --git a/app/components/settings/connections/ConnectionsTab.tsx b/app/components/settings/connections/ConnectionsTab.tsx index 32d0fa0..8338395 100644 --- a/app/components/settings/connections/ConnectionsTab.tsx +++ b/app/components/settings/connections/ConnectionsTab.tsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { toast } from 'react-toastify'; import Cookies from 'js-cookie'; +import { logStore } from '~/lib/stores/logs'; export default function ConnectionsTab() { const [githubUsername, setGithubUsername] = useState(Cookies.get('githubUsername') || ''); @@ -9,6 +10,10 @@ export default function ConnectionsTab() { const handleSaveConnection = () => { Cookies.set('githubUsername', githubUsername); Cookies.set('githubToken', githubToken); + logStore.logSystem('GitHub connection settings updated', { + username: githubUsername, + hasToken: !!githubToken, + }); toast.success('GitHub credentials saved successfully!'); }; diff --git a/app/components/settings/event-logs/EventLogsTab.tsx b/app/components/settings/event-logs/EventLogsTab.tsx new file mode 100644 index 0000000..d8766c7 --- /dev/null +++ b/app/components/settings/event-logs/EventLogsTab.tsx @@ -0,0 +1,145 @@ +import React, { useCallback, useEffect, useState, useMemo } from 'react'; +import { useSettings } from '~/lib/hooks/useSettings'; +import { toast } from 'react-toastify'; +import { Switch } from '~/components/ui/Switch'; +import { logStore, type LogEntry } from '~/lib/stores/logs'; + +export default function EventLogsTab() { + const {} = useSettings(); + const [logLevel, setLogLevel] = useState('info'); + const [autoScroll, setAutoScroll] = useState(true); + const [searchQuery, setSearchQuery] = useState(''); + const [, forceUpdate] = useState({}); + + useEffect(() => { + // Add some initial logs for testing + logStore.logSystem('System started', { version: '1.0.0' }); + logStore.logWarning('High memory usage detected', { memoryUsage: '85%' }); + logStore.logError('Failed to connect to provider', new Error('Connection timeout'), { provider: 'OpenAI' }); + }, []); + + const handleClearLogs = useCallback(() => { + if (confirm('Are you sure you want to clear all logs?')) { + logStore.clearLogs(); + toast.success('Logs cleared successfully'); + forceUpdate({}); // Force a re-render after clearing logs + } + }, []); + + const handleExportLogs = useCallback(() => { + try { + const logText = logStore + .getLogs() + .map( + (log) => + `[${log.level.toUpperCase()}] ${log.timestamp} - ${log.message}${ + log.details ? '\nDetails: ' + JSON.stringify(log.details, null, 2) : '' + }`, + ) + .join('\n\n'); + + const blob = new Blob([logText], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `event-logs-${new Date().toISOString()}.txt`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + toast.success('Logs exported successfully'); + } catch (error) { + toast.error('Failed to export logs'); + console.error('Export error:', error); + } + }, []); + + const filteredLogs = useMemo(() => { + return logStore.getFilteredLogs(logLevel, undefined, searchQuery); + }, [logLevel, searchQuery]); + + const getLevelColor = (level: LogEntry['level']) => { + switch (level) { + case 'info': + return 'text-blue-500'; + case 'warning': + return 'text-yellow-500'; + case 'error': + return 'text-red-500'; + case 'debug': + return 'text-gray-500'; + default: + return 'text-bolt-elements-textPrimary'; + } + }; + + return ( +
+
+
+

Event Logs

+
+ Auto-scroll + +
+
+ +
+ + setSearchQuery(e.target.value)} + className="bg-bolt-elements-bg-depth-2 text-bolt-elements-textPrimary rounded-lg px-3 py-1.5 text-sm flex-1" + /> + + +
+
+ +
+ {filteredLogs.length === 0 ? ( +
No logs found
+ ) : ( + filteredLogs.map((log, index) => ( +
+
+ [{log.level.toUpperCase()}] + {new Date(log.timestamp).toLocaleString()} + {log.message} +
+ {log.details && ( +
+                  {JSON.stringify(log.details, null, 2)}
+                
+ )} +
+ )) + )} +
+
+ ); +} diff --git a/app/components/settings/features/FeaturesTab.tsx b/app/components/settings/features/FeaturesTab.tsx index 0b4fa75..9c9e4a0 100644 --- a/app/components/settings/features/FeaturesTab.tsx +++ b/app/components/settings/features/FeaturesTab.tsx @@ -3,7 +3,7 @@ import { Switch } from '~/components/ui/Switch'; import { useSettings } from '~/lib/hooks/useSettings'; export default function FeaturesTab() { - const { debug, enableDebugMode, isLocalModel, enableLocalModels } = useSettings(); + const { debug, enableDebugMode, isLocalModel, enableLocalModels, eventLogs, enableEventLogs } = useSettings(); return (
@@ -12,6 +12,10 @@ export default function FeaturesTab() { Debug Info
+
+ Event Logs + +
diff --git a/app/components/settings/providers/ProvidersTab.tsx b/app/components/settings/providers/ProvidersTab.tsx index 9e9da36..edcf6f9 100644 --- a/app/components/settings/providers/ProvidersTab.tsx +++ b/app/components/settings/providers/ProvidersTab.tsx @@ -3,6 +3,7 @@ import { Switch } from '~/components/ui/Switch'; import { useSettings } from '~/lib/hooks/useSettings'; import { LOCAL_PROVIDERS, URL_CONFIGURABLE_PROVIDERS } from '~/lib/stores/settings'; import type { IProviderConfig } from '~/types/model'; +import { logStore } from '~/lib/stores/logs'; export default function ProvidersTab() { const { providers, updateProviderSettings, isLocalModel } = useSettings(); @@ -60,7 +61,15 @@ export default function ProvidersTab() { updateProviderSettings(provider.name, { ...provider.settings, enabled })} + onCheckedChange={(enabled) => { + updateProviderSettings(provider.name, { ...provider.settings, enabled }); + + if (enabled) { + logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name }); + } else { + logStore.logProvider(`Provider ${provider.name} disabled`, { provider: provider.name }); + } + }} />
{/* Base URL input for configurable providers */} @@ -70,9 +79,14 @@ export default function ProvidersTab() { - updateProviderSettings(provider.name, { ...provider.settings, baseUrl: e.target.value }) - } + onChange={(e) => { + const newBaseUrl = e.target.value; + updateProviderSettings(provider.name, { ...provider.settings, baseUrl: newBaseUrl }); + logStore.logProvider(`Base URL updated for ${provider.name}`, { + provider: provider.name, + baseUrl: newBaseUrl, + }); + }} placeholder={`Enter ${provider.name} base URL`} className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor" /> diff --git a/app/components/sidebar/Menu.client.tsx b/app/components/sidebar/Menu.client.tsx index 38ffb54..ac578d6 100644 --- a/app/components/sidebar/Menu.client.tsx +++ b/app/components/sidebar/Menu.client.tsx @@ -11,6 +11,7 @@ import { logger } from '~/utils/logger'; import { HistoryItem } from './HistoryItem'; import { binDates } from './date-binning'; import { useSearchFilter } from '~/lib/hooks/useSearchFilter'; +import { ClockIcon } from '@heroicons/react/24/outline'; const menuVariants = { closed: { @@ -35,6 +36,25 @@ const menuVariants = { type DialogContent = { type: 'delete'; item: ChatHistoryItem } | null; +function CurrentDateTime() { + const [dateTime, setDateTime] = useState(new Date()); + + useEffect(() => { + const timer = setInterval(() => { + setDateTime(new Date()); + }, 60000); // Update every minute + + return () => clearInterval(timer); + }, []); + + return ( +
+ + {dateTime.toLocaleDateString()} {dateTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} +
+ ); +} + export const Menu = () => { const { duplicateCurrentChat, exportChat } = useChatHistory(); const menuRef = useRef(null); @@ -126,18 +146,17 @@ export const Menu = () => { variants={menuVariants} className="flex selection-accent flex-col side-menu fixed top-0 w-[350px] h-full bg-bolt-elements-background-depth-2 border-r rounded-r-3xl border-bolt-elements-borderColor z-sidebar shadow-xl shadow-bolt-elements-sidebar-dropdownShadow text-sm" > -
{/* Placeholder */}
+
{/* Spacer for top margin */} +
-
([]); @@ -39,6 +47,13 @@ export function useSettings() { isDebugMode.set(savedDebugMode === 'true'); } + // load event logs from cookies + const savedEventLogs = Cookies.get('isEventLogsEnabled'); + + if (savedEventLogs) { + isEventLogsEnabled.set(savedEventLogs === 'true'); + } + // load local models from cookies const savedLocalModels = Cookies.get('isLocalModelsEnabled'); @@ -80,11 +95,19 @@ export function useSettings() { const enableDebugMode = useCallback((enabled: boolean) => { isDebugMode.set(enabled); + logStore.logSystem(`Debug mode ${enabled ? 'enabled' : 'disabled'}`); Cookies.set('isDebugEnabled', String(enabled)); }, []); + const enableEventLogs = useCallback((enabled: boolean) => { + isEventLogsEnabled.set(enabled); + logStore.logSystem(`Event logs ${enabled ? 'enabled' : 'disabled'}`); + Cookies.set('isEventLogsEnabled', String(enabled)); + }, []); + const enableLocalModels = useCallback((enabled: boolean) => { isLocalModelsEnabled.set(enabled); + logStore.logSystem(`Local models ${enabled ? 'enabled' : 'disabled'}`); Cookies.set('isLocalModelsEnabled', String(enabled)); }, []); @@ -94,6 +117,8 @@ export function useSettings() { updateProviderSettings, debug, enableDebugMode, + eventLogs, + enableEventLogs, isLocalModel, enableLocalModels, }; diff --git a/app/lib/persistence/useChatHistory.ts b/app/lib/persistence/useChatHistory.ts index ca44b4b..0a8eeb5 100644 --- a/app/lib/persistence/useChatHistory.ts +++ b/app/lib/persistence/useChatHistory.ts @@ -4,6 +4,7 @@ import { atom } from 'nanostores'; import type { Message } from 'ai'; import { toast } from 'react-toastify'; import { workbenchStore } from '~/lib/stores/workbench'; +import { logStore } from '~/lib/stores/logs'; // Import logStore import { getMessages, getNextId, @@ -43,6 +44,8 @@ export function useChatHistory() { setReady(true); if (persistenceEnabled) { + const error = new Error('Chat persistence is unavailable'); + logStore.logError('Chat persistence initialization failed', error); toast.error('Chat persistence is unavailable'); } @@ -69,6 +72,7 @@ export function useChatHistory() { setReady(true); }) .catch((error) => { + logStore.logError('Failed to load chat messages', error); toast.error(error.message); }); } diff --git a/app/lib/stores/logs.ts b/app/lib/stores/logs.ts new file mode 100644 index 0000000..b2c62dc --- /dev/null +++ b/app/lib/stores/logs.ts @@ -0,0 +1,149 @@ +import { atom, map } from 'nanostores'; +import Cookies from 'js-cookie'; +import { createScopedLogger } from '~/utils/logger'; + +const logger = createScopedLogger('LogStore'); + +export interface LogEntry { + id: string; + timestamp: string; + level: 'info' | 'warning' | 'error' | 'debug'; + message: string; + details?: Record; + category: 'system' | 'provider' | 'user' | 'error'; +} + +const MAX_LOGS = 1000; // Maximum number of logs to keep in memory + +class LogStore { + private _logs = map>({}); + showLogs = atom(false); + + constructor() { + // Load saved logs from cookies on initialization + this._loadLogs(); + } + + private _loadLogs() { + const savedLogs = Cookies.get('eventLogs'); + + if (savedLogs) { + try { + const parsedLogs = JSON.parse(savedLogs); + this._logs.set(parsedLogs); + } catch (error) { + logger.error('Failed to parse logs from cookies:', error); + } + } + } + + private _saveLogs() { + const currentLogs = this._logs.get(); + Cookies.set('eventLogs', JSON.stringify(currentLogs)); + } + + private _generateId(): string { + return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + } + + private _trimLogs() { + const currentLogs = Object.entries(this._logs.get()); + + if (currentLogs.length > MAX_LOGS) { + const sortedLogs = currentLogs.sort( + ([, a], [, b]) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(), + ); + const newLogs = Object.fromEntries(sortedLogs.slice(0, MAX_LOGS)); + this._logs.set(newLogs); + } + } + + addLog( + message: string, + level: LogEntry['level'] = 'info', + category: LogEntry['category'] = 'system', + details?: Record, + ) { + const id = this._generateId(); + const entry: LogEntry = { + id, + timestamp: new Date().toISOString(), + level, + message, + details, + category, + }; + + this._logs.setKey(id, entry); + this._trimLogs(); + this._saveLogs(); + + return id; + } + + // System events + logSystem(message: string, details?: Record) { + return this.addLog(message, 'info', 'system', details); + } + + // Provider events + logProvider(message: string, details?: Record) { + return this.addLog(message, 'info', 'provider', details); + } + + // User actions + logUserAction(message: string, details?: Record) { + return this.addLog(message, 'info', 'user', details); + } + + // Error events + logError(message: string, error?: Error | unknown, details?: Record) { + const errorDetails = { + ...(details || {}), + error: + error instanceof Error + ? { + message: error.message, + stack: error.stack, + } + : error, + }; + return this.addLog(message, 'error', 'error', errorDetails); + } + + // Warning events + logWarning(message: string, details?: Record) { + return this.addLog(message, 'warning', 'system', details); + } + + // Debug events + logDebug(message: string, details?: Record) { + return this.addLog(message, 'debug', 'system', details); + } + + clearLogs() { + this._logs.set({}); + this._saveLogs(); + } + + getLogs() { + return Object.values(this._logs.get()).sort( + (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(), + ); + } + + getFilteredLogs(level?: LogEntry['level'], category?: LogEntry['category'], searchQuery?: string) { + return this.getLogs().filter((log) => { + const matchesLevel = !level || level === 'debug' || log.level === level; + const matchesCategory = !category || log.category === category; + const matchesSearch = + !searchQuery || + log.message.toLowerCase().includes(searchQuery.toLowerCase()) || + JSON.stringify(log.details).toLowerCase().includes(searchQuery.toLowerCase()); + + return matchesLevel && matchesCategory && matchesSearch; + }); + } +} + +export const logStore = new LogStore(); diff --git a/app/lib/stores/settings.ts b/app/lib/stores/settings.ts index 31564e6..667f049 100644 --- a/app/lib/stores/settings.ts +++ b/app/lib/stores/settings.ts @@ -43,4 +43,6 @@ export const providersStore = map(initialProviderSettings); export const isDebugMode = atom(false); +export const isEventLogsEnabled = atom(false); + export const isLocalModelsEnabled = atom(true); diff --git a/app/lib/stores/theme.ts b/app/lib/stores/theme.ts index 4f3e47b..e70cb15 100644 --- a/app/lib/stores/theme.ts +++ b/app/lib/stores/theme.ts @@ -1,4 +1,5 @@ import { atom } from 'nanostores'; +import { logStore } from './logs'; export type Theme = 'dark' | 'light'; @@ -26,10 +27,8 @@ function initStore() { export function toggleTheme() { const currentTheme = themeStore.get(); const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; - themeStore.set(newTheme); - + logStore.logSystem(`Theme changed to ${newTheme} mode`); localStorage.setItem(kTheme, newTheme); - document.querySelector('html')?.setAttribute('data-theme', newTheme); } diff --git a/app/root.tsx b/app/root.tsx index 31eb387..23978f6 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -78,6 +78,23 @@ export function Layout({ children }: { children: React.ReactNode }) { ); } +import { logStore } from './lib/stores/logs'; + export default function App() { - return ; + const theme = useStore(themeStore); + + useEffect(() => { + logStore.logSystem('Application initialized', { + theme, + platform: navigator.platform, + userAgent: navigator.userAgent, + timestamp: new Date().toISOString(), + }); + }, []); + + return ( + + + + ); } diff --git a/app/utils/constants.ts b/app/utils/constants.ts index 240b4b9..61e709b 100644 --- a/app/utils/constants.ts +++ b/app/utils/constants.ts @@ -2,6 +2,7 @@ import Cookies from 'js-cookie'; import type { ModelInfo, OllamaApiResponse, OllamaModel } from './types'; import type { ProviderInfo, IProviderSetting } from '~/types/model'; import { createScopedLogger } from './logger'; +import { logStore } from '~/lib/stores/logs'; export const WORK_DIR_NAME = 'project'; export const WORK_DIR = `/home/${WORK_DIR_NAME}`; @@ -373,12 +374,6 @@ const getOllamaBaseUrl = (settings?: IProviderSetting) => { }; async function getOllamaModels(apiKeys?: Record, settings?: IProviderSetting): Promise { - /* - * if (typeof window === 'undefined') { - * return []; - * } - */ - try { const baseUrl = getOllamaBaseUrl(settings); const response = await fetch(`${baseUrl}/api/tags`); @@ -391,7 +386,9 @@ async function getOllamaModels(apiKeys?: Record, settings?: IPro maxTokenAllowed: 8000, })); } catch (e: any) { + logStore.logError('Failed to get Ollama models', e, { baseUrl: settings?.baseUrl }); logger.warn('Failed to get Ollama models: ', e.message || ''); + return []; } } @@ -480,7 +477,9 @@ async function getLMStudioModels(_apiKeys?: Record, settings?: I provider: 'LMStudio', })); } catch (e: any) { + logStore.logError('Failed to get LMStudio models', e, { baseUrl: settings?.baseUrl }); logger.warn('Failed to get LMStudio models: ', e.message || ''); + return []; } } @@ -499,6 +498,7 @@ async function initializeModelList(providerSettings?: Record