keychain permission + modelStateService rely only on db
This commit is contained in:
		
							parent
							
								
									a27ab05fa8
								
							
						
					
					
						commit
						dad74875a0
					
				@ -109,15 +109,15 @@ module.exports = {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // ModelStateService
 | 
					    // ModelStateService
 | 
				
			||||||
    ipcMain.handle('model:validate-key', async (e, { provider, key }) => await modelStateService.handleValidateKey(provider, key));
 | 
					    ipcMain.handle('model:validate-key', async (e, { provider, key }) => await modelStateService.handleValidateKey(provider, key));
 | 
				
			||||||
    ipcMain.handle('model:get-all-keys', () => modelStateService.getAllApiKeys());
 | 
					    ipcMain.handle('model:get-all-keys', async () => await modelStateService.getAllApiKeys());
 | 
				
			||||||
    ipcMain.handle('model:set-api-key', async (e, { provider, key }) => await modelStateService.setApiKey(provider, key));
 | 
					    ipcMain.handle('model:set-api-key', async (e, { provider, key }) => await modelStateService.setApiKey(provider, key));
 | 
				
			||||||
    ipcMain.handle('model:remove-api-key', async (e, provider) => await modelStateService.handleRemoveApiKey(provider));
 | 
					    ipcMain.handle('model:remove-api-key', async (e, provider) => await modelStateService.handleRemoveApiKey(provider));
 | 
				
			||||||
    ipcMain.handle('model:get-selected-models', () => modelStateService.getSelectedModels());
 | 
					    ipcMain.handle('model:get-selected-models', async () => await modelStateService.getSelectedModels());
 | 
				
			||||||
    ipcMain.handle('model:set-selected-model', async (e, { type, modelId }) => await modelStateService.handleSetSelectedModel(type, modelId));
 | 
					    ipcMain.handle('model:set-selected-model', async (e, { type, modelId }) => await modelStateService.handleSetSelectedModel(type, modelId));
 | 
				
			||||||
    ipcMain.handle('model:get-available-models', (e, { type }) => modelStateService.getAvailableModels(type));
 | 
					    ipcMain.handle('model:get-available-models', async (e, { type }) => await modelStateService.getAvailableModels(type));
 | 
				
			||||||
    ipcMain.handle('model:are-providers-configured', () => modelStateService.areProvidersConfigured());
 | 
					    ipcMain.handle('model:are-providers-configured', async () => await modelStateService.areProvidersConfigured());
 | 
				
			||||||
    ipcMain.handle('model:get-provider-config', () => modelStateService.getProviderConfig());
 | 
					    ipcMain.handle('model:get-provider-config', () => modelStateService.getProviderConfig());
 | 
				
			||||||
    ipcMain.handle('model:re-initialize-state', () => modelStateService.initialize());
 | 
					    ipcMain.handle('model:re-initialize-state', async () => await modelStateService.initialize());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // LocalAIManager 이벤트를 모든 윈도우에 브로드캐스트
 | 
					    // LocalAIManager 이벤트를 모든 윈도우에 브로드캐스트
 | 
				
			||||||
    localAIManager.on('install-progress', (service, data) => {
 | 
					    localAIManager.on('install-progress', (service, data) => {
 | 
				
			||||||
 | 
				
			|||||||
@ -243,7 +243,7 @@ class AskService {
 | 
				
			|||||||
            await askRepository.addAiMessage({ sessionId, role: 'user', content: userPrompt.trim() });
 | 
					            await askRepository.addAiMessage({ sessionId, role: 'user', content: userPrompt.trim() });
 | 
				
			||||||
            console.log(`[AskService] DB: Saved user prompt to session ${sessionId}`);
 | 
					            console.log(`[AskService] DB: Saved user prompt to session ${sessionId}`);
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            const modelInfo = modelStateService.getCurrentModelInfo('llm');
 | 
					            const modelInfo = await modelStateService.getCurrentModelInfo('llm');
 | 
				
			||||||
            if (!modelInfo || !modelInfo.apiKey) {
 | 
					            if (!modelInfo || !modelInfo.apiKey) {
 | 
				
			||||||
                throw new Error('AI model or API key not configured.');
 | 
					                throw new Error('AI model or API key not configured.');
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
				
			|||||||
@ -91,7 +91,6 @@ const LATEST_SCHEMA = {
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    provider_settings: {
 | 
					    provider_settings: {
 | 
				
			||||||
        columns: [
 | 
					        columns: [
 | 
				
			||||||
            { name: 'uid', type: 'TEXT NOT NULL' },
 | 
					 | 
				
			||||||
            { name: 'provider', type: 'TEXT NOT NULL' },
 | 
					            { name: 'provider', type: 'TEXT NOT NULL' },
 | 
				
			||||||
            { name: 'api_key', type: 'TEXT' },
 | 
					            { name: 'api_key', type: 'TEXT' },
 | 
				
			||||||
            { name: 'selected_llm_model', type: 'TEXT' },
 | 
					            { name: 'selected_llm_model', type: 'TEXT' },
 | 
				
			||||||
@ -101,7 +100,7 @@ const LATEST_SCHEMA = {
 | 
				
			|||||||
            { name: 'created_at', type: 'INTEGER' },
 | 
					            { name: 'created_at', type: 'INTEGER' },
 | 
				
			||||||
            { name: 'updated_at', type: 'INTEGER' }
 | 
					            { name: 'updated_at', type: 'INTEGER' }
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
        constraints: ['PRIMARY KEY (uid, provider)']
 | 
					        constraints: ['PRIMARY KEY (provider)']
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    shortcuts: {
 | 
					    shortcuts: {
 | 
				
			||||||
        columns: [
 | 
					        columns: [
 | 
				
			||||||
 | 
				
			|||||||
@ -33,7 +33,13 @@ function createEncryptedConverter(fieldsToEncrypt = []) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            for (const field of fieldsToEncrypt) {
 | 
					            for (const field of fieldsToEncrypt) {
 | 
				
			||||||
                 if (Object.prototype.hasOwnProperty.call(appObject, field) && appObject[field] != null) {
 | 
					                 if (Object.prototype.hasOwnProperty.call(appObject, field) && appObject[field] != null) {
 | 
				
			||||||
                    appObject[field] = encryptionService.decrypt(appObject[field]);
 | 
					                    try {
 | 
				
			||||||
 | 
					                        appObject[field] = encryptionService.decrypt(appObject[field]);
 | 
				
			||||||
 | 
					                    } catch (error) {
 | 
				
			||||||
 | 
					                        console.warn(`[FirestoreConverter] Failed to decrypt field '${field}' (possibly plaintext or key mismatch):`, error.message);
 | 
				
			||||||
 | 
					                        // Keep the original value instead of failing
 | 
				
			||||||
 | 
					                        // appObject[field] remains as is
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,12 +1,7 @@
 | 
				
			|||||||
const sqliteRepository = require('./sqlite.repository');
 | 
					const sqliteRepository = require('./sqlite.repository');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let authService = null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function setAuthService(service) {
 | 
					 | 
				
			||||||
    authService = service;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function getBaseRepository() {
 | 
					function getBaseRepository() {
 | 
				
			||||||
 | 
					    // For now, we only have sqlite. This could be expanded later.
 | 
				
			||||||
    return sqliteRepository;
 | 
					    return sqliteRepository;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -14,71 +9,60 @@ const providerSettingsRepositoryAdapter = {
 | 
				
			|||||||
    // Core CRUD operations
 | 
					    // Core CRUD operations
 | 
				
			||||||
    async getByProvider(provider) {
 | 
					    async getByProvider(provider) {
 | 
				
			||||||
        const repo = getBaseRepository();
 | 
					        const repo = getBaseRepository();
 | 
				
			||||||
        const uid = authService.getCurrentUserId();
 | 
					        return await repo.getByProvider(provider);
 | 
				
			||||||
        return await repo.getByProvider(uid, provider);
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async getAllByUid() {
 | 
					    async getAll() {
 | 
				
			||||||
        const repo = getBaseRepository();
 | 
					        const repo = getBaseRepository();
 | 
				
			||||||
        const uid = authService.getCurrentUserId();
 | 
					        return await repo.getAll();
 | 
				
			||||||
        return await repo.getAllByUid(uid);
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async upsert(provider, settings) {
 | 
					    async upsert(provider, settings) {
 | 
				
			||||||
        const repo = getBaseRepository();
 | 
					        const repo = getBaseRepository();
 | 
				
			||||||
        const uid = authService.getCurrentUserId();
 | 
					 | 
				
			||||||
        const now = Date.now();
 | 
					        const now = Date.now();
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        const settingsWithMeta = {
 | 
					        const settingsWithMeta = {
 | 
				
			||||||
            ...settings,
 | 
					            ...settings,
 | 
				
			||||||
            uid,
 | 
					 | 
				
			||||||
            provider,
 | 
					            provider,
 | 
				
			||||||
            updated_at: now,
 | 
					            updated_at: now,
 | 
				
			||||||
            created_at: settings.created_at || now
 | 
					            created_at: settings.created_at || now
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        return await repo.upsert(uid, provider, settingsWithMeta);
 | 
					        return await repo.upsert(provider, settingsWithMeta);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async remove(provider) {
 | 
					    async remove(provider) {
 | 
				
			||||||
        const repo = getBaseRepository();
 | 
					        const repo = getBaseRepository();
 | 
				
			||||||
        const uid = authService.getCurrentUserId();
 | 
					        return await repo.remove(provider);
 | 
				
			||||||
        return await repo.remove(uid, provider);
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async removeAllByUid() {
 | 
					    async removeAll() {
 | 
				
			||||||
        const repo = getBaseRepository();
 | 
					        const repo = getBaseRepository();
 | 
				
			||||||
        const uid = authService.getCurrentUserId();
 | 
					        return await repo.removeAll();
 | 
				
			||||||
        return await repo.removeAllByUid(uid);
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async getRawApiKeysByUid() {
 | 
					    async getRawApiKeys() {
 | 
				
			||||||
        // This function should always target the local sqlite DB,
 | 
					        // This function should always target the local sqlite DB,
 | 
				
			||||||
        // as it's part of the local-first boot sequence.
 | 
					        // as it's part of the local-first boot sequence.
 | 
				
			||||||
        const uid = authService.getCurrentUserId();
 | 
					        return await sqliteRepository.getRawApiKeys();
 | 
				
			||||||
        return await sqliteRepository.getRawApiKeysByUid(uid);
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    async getActiveProvider(type) {
 | 
					    async getActiveProvider(type) {
 | 
				
			||||||
        const repo = getBaseRepository();
 | 
					        const repo = getBaseRepository();
 | 
				
			||||||
        const uid = authService.getCurrentUserId();
 | 
					        return await repo.getActiveProvider(type);
 | 
				
			||||||
        return await repo.getActiveProvider(uid, type);
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    async setActiveProvider(provider, type) {
 | 
					    async setActiveProvider(provider, type) {
 | 
				
			||||||
        const repo = getBaseRepository();
 | 
					        const repo = getBaseRepository();
 | 
				
			||||||
        const uid = authService.getCurrentUserId();
 | 
					        return await repo.setActiveProvider(provider, type);
 | 
				
			||||||
        return await repo.setActiveProvider(uid, provider, type);
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    async getActiveSettings() {
 | 
					    async getActiveSettings() {
 | 
				
			||||||
        const repo = getBaseRepository();
 | 
					        const repo = getBaseRepository();
 | 
				
			||||||
        const uid = authService.getCurrentUserId();
 | 
					        return await repo.getActiveSettings();
 | 
				
			||||||
        return await repo.getActiveSettings(uid);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
    ...providerSettingsRepositoryAdapter,
 | 
					    ...providerSettingsRepositoryAdapter
 | 
				
			||||||
    setAuthService
 | 
					 | 
				
			||||||
}; 
 | 
					}; 
 | 
				
			||||||
@ -1,31 +1,32 @@
 | 
				
			|||||||
const sqliteClient = require('../../services/sqliteClient');
 | 
					const sqliteClient = require('../../services/sqliteClient');
 | 
				
			||||||
 | 
					const encryptionService = require('../../services/encryptionService');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getByProvider(uid, provider) {
 | 
					function getByProvider(provider) {
 | 
				
			||||||
    const db = sqliteClient.getDb();
 | 
					    const db = sqliteClient.getDb();
 | 
				
			||||||
    const stmt = db.prepare('SELECT * FROM provider_settings WHERE uid = ? AND provider = ?');
 | 
					    const stmt = db.prepare('SELECT * FROM provider_settings WHERE provider = ?');
 | 
				
			||||||
    const result = stmt.get(uid, provider) || null;
 | 
					    const result = stmt.get(provider) || null;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    if (result && result.api_key) {
 | 
					    if (result && result.api_key && encryptionService.looksEncrypted(result.api_key)) {
 | 
				
			||||||
        result.api_key = encryptionService.decrypt(result.api_key);
 | 
					        result.api_key = encryptionService.decrypt(result.api_key);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    return result;
 | 
					    return result;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getAllByUid(uid) {
 | 
					function getAll() {
 | 
				
			||||||
    const db = sqliteClient.getDb();
 | 
					    const db = sqliteClient.getDb();
 | 
				
			||||||
    const stmt = db.prepare('SELECT * FROM provider_settings WHERE uid = ? ORDER BY provider');
 | 
					    const stmt = db.prepare('SELECT * FROM provider_settings ORDER BY provider');
 | 
				
			||||||
    const results = stmt.all(uid);
 | 
					    const results = stmt.all();
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    return results.map(result => {
 | 
					    return results.map(result => {
 | 
				
			||||||
        if (result.api_key) {
 | 
					        if (result.api_key && encryptionService.looksEncrypted(result.api_key)) {
 | 
				
			||||||
            result.api_key = result.api_key;
 | 
					            result.api_key = encryptionService.decrypt(result.api_key);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return result;
 | 
					        return result;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function upsert(uid, provider, settings) {
 | 
					function upsert(provider, settings) {
 | 
				
			||||||
    // Validate: prevent direct setting of active status
 | 
					    // Validate: prevent direct setting of active status
 | 
				
			||||||
    if (settings.is_active_llm || settings.is_active_stt) {
 | 
					    if (settings.is_active_llm || settings.is_active_stt) {
 | 
				
			||||||
        console.warn('[ProviderSettings] Warning: is_active_llm/is_active_stt should not be set directly. Use setActiveProvider() instead.');
 | 
					        console.warn('[ProviderSettings] Warning: is_active_llm/is_active_stt should not be set directly. Use setActiveProvider() instead.');
 | 
				
			||||||
@ -35,9 +36,9 @@ function upsert(uid, provider, settings) {
 | 
				
			|||||||
    
 | 
					    
 | 
				
			||||||
    // Use SQLite's UPSERT syntax (INSERT ... ON CONFLICT ... DO UPDATE)
 | 
					    // Use SQLite's UPSERT syntax (INSERT ... ON CONFLICT ... DO UPDATE)
 | 
				
			||||||
    const stmt = db.prepare(`
 | 
					    const stmt = db.prepare(`
 | 
				
			||||||
        INSERT INTO provider_settings (uid, provider, api_key, selected_llm_model, selected_stt_model, is_active_llm, is_active_stt, created_at, updated_at)
 | 
					        INSERT INTO provider_settings (provider, api_key, selected_llm_model, selected_stt_model, is_active_llm, is_active_stt, created_at, updated_at)
 | 
				
			||||||
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
 | 
					        VALUES (?, ?, ?, ?, ?, ?, ?, ?)
 | 
				
			||||||
        ON CONFLICT(uid, provider) DO UPDATE SET
 | 
					        ON CONFLICT(provider) DO UPDATE SET
 | 
				
			||||||
            api_key = excluded.api_key,
 | 
					            api_key = excluded.api_key,
 | 
				
			||||||
            selected_llm_model = excluded.selected_llm_model,
 | 
					            selected_llm_model = excluded.selected_llm_model,
 | 
				
			||||||
            selected_stt_model = excluded.selected_stt_model,
 | 
					            selected_stt_model = excluded.selected_stt_model,
 | 
				
			||||||
@ -47,7 +48,6 @@ function upsert(uid, provider, settings) {
 | 
				
			|||||||
    `);
 | 
					    `);
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    const result = stmt.run(
 | 
					    const result = stmt.run(
 | 
				
			||||||
        uid,
 | 
					 | 
				
			||||||
        provider,
 | 
					        provider,
 | 
				
			||||||
        settings.api_key || null,
 | 
					        settings.api_key || null,
 | 
				
			||||||
        settings.selected_llm_model || null,
 | 
					        settings.selected_llm_model || null,
 | 
				
			||||||
@ -61,55 +61,55 @@ function upsert(uid, provider, settings) {
 | 
				
			|||||||
    return { changes: result.changes };
 | 
					    return { changes: result.changes };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function remove(uid, provider) {
 | 
					function remove(provider) {
 | 
				
			||||||
    const db = sqliteClient.getDb();
 | 
					    const db = sqliteClient.getDb();
 | 
				
			||||||
    const stmt = db.prepare('DELETE FROM provider_settings WHERE uid = ? AND provider = ?');
 | 
					    const stmt = db.prepare('DELETE FROM provider_settings WHERE provider = ?');
 | 
				
			||||||
    const result = stmt.run(uid, provider);
 | 
					    const result = stmt.run(provider);
 | 
				
			||||||
    return { changes: result.changes };
 | 
					    return { changes: result.changes };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function removeAllByUid(uid) {
 | 
					function removeAll() {
 | 
				
			||||||
    const db = sqliteClient.getDb();
 | 
					    const db = sqliteClient.getDb();
 | 
				
			||||||
    const stmt = db.prepare('DELETE FROM provider_settings WHERE uid = ?');
 | 
					    const stmt = db.prepare('DELETE FROM provider_settings');
 | 
				
			||||||
    const result = stmt.run(uid);
 | 
					    const result = stmt.run();
 | 
				
			||||||
    return { changes: result.changes };
 | 
					    return { changes: result.changes };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getRawApiKeysByUid(uid) {
 | 
					function getRawApiKeys() {
 | 
				
			||||||
    const db = sqliteClient.getDb();
 | 
					    const db = sqliteClient.getDb();
 | 
				
			||||||
    const stmt = db.prepare('SELECT api_key FROM provider_settings WHERE uid = ?');
 | 
					    const stmt = db.prepare('SELECT api_key FROM provider_settings');
 | 
				
			||||||
    return stmt.all(uid);
 | 
					    return stmt.all();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Get active provider for a specific type (llm or stt)
 | 
					// Get active provider for a specific type (llm or stt)
 | 
				
			||||||
function getActiveProvider(uid, type) {
 | 
					function getActiveProvider(type) {
 | 
				
			||||||
    const db = sqliteClient.getDb();
 | 
					    const db = sqliteClient.getDb();
 | 
				
			||||||
    const column = type === 'llm' ? 'is_active_llm' : 'is_active_stt';
 | 
					    const column = type === 'llm' ? 'is_active_llm' : 'is_active_stt';
 | 
				
			||||||
    const stmt = db.prepare(`SELECT * FROM provider_settings WHERE uid = ? AND ${column} = 1`);
 | 
					    const stmt = db.prepare(`SELECT * FROM provider_settings WHERE ${column} = 1`);
 | 
				
			||||||
    const result = stmt.get(uid) || null;
 | 
					    const result = stmt.get() || null;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    if (result && result.api_key) {
 | 
					    if (result && result.api_key && encryptionService.looksEncrypted(result.api_key)) {
 | 
				
			||||||
        result.api_key = result.api_key;
 | 
					        result.api_key = encryptionService.decrypt(result.api_key);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    return result;
 | 
					    return result;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Set active provider for a specific type
 | 
					// Set active provider for a specific type
 | 
				
			||||||
function setActiveProvider(uid, provider, type) {
 | 
					function setActiveProvider(provider, type) {
 | 
				
			||||||
    const db = sqliteClient.getDb();
 | 
					    const db = sqliteClient.getDb();
 | 
				
			||||||
    const column = type === 'llm' ? 'is_active_llm' : 'is_active_stt';
 | 
					    const column = type === 'llm' ? 'is_active_llm' : 'is_active_stt';
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // Start transaction to ensure only one provider is active
 | 
					    // Start transaction to ensure only one provider is active
 | 
				
			||||||
    db.transaction(() => {
 | 
					    db.transaction(() => {
 | 
				
			||||||
        // First, deactivate all providers for this type
 | 
					        // First, deactivate all providers for this type
 | 
				
			||||||
        const deactivateStmt = db.prepare(`UPDATE provider_settings SET ${column} = 0 WHERE uid = ?`);
 | 
					        const deactivateStmt = db.prepare(`UPDATE provider_settings SET ${column} = 0`);
 | 
				
			||||||
        deactivateStmt.run(uid);
 | 
					        deactivateStmt.run();
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        // Then activate the specified provider
 | 
					        // Then activate the specified provider
 | 
				
			||||||
        if (provider) {
 | 
					        if (provider) {
 | 
				
			||||||
            const activateStmt = db.prepare(`UPDATE provider_settings SET ${column} = 1 WHERE uid = ? AND provider = ?`);
 | 
					            const activateStmt = db.prepare(`UPDATE provider_settings SET ${column} = 1 WHERE provider = ?`);
 | 
				
			||||||
            activateStmt.run(uid, provider);
 | 
					            activateStmt.run(provider);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    })();
 | 
					    })();
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
@ -117,14 +117,14 @@ function setActiveProvider(uid, provider, type) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Get all active settings (both llm and stt)
 | 
					// Get all active settings (both llm and stt)
 | 
				
			||||||
function getActiveSettings(uid) {
 | 
					function getActiveSettings() {
 | 
				
			||||||
    const db = sqliteClient.getDb();
 | 
					    const db = sqliteClient.getDb();
 | 
				
			||||||
    const stmt = db.prepare(`
 | 
					    const stmt = db.prepare(`
 | 
				
			||||||
        SELECT * FROM provider_settings 
 | 
					        SELECT * FROM provider_settings 
 | 
				
			||||||
        WHERE uid = ? AND (is_active_llm = 1 OR is_active_stt = 1)
 | 
					        WHERE (is_active_llm = 1 OR is_active_stt = 1)
 | 
				
			||||||
        ORDER BY provider
 | 
					        ORDER BY provider
 | 
				
			||||||
    `);
 | 
					    `);
 | 
				
			||||||
    const results = stmt.all(uid);
 | 
					    const results = stmt.all();
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // Decrypt API keys and organize by type
 | 
					    // Decrypt API keys and organize by type
 | 
				
			||||||
    const activeSettings = {
 | 
					    const activeSettings = {
 | 
				
			||||||
@ -133,8 +133,8 @@ function getActiveSettings(uid) {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    results.forEach(result => {
 | 
					    results.forEach(result => {
 | 
				
			||||||
        if (result.api_key) {
 | 
					        if (result.api_key && encryptionService.looksEncrypted(result.api_key)) {
 | 
				
			||||||
            result.api_key = result.api_key;
 | 
					            result.api_key = encryptionService.decrypt(result.api_key);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (result.is_active_llm) {
 | 
					        if (result.is_active_llm) {
 | 
				
			||||||
            activeSettings.llm = result;
 | 
					            activeSettings.llm = result;
 | 
				
			||||||
@ -149,11 +149,11 @@ function getActiveSettings(uid) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
    getByProvider,
 | 
					    getByProvider,
 | 
				
			||||||
    getAllByUid,
 | 
					    getAll,
 | 
				
			||||||
    upsert,
 | 
					    upsert,
 | 
				
			||||||
    remove,
 | 
					    remove,
 | 
				
			||||||
    removeAllByUid,
 | 
					    removeAll,
 | 
				
			||||||
    getRawApiKeysByUid,
 | 
					    getRawApiKeys,
 | 
				
			||||||
    getActiveProvider,
 | 
					    getActiveProvider,
 | 
				
			||||||
    setActiveProvider,
 | 
					    setActiveProvider,
 | 
				
			||||||
    getActiveSettings
 | 
					    getActiveSettings
 | 
				
			||||||
 | 
				
			|||||||
@ -46,7 +46,6 @@ class AuthService {
 | 
				
			|||||||
        this.initializationPromise = null;
 | 
					        this.initializationPromise = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        sessionRepository.setAuthService(this);
 | 
					        sessionRepository.setAuthService(this);
 | 
				
			||||||
        providerSettingsRepository.setAuthService(this);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    initialize() {
 | 
					    initialize() {
 | 
				
			||||||
@ -78,22 +77,21 @@ class AuthService {
 | 
				
			|||||||
                    // No 'await' here, so it runs in the background without blocking startup.
 | 
					                    // No 'await' here, so it runs in the background without blocking startup.
 | 
				
			||||||
                    migrationService.checkAndRunMigration(user);
 | 
					                    migrationService.checkAndRunMigration(user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // ***** CRITICAL: Wait for the virtual key and model state update to complete *****
 | 
				
			||||||
 | 
					                    try {
 | 
				
			||||||
 | 
					                        const idToken = await user.getIdToken(true);
 | 
				
			||||||
 | 
					                        const virtualKey = await getVirtualKeyByEmail(user.email, idToken);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    // Start background task to fetch and save virtual key
 | 
					                        if (global.modelStateService) {
 | 
				
			||||||
                    (async () => {
 | 
					                            // The model state service now writes directly to the DB, no in-memory state.
 | 
				
			||||||
                        try {
 | 
					                            await global.modelStateService.setFirebaseVirtualKey(virtualKey);
 | 
				
			||||||
                            const idToken = await user.getIdToken(true);
 | 
					 | 
				
			||||||
                            const virtualKey = await getVirtualKeyByEmail(user.email, idToken);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            if (global.modelStateService) {
 | 
					 | 
				
			||||||
                                global.modelStateService.setFirebaseVirtualKey(virtualKey);
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                            console.log(`[AuthService] BG: Virtual key for ${user.email} has been processed.`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        } catch (error) {
 | 
					 | 
				
			||||||
                            console.error('[AuthService] BG: Failed to fetch or save virtual key:', error);
 | 
					 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    })();
 | 
					                        console.log(`[AuthService] Virtual key for ${user.email} has been processed and state updated.`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    } catch (error) {
 | 
				
			||||||
 | 
					                        console.error('[AuthService] Failed to fetch or save virtual key:', error);
 | 
				
			||||||
 | 
					                        // This is not critical enough to halt the login, but we should log it.
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    // User signed OUT
 | 
					                    // User signed OUT
 | 
				
			||||||
@ -101,7 +99,8 @@ class AuthService {
 | 
				
			|||||||
                    if (previousUser) {
 | 
					                    if (previousUser) {
 | 
				
			||||||
                        console.log(`[AuthService] Clearing API key for logged-out user: ${previousUser.uid}`);
 | 
					                        console.log(`[AuthService] Clearing API key for logged-out user: ${previousUser.uid}`);
 | 
				
			||||||
                        if (global.modelStateService) {
 | 
					                        if (global.modelStateService) {
 | 
				
			||||||
                            global.modelStateService.setFirebaseVirtualKey(null);
 | 
					                            // The model state service now writes directly to the DB.
 | 
				
			||||||
 | 
					                            await global.modelStateService.setFirebaseVirtualKey(null);
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    this.currentUser = null;
 | 
					                    this.currentUser = null;
 | 
				
			||||||
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1066,7 +1066,7 @@ class OllamaService extends EventEmitter {
 | 
				
			|||||||
                return false;
 | 
					                return false;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const selectedModels = modelStateService.getSelectedModels();
 | 
					            const selectedModels = await modelStateService.getSelectedModels();
 | 
				
			||||||
            const llmModelId = selectedModels.llm;
 | 
					            const llmModelId = selectedModels.llm;
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            // Check if it's an Ollama model
 | 
					            // Check if it's an Ollama model
 | 
				
			||||||
 | 
				
			|||||||
@ -106,6 +106,9 @@ class PermissionService {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async checkKeychainCompleted(uid) {
 | 
					  async checkKeychainCompleted(uid) {
 | 
				
			||||||
 | 
					    if (uid === "default_user") {
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const completed = permissionRepository.checkKeychainCompleted(uid);
 | 
					      const completed = permissionRepository.checkKeychainCompleted(uid);
 | 
				
			||||||
      console.log('[Permissions] Keychain completed status:', completed);
 | 
					      console.log('[Permissions] Keychain completed status:', completed);
 | 
				
			||||||
 | 
				
			|||||||
@ -40,8 +40,82 @@ class SQLiteClient {
 | 
				
			|||||||
        return `"${identifier}"`;
 | 
					        return `"${identifier}"`;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _migrateProviderSettings() {
 | 
				
			||||||
 | 
					        const tablesInDb = this.getTablesFromDb();
 | 
				
			||||||
 | 
					        if (!tablesInDb.includes('provider_settings')) {
 | 
				
			||||||
 | 
					            return; // Table doesn't exist, no migration needed.
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					        const providerSettingsInfo = this.db.prepare(`PRAGMA table_info(provider_settings)`).all();
 | 
				
			||||||
 | 
					        const hasUidColumn = providerSettingsInfo.some(col => col.name === 'uid');
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					        if (hasUidColumn) {
 | 
				
			||||||
 | 
					            console.log('[DB Migration] Old provider_settings schema detected. Starting robust migration...');
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                this.db.transaction(() => {
 | 
				
			||||||
 | 
					                    this.db.exec('ALTER TABLE provider_settings RENAME TO provider_settings_old');
 | 
				
			||||||
 | 
					                    console.log('[DB Migration] Renamed provider_settings to provider_settings_old');
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					                    this.createTable('provider_settings', LATEST_SCHEMA.provider_settings);
 | 
				
			||||||
 | 
					                    console.log('[DB Migration] Created new provider_settings table');
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					                    // Dynamically build the migration query for robustness
 | 
				
			||||||
 | 
					                    const oldColumnNames = this.db.prepare(`PRAGMA table_info(provider_settings_old)`).all().map(c => c.name);
 | 
				
			||||||
 | 
					                    const newColumnNames = LATEST_SCHEMA.provider_settings.columns.map(c => c.name);
 | 
				
			||||||
 | 
					                    const commonColumns = newColumnNames.filter(name => oldColumnNames.includes(name));
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					                    if (!commonColumns.includes('provider')) {
 | 
				
			||||||
 | 
					                        console.warn('[DB Migration] Old table is missing the "provider" column. Aborting migration for this table.');
 | 
				
			||||||
 | 
					                        this.db.exec('DROP TABLE provider_settings_old');
 | 
				
			||||||
 | 
					                        return;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					                    const orderParts = [];
 | 
				
			||||||
 | 
					                    if (oldColumnNames.includes('updated_at')) orderParts.push('updated_at DESC');
 | 
				
			||||||
 | 
					                    if (oldColumnNames.includes('created_at')) orderParts.push('created_at DESC');
 | 
				
			||||||
 | 
					                    const orderByClause = orderParts.length > 0 ? `ORDER BY ${orderParts.join(', ')}` : '';
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					                    const columnsForInsert = commonColumns.map(c => this._validateAndQuoteIdentifier(c)).join(', ');
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					                    const migrationQuery = `
 | 
				
			||||||
 | 
					                        INSERT INTO provider_settings (${columnsForInsert})
 | 
				
			||||||
 | 
					                        SELECT ${columnsForInsert}
 | 
				
			||||||
 | 
					                        FROM (
 | 
				
			||||||
 | 
					                            SELECT *, ROW_NUMBER() OVER(PARTITION BY provider ${orderByClause}) as rn
 | 
				
			||||||
 | 
					                            FROM provider_settings_old
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                        WHERE rn = 1
 | 
				
			||||||
 | 
					                    `;
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    console.log(`[DB Migration] Executing robust migration query for columns: ${commonColumns.join(', ')}`);
 | 
				
			||||||
 | 
					                    const result = this.db.prepare(migrationQuery).run();
 | 
				
			||||||
 | 
					                    console.log(`[DB Migration] Migrated ${result.changes} rows to the new provider_settings table.`);
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					                    this.db.exec('DROP TABLE provider_settings_old');
 | 
				
			||||||
 | 
					                    console.log('[DB Migration] Dropped provider_settings_old table.');
 | 
				
			||||||
 | 
					                })();
 | 
				
			||||||
 | 
					                console.log('[DB Migration] provider_settings migration completed successfully.');
 | 
				
			||||||
 | 
					            } catch (error) {
 | 
				
			||||||
 | 
					                console.error('[DB Migration] Failed to migrate provider_settings table.', error);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Try to recover by dropping the temp table if it exists
 | 
				
			||||||
 | 
					                const oldTableExists = this.getTablesFromDb().includes('provider_settings_old');
 | 
				
			||||||
 | 
					                if (oldTableExists) {
 | 
				
			||||||
 | 
					                    this.db.exec('DROP TABLE provider_settings_old');
 | 
				
			||||||
 | 
					                    console.warn('[DB Migration] Cleaned up temporary old table after failure.');
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                throw error;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async synchronizeSchema() {
 | 
					    async synchronizeSchema() {
 | 
				
			||||||
        console.log('[DB Sync] Starting schema synchronization...');
 | 
					        console.log('[DB Sync] Starting schema synchronization...');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Run special migration for provider_settings before the generic sync logic
 | 
				
			||||||
 | 
					        this._migrateProviderSettings();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const tablesInDb = this.getTablesFromDb();
 | 
					        const tablesInDb = this.getTablesFromDb();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const tableName of Object.keys(LATEST_SCHEMA)) {
 | 
					        for (const tableName of Object.keys(LATEST_SCHEMA)) {
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,6 @@ const { BrowserWindow } = require('electron');
 | 
				
			|||||||
const { spawn } = require('child_process');
 | 
					const { spawn } = require('child_process');
 | 
				
			||||||
const { createSTT } = require('../../common/ai/factory');
 | 
					const { createSTT } = require('../../common/ai/factory');
 | 
				
			||||||
const modelStateService = require('../../common/services/modelStateService');
 | 
					const modelStateService = require('../../common/services/modelStateService');
 | 
				
			||||||
// const { getStoredApiKey, getStoredProvider, getCurrentModelInfo } = require('../../../window/windowManager');
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const COMPLETION_DEBOUNCE_MS = 2000;
 | 
					const COMPLETION_DEBOUNCE_MS = 2000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -134,7 +133,7 @@ class SttService {
 | 
				
			|||||||
    async initializeSttSessions(language = 'en') {
 | 
					    async initializeSttSessions(language = 'en') {
 | 
				
			||||||
        const effectiveLanguage = process.env.OPENAI_TRANSCRIBE_LANG || language || 'en';
 | 
					        const effectiveLanguage = process.env.OPENAI_TRANSCRIBE_LANG || language || 'en';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const modelInfo = modelStateService.getCurrentModelInfo('stt');
 | 
					        const modelInfo = await modelStateService.getCurrentModelInfo('stt');
 | 
				
			||||||
        if (!modelInfo || !modelInfo.apiKey) {
 | 
					        if (!modelInfo || !modelInfo.apiKey) {
 | 
				
			||||||
            throw new Error('AI model or API key is not configured.');
 | 
					            throw new Error('AI model or API key is not configured.');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -467,7 +466,7 @@ class SttService {
 | 
				
			|||||||
        let modelInfo = this.modelInfo;
 | 
					        let modelInfo = this.modelInfo;
 | 
				
			||||||
        if (!modelInfo) {
 | 
					        if (!modelInfo) {
 | 
				
			||||||
            console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...');
 | 
					            console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...');
 | 
				
			||||||
            modelInfo = modelStateService.getCurrentModelInfo('stt');
 | 
					            modelInfo = await modelStateService.getCurrentModelInfo('stt');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (!modelInfo) {
 | 
					        if (!modelInfo) {
 | 
				
			||||||
            throw new Error('STT model info could not be retrieved.');
 | 
					            throw new Error('STT model info could not be retrieved.');
 | 
				
			||||||
@ -492,7 +491,7 @@ class SttService {
 | 
				
			|||||||
        let modelInfo = this.modelInfo;
 | 
					        let modelInfo = this.modelInfo;
 | 
				
			||||||
        if (!modelInfo) {
 | 
					        if (!modelInfo) {
 | 
				
			||||||
            console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...');
 | 
					            console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...');
 | 
				
			||||||
            modelInfo = modelStateService.getCurrentModelInfo('stt');
 | 
					            modelInfo = await modelStateService.getCurrentModelInfo('stt');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (!modelInfo) {
 | 
					        if (!modelInfo) {
 | 
				
			||||||
            throw new Error('STT model info could not be retrieved.');
 | 
					            throw new Error('STT model info could not be retrieved.');
 | 
				
			||||||
@ -578,7 +577,7 @@ class SttService {
 | 
				
			|||||||
        let modelInfo = this.modelInfo;
 | 
					        let modelInfo = this.modelInfo;
 | 
				
			||||||
        if (!modelInfo) {
 | 
					        if (!modelInfo) {
 | 
				
			||||||
            console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...');
 | 
					            console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...');
 | 
				
			||||||
            modelInfo = modelStateService.getCurrentModelInfo('stt');
 | 
					            modelInfo = await modelStateService.getCurrentModelInfo('stt');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (!modelInfo) {
 | 
					        if (!modelInfo) {
 | 
				
			||||||
            throw new Error('STT model info could not be retrieved.');
 | 
					            throw new Error('STT model info could not be retrieved.');
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,6 @@ const { createLLM } = require('../../common/ai/factory');
 | 
				
			|||||||
const sessionRepository = require('../../common/repositories/session');
 | 
					const sessionRepository = require('../../common/repositories/session');
 | 
				
			||||||
const summaryRepository = require('./repositories');
 | 
					const summaryRepository = require('./repositories');
 | 
				
			||||||
const modelStateService = require('../../common/services/modelStateService');
 | 
					const modelStateService = require('../../common/services/modelStateService');
 | 
				
			||||||
// const { getStoredApiKey, getStoredProvider, getCurrentModelInfo } = require('../../../window/windowManager.js');
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SummaryService {
 | 
					class SummaryService {
 | 
				
			||||||
    constructor() {
 | 
					    constructor() {
 | 
				
			||||||
@ -99,7 +98,7 @@ Please build upon this context while analyzing the new conversation segments.
 | 
				
			|||||||
                await sessionRepository.touch(this.currentSessionId);
 | 
					                await sessionRepository.touch(this.currentSessionId);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const modelInfo = modelStateService.getCurrentModelInfo('llm');
 | 
					            const modelInfo = await modelStateService.getCurrentModelInfo('llm');
 | 
				
			||||||
            if (!modelInfo || !modelInfo.apiKey) {
 | 
					            if (!modelInfo || !modelInfo.apiKey) {
 | 
				
			||||||
                throw new Error('AI model or API key is not configured.');
 | 
					                throw new Error('AI model or API key is not configured.');
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
				
			|||||||
@ -26,16 +26,14 @@ const NOTIFICATION_CONFIG = {
 | 
				
			|||||||
// New facade functions for model state management
 | 
					// New facade functions for model state management
 | 
				
			||||||
async function getModelSettings() {
 | 
					async function getModelSettings() {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
        const [config, storedKeys, selectedModels] = await Promise.all([
 | 
					        const [config, storedKeys, selectedModels, availableLlm, availableStt] = await Promise.all([
 | 
				
			||||||
            modelStateService.getProviderConfig(),
 | 
					            modelStateService.getProviderConfig(),
 | 
				
			||||||
            modelStateService.getAllApiKeys(),
 | 
					            modelStateService.getAllApiKeys(),
 | 
				
			||||||
            modelStateService.getSelectedModels(),
 | 
					            modelStateService.getSelectedModels(),
 | 
				
			||||||
 | 
					            modelStateService.getAvailableModels('llm'),
 | 
				
			||||||
 | 
					            modelStateService.getAvailableModels('stt')
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        // 동기 함수들은 별도로 호출
 | 
					 | 
				
			||||||
        const availableLlm = modelStateService.getAvailableModels('llm');
 | 
					 | 
				
			||||||
        const availableStt = modelStateService.getAvailableModels('stt');
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        return { success: true, data: { config, storedKeys, availableLlm, availableStt, selectedModels } };
 | 
					        return { success: true, data: { config, storedKeys, availableLlm, availableStt, selectedModels } };
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
        console.error('[SettingsService] Error getting model settings:', error);
 | 
					        console.error('[SettingsService] Error getting model settings:', error);
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										82
									
								
								src/index.js
									
									
									
									
									
								
							
							
						
						
									
										82
									
								
								src/index.js
									
									
									
									
									
								
							@ -686,75 +686,43 @@ async function startWebStack() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  console.log(`✅ API server started on http://localhost:${apiPort}`);
 | 
					  console.log(`✅ API server started on http://localhost:${apiPort}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  console.log(`🚀 All services ready:`);
 | 
					  console.log(`🚀 All services ready:
 | 
				
			||||||
  console.log(`   Frontend: http://localhost:${frontendPort}`);
 | 
					   Frontend: http://localhost:${frontendPort}
 | 
				
			||||||
  console.log(`   API:      http://localhost:${apiPort}`);
 | 
					   API:      http://localhost:${apiPort}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return frontendPort;
 | 
					  return frontendPort;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Auto-update initialization
 | 
					// Auto-update initialization
 | 
				
			||||||
async function initAutoUpdater() {
 | 
					async function initAutoUpdater() {
 | 
				
			||||||
 | 
					    if (process.env.NODE_ENV === 'development') {
 | 
				
			||||||
 | 
					        console.log('Development environment, skipping auto-updater.');
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
        const autoUpdateEnabled = await settingsService.getAutoUpdateSetting();
 | 
					        await autoUpdater.checkForUpdates();
 | 
				
			||||||
        if (!autoUpdateEnabled) {
 | 
					        autoUpdater.on('update-available', () => {
 | 
				
			||||||
            console.log('[AutoUpdater] Skipped because auto-updates are disabled in settings');
 | 
					            console.log('Update available!');
 | 
				
			||||||
            return;
 | 
					            autoUpdater.downloadUpdate();
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        // Skip auto-updater in development mode
 | 
					 | 
				
			||||||
        if (!app.isPackaged) {
 | 
					 | 
				
			||||||
            console.log('[AutoUpdater] Skipped in development (app is not packaged)');
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        autoUpdater.setFeedURL({
 | 
					 | 
				
			||||||
            provider: 'github',
 | 
					 | 
				
			||||||
            owner: 'pickle-com',
 | 
					 | 
				
			||||||
            repo: 'glass',
 | 
					 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					        autoUpdater.on('update-downloaded', (event, releaseNotes, releaseName, date, url) => {
 | 
				
			||||||
        // Immediately check for updates & notify
 | 
					            console.log('Update downloaded:', releaseNotes, releaseName, date, url);
 | 
				
			||||||
        autoUpdater.checkForUpdatesAndNotify()
 | 
					            dialog.showMessageBox({
 | 
				
			||||||
            .catch(err => {
 | 
					 | 
				
			||||||
                console.error('[AutoUpdater] Error checking for updates:', err);
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        autoUpdater.on('checking-for-update', () => {
 | 
					 | 
				
			||||||
            console.log('[AutoUpdater] Checking for updates…');
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        autoUpdater.on('update-available', (info) => {
 | 
					 | 
				
			||||||
            console.log('[AutoUpdater] Update available:', info.version);
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        autoUpdater.on('update-not-available', () => {
 | 
					 | 
				
			||||||
            console.log('[AutoUpdater] Application is up-to-date');
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        autoUpdater.on('error', (err) => {
 | 
					 | 
				
			||||||
            console.error('[AutoUpdater] Error while updating:', err);
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        autoUpdater.on('update-downloaded', (info) => {
 | 
					 | 
				
			||||||
            console.log(`[AutoUpdater] Update downloaded: ${info.version}`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            const dialogOpts = {
 | 
					 | 
				
			||||||
                type: 'info',
 | 
					                type: 'info',
 | 
				
			||||||
                buttons: ['Install now', 'Install on next launch'],
 | 
					                title: 'Application Update',
 | 
				
			||||||
                title: 'Update Available',
 | 
					                message: `A new version of PickleGlass (${releaseName}) has been downloaded. It will be installed the next time you launch the application.`,
 | 
				
			||||||
                message: 'A new version of Glass is ready to be installed.',
 | 
					                buttons: ['Restart', 'Later']
 | 
				
			||||||
                defaultId: 0,
 | 
					            }).then(response => {
 | 
				
			||||||
                cancelId: 1
 | 
					                if (response.response === 0) {
 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            dialog.showMessageBox(dialogOpts).then((returnValue) => {
 | 
					 | 
				
			||||||
                // returnValue.response 0 is for 'Install Now'
 | 
					 | 
				
			||||||
                if (returnValue.response === 0) {
 | 
					 | 
				
			||||||
                    autoUpdater.quitAndInstall();
 | 
					                    autoUpdater.quitAndInstall();
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    } catch (e) {
 | 
					        autoUpdater.on('error', (err) => {
 | 
				
			||||||
        console.error('[AutoUpdater] Failed to initialise:', e);
 | 
					            console.error('Error in auto-updater:', err);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					        console.error('Error initializing auto-updater:', err);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -98,8 +98,6 @@ contextBridge.exposeInMainWorld('api', {
 | 
				
			|||||||
    removeOnAuthFailed: (callback) => ipcRenderer.removeListener('auth-failed', callback),
 | 
					    removeOnAuthFailed: (callback) => ipcRenderer.removeListener('auth-failed', callback),
 | 
				
			||||||
    onForceShowApiKeyHeader: (callback) => ipcRenderer.on('force-show-apikey-header', callback),
 | 
					    onForceShowApiKeyHeader: (callback) => ipcRenderer.on('force-show-apikey-header', callback),
 | 
				
			||||||
    removeOnForceShowApiKeyHeader: (callback) => ipcRenderer.removeListener('force-show-apikey-header', callback),
 | 
					    removeOnForceShowApiKeyHeader: (callback) => ipcRenderer.removeListener('force-show-apikey-header', callback),
 | 
				
			||||||
    onForceShowPermissionHeader: (callback) => ipcRenderer.on('force-show-permission-header', callback),
 | 
					 | 
				
			||||||
    removeOnForceShowPermissionHeader: (callback) => ipcRenderer.removeListener('force-show-permission-header', callback)
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // src/ui/app/MainHeader.js
 | 
					  // src/ui/app/MainHeader.js
 | 
				
			||||||
 | 
				
			|||||||
@ -48,6 +48,9 @@ class HeaderTransitionManager {
 | 
				
			|||||||
                console.log('[HeaderController] ensureHeader: Header of type:', type, 'created.');
 | 
					                console.log('[HeaderController] ensureHeader: Header of type:', type, 'created.');
 | 
				
			||||||
            } else if (type === 'permission') {
 | 
					            } else if (type === 'permission') {
 | 
				
			||||||
                this.permissionHeader = document.createElement('permission-setup');
 | 
					                this.permissionHeader = document.createElement('permission-setup');
 | 
				
			||||||
 | 
					                this.permissionHeader.addEventListener('request-resize', e => {
 | 
				
			||||||
 | 
					                    this._resizeForPermissionHeader(e.detail.height); 
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
                this.permissionHeader.continueCallback = async () => {
 | 
					                this.permissionHeader.continueCallback = async () => {
 | 
				
			||||||
                    if (window.api && window.api.headerController) {
 | 
					                    if (window.api && window.api.headerController) {
 | 
				
			||||||
                        console.log('[HeaderController] Re-initializing model state after permission grant...');
 | 
					                        console.log('[HeaderController] Re-initializing model state after permission grant...');
 | 
				
			||||||
@ -198,7 +201,19 @@ class HeaderTransitionManager {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await this._resizeForPermissionHeader();
 | 
					        let initialHeight = 220;
 | 
				
			||||||
 | 
					        if (window.api) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                const userState = await window.api.common.getCurrentUser();
 | 
				
			||||||
 | 
					                if (userState.mode === 'firebase') {
 | 
				
			||||||
 | 
					                    initialHeight = 280;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } catch (e) {
 | 
				
			||||||
 | 
					                console.error('Could not get user state for resize', e);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await this._resizeForPermissionHeader(initialHeight);
 | 
				
			||||||
        this.ensureHeader('permission');
 | 
					        this.ensureHeader('permission');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -223,9 +238,10 @@ class HeaderTransitionManager {
 | 
				
			|||||||
        return window.api.headerController.resizeHeaderWindow({ width: 456, height: height }).catch(() => {});
 | 
					        return window.api.headerController.resizeHeaderWindow({ width: 456, height: height }).catch(() => {});
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async _resizeForPermissionHeader() {
 | 
					    async _resizeForPermissionHeader(height) {
 | 
				
			||||||
        if (!window.api) return;
 | 
					        if (!window.api) return;
 | 
				
			||||||
        return window.api.headerController.resizeHeaderWindow({ width: 285, height: 220 })
 | 
					        const finalHeight = height || 220;
 | 
				
			||||||
 | 
					        return window.api.headerController.resizeHeaderWindow({ width: 285, height: finalHeight })
 | 
				
			||||||
            .catch(() => {});
 | 
					            .catch(() => {});
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -28,7 +28,7 @@ export class PermissionHeader extends LitElement {
 | 
				
			|||||||
        .container {
 | 
					        .container {
 | 
				
			||||||
            -webkit-app-region: drag;
 | 
					            -webkit-app-region: drag;
 | 
				
			||||||
            width: 285px;
 | 
					            width: 285px;
 | 
				
			||||||
            height: 220px;
 | 
					            /* height is now set dynamically */
 | 
				
			||||||
            padding: 18px 20px;
 | 
					            padding: 18px 20px;
 | 
				
			||||||
            background: rgba(0, 0, 0, 0.3);
 | 
					            background: rgba(0, 0, 0, 0.3);
 | 
				
			||||||
            border-radius: 16px;
 | 
					            border-radius: 16px;
 | 
				
			||||||
@ -103,6 +103,12 @@ export class PermissionHeader extends LitElement {
 | 
				
			|||||||
            margin-top: auto;
 | 
					            margin-top: auto;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .form-content.all-granted {
 | 
				
			||||||
 | 
					            flex-grow: 1;
 | 
				
			||||||
 | 
					            justify-content: center;
 | 
				
			||||||
 | 
					            margin-top: 0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        .subtitle {
 | 
					        .subtitle {
 | 
				
			||||||
            color: rgba(255, 255, 255, 0.7);
 | 
					            color: rgba(255, 255, 255, 0.7);
 | 
				
			||||||
            font-size: 11px;
 | 
					            font-size: 11px;
 | 
				
			||||||
@ -260,7 +266,8 @@ export class PermissionHeader extends LitElement {
 | 
				
			|||||||
        screenGranted: { type: String },
 | 
					        screenGranted: { type: String },
 | 
				
			||||||
        keychainGranted: { type: String },
 | 
					        keychainGranted: { type: String },
 | 
				
			||||||
        isChecking: { type: String },
 | 
					        isChecking: { type: String },
 | 
				
			||||||
        continueCallback: { type: Function }
 | 
					        continueCallback: { type: Function },
 | 
				
			||||||
 | 
					        userMode: { type: String }, // 'local' or 'firebase'
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor() {
 | 
					    constructor() {
 | 
				
			||||||
@ -270,14 +277,47 @@ export class PermissionHeader extends LitElement {
 | 
				
			|||||||
        this.keychainGranted = 'unknown';
 | 
					        this.keychainGranted = 'unknown';
 | 
				
			||||||
        this.isChecking = false;
 | 
					        this.isChecking = false;
 | 
				
			||||||
        this.continueCallback = null;
 | 
					        this.continueCallback = null;
 | 
				
			||||||
 | 
					        this.userMode = 'local'; // Default to local
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    updated(changedProperties) {
 | 
				
			||||||
 | 
					        super.updated(changedProperties);
 | 
				
			||||||
 | 
					        if (changedProperties.has('userMode')) {
 | 
				
			||||||
 | 
					            const newHeight = this.userMode === 'firebase' ? 280 : 220;
 | 
				
			||||||
 | 
					            console.log(`[PermissionHeader] User mode changed to ${this.userMode}, requesting resize to ${newHeight}px`);
 | 
				
			||||||
 | 
					            this.dispatchEvent(new CustomEvent('request-resize', {
 | 
				
			||||||
 | 
					                detail: { height: newHeight },
 | 
				
			||||||
 | 
					                bubbles: true,
 | 
				
			||||||
 | 
					                composed: true
 | 
				
			||||||
 | 
					            }));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async connectedCallback() {
 | 
					    async connectedCallback() {
 | 
				
			||||||
        super.connectedCallback();
 | 
					        super.connectedCallback();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (window.api) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                const userState = await window.api.common.getCurrentUser();
 | 
				
			||||||
 | 
					                this.userMode = userState.mode;
 | 
				
			||||||
 | 
					            } catch (e) {
 | 
				
			||||||
 | 
					                console.error('[PermissionHeader] Failed to get user state', e);
 | 
				
			||||||
 | 
					                this.userMode = 'local'; // Fallback to local
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await this.checkPermissions();
 | 
					        await this.checkPermissions();
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        // Set up periodic permission check
 | 
					        // Set up periodic permission check
 | 
				
			||||||
        this.permissionCheckInterval = setInterval(() => {
 | 
					        this.permissionCheckInterval = setInterval(async () => {
 | 
				
			||||||
 | 
					            if (window.api) {
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    const userState = await window.api.common.getCurrentUser();
 | 
				
			||||||
 | 
					                    this.userMode = userState.mode;
 | 
				
			||||||
 | 
					                } catch (e) {
 | 
				
			||||||
 | 
					                    this.userMode = 'local';
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            this.checkPermissions();
 | 
					            this.checkPermissions();
 | 
				
			||||||
        }, 1000);
 | 
					        }, 1000);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -311,11 +351,14 @@ export class PermissionHeader extends LitElement {
 | 
				
			|||||||
                console.log('[PermissionHeader] Permission status changed, updating UI');
 | 
					                console.log('[PermissionHeader] Permission status changed, updating UI');
 | 
				
			||||||
                this.requestUpdate();
 | 
					                this.requestUpdate();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const isKeychainRequired = this.userMode === 'firebase';
 | 
				
			||||||
 | 
					            const keychainOk = !isKeychainRequired || this.keychainGranted === 'granted';
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            // if all permissions granted == automatically continue
 | 
					            // if all permissions granted == automatically continue
 | 
				
			||||||
            if (this.microphoneGranted === 'granted' && 
 | 
					            if (this.microphoneGranted === 'granted' && 
 | 
				
			||||||
                this.screenGranted === 'granted' && 
 | 
					                this.screenGranted === 'granted' && 
 | 
				
			||||||
                this.keychainGranted === 'granted' && 
 | 
					                keychainOk && 
 | 
				
			||||||
                this.continueCallback) {
 | 
					                this.continueCallback) {
 | 
				
			||||||
                console.log('[PermissionHeader] All permissions granted, proceeding automatically');
 | 
					                console.log('[PermissionHeader] All permissions granted, proceeding automatically');
 | 
				
			||||||
                setTimeout(() => this.handleContinue(), 500);
 | 
					                setTimeout(() => this.handleContinue(), 500);
 | 
				
			||||||
@ -405,12 +448,15 @@ export class PermissionHeader extends LitElement {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async handleContinue() {
 | 
					    async handleContinue() {
 | 
				
			||||||
 | 
					        const isKeychainRequired = this.userMode === 'firebase';
 | 
				
			||||||
 | 
					        const keychainOk = !isKeychainRequired || this.keychainGranted === 'granted';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (this.continueCallback && 
 | 
					        if (this.continueCallback && 
 | 
				
			||||||
            this.microphoneGranted === 'granted' && 
 | 
					            this.microphoneGranted === 'granted' && 
 | 
				
			||||||
            this.screenGranted === 'granted' && 
 | 
					            this.screenGranted === 'granted' && 
 | 
				
			||||||
            this.keychainGranted === 'granted') {
 | 
					            keychainOk) {
 | 
				
			||||||
            // Mark permissions as completed
 | 
					            // Mark permissions as completed
 | 
				
			||||||
            if (window.api) {
 | 
					            if (window.api && isKeychainRequired) {
 | 
				
			||||||
                try {
 | 
					                try {
 | 
				
			||||||
                    await window.api.permissionHeader.markKeychainCompleted();
 | 
					                    await window.api.permissionHeader.markKeychainCompleted();
 | 
				
			||||||
                    console.log('[PermissionHeader] Marked keychain as completed');
 | 
					                    console.log('[PermissionHeader] Marked keychain as completed');
 | 
				
			||||||
@ -431,10 +477,13 @@ export class PermissionHeader extends LitElement {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    render() {
 | 
					    render() {
 | 
				
			||||||
        const allGranted = this.microphoneGranted === 'granted' && this.screenGranted === 'granted' && this.keychainGranted === 'granted';
 | 
					        const isKeychainRequired = this.userMode === 'firebase';
 | 
				
			||||||
 | 
					        const containerHeight = isKeychainRequired ? 280 : 220;
 | 
				
			||||||
 | 
					        const keychainOk = !isKeychainRequired || this.keychainGranted === 'granted';
 | 
				
			||||||
 | 
					        const allGranted = this.microphoneGranted === 'granted' && this.screenGranted === 'granted' && keychainOk;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return html`
 | 
					        return html`
 | 
				
			||||||
            <div class="container">
 | 
					            <div class="container" style="height: ${containerHeight}px">
 | 
				
			||||||
                <button class="close-button" @click=${this.handleClose} title="Close application">
 | 
					                <button class="close-button" @click=${this.handleClose} title="Close application">
 | 
				
			||||||
                    <svg width="8" height="8" viewBox="0 0 10 10" fill="currentColor">
 | 
					                    <svg width="8" height="8" viewBox="0 0 10 10" fill="currentColor">
 | 
				
			||||||
                        <path d="M1 1L9 9M9 1L1 9" stroke="currentColor" stroke-width="1.2" />
 | 
					                        <path d="M1 1L9 9M9 1L1 9" stroke="currentColor" stroke-width="1.2" />
 | 
				
			||||||
@ -442,89 +491,92 @@ export class PermissionHeader extends LitElement {
 | 
				
			|||||||
                </button>
 | 
					                </button>
 | 
				
			||||||
                <h1 class="title">Permission Setup Required</h1>
 | 
					                <h1 class="title">Permission Setup Required</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <div class="form-content">
 | 
					                <div class="form-content ${allGranted ? 'all-granted' : ''}">
 | 
				
			||||||
                    <div class="subtitle">Grant access to microphone, screen recording and keychain to continue</div>
 | 
					                    ${!allGranted ? html`
 | 
				
			||||||
                    
 | 
					                        <div class="subtitle">Grant access to microphone, screen recording${isKeychainRequired ? ' and keychain' : ''} to continue</div>
 | 
				
			||||||
                    <div class="permission-status">
 | 
					 | 
				
			||||||
                        <div class="permission-item ${this.microphoneGranted === 'granted' ? 'granted' : ''}">
 | 
					 | 
				
			||||||
                            ${this.microphoneGranted === 'granted' ? html`
 | 
					 | 
				
			||||||
                                <svg class="check-icon" viewBox="0 0 20 20" fill="currentColor">
 | 
					 | 
				
			||||||
                                    <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
 | 
					 | 
				
			||||||
                                </svg>
 | 
					 | 
				
			||||||
                                <span>Microphone ✓</span>
 | 
					 | 
				
			||||||
                            ` : html`
 | 
					 | 
				
			||||||
                                <svg class="permission-icon" viewBox="0 0 20 20" fill="currentColor">
 | 
					 | 
				
			||||||
                                    <path fill-rule="evenodd" d="M7 4a3 3 0 016 0v4a3 3 0 11-6 0V4zm4 10.93A7.001 7.001 0 0017 8a1 1 0 10-2 0A5 5 0 015 8a1 1 0 00-2 0 7.001 7.001 0 006 6.93V17H6a1 1 0 100 2h8a1 1 0 100-2h-3v-2.07z" clip-rule="evenodd" />
 | 
					 | 
				
			||||||
                                </svg>
 | 
					 | 
				
			||||||
                                <span>Microphone</span>
 | 
					 | 
				
			||||||
                            `}
 | 
					 | 
				
			||||||
                        </div>
 | 
					 | 
				
			||||||
                        
 | 
					                        
 | 
				
			||||||
                        <div class="permission-item ${this.screenGranted === 'granted' ? 'granted' : ''}">
 | 
					                        <div class="permission-status">
 | 
				
			||||||
                            ${this.screenGranted === 'granted' ? html`
 | 
					                            <div class="permission-item ${this.microphoneGranted === 'granted' ? 'granted' : ''}">
 | 
				
			||||||
                                <svg class="check-icon" viewBox="0 0 20 20" fill="currentColor">
 | 
					                                ${this.microphoneGranted === 'granted' ? html`
 | 
				
			||||||
                                    <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
 | 
					                                    <svg class="check-icon" viewBox="0 0 20 20" fill="currentColor">
 | 
				
			||||||
                                </svg>
 | 
					                                        <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
 | 
				
			||||||
                                <span>Screen ✓</span>
 | 
					                                    </svg>
 | 
				
			||||||
                            ` : html`
 | 
					                                    <span>Microphone ✓</span>
 | 
				
			||||||
                                <svg class="permission-icon" viewBox="0 0 20 20" fill="currentColor">
 | 
					                                ` : html`
 | 
				
			||||||
                                    <path fill-rule="evenodd" d="M3 5a2 2 0 012-2h10a2 2 0 012 2v8a2 2 0 01-2 2h-2.22l.123.489.804.804A1 1 0 0113 18H7a1 1 0 01-.707-1.707l.804-.804L7.22 15H5a2 2 0 01-2-2V5zm5.771 7H5V5h10v7H8.771z" clip-rule="evenodd" />
 | 
					                                    <svg class="permission-icon" viewBox="0 0 20 20" fill="currentColor">
 | 
				
			||||||
                                </svg>
 | 
					                                        <path fill-rule="evenodd" d="M7 4a3 3 0 016 0v4a3 3 0 11-6 0V4zm4 10.93A7.001 7.001 0 0017 8a1 1 0 10-2 0A5 5 0 015 8a1 1 0 00-2 0 7.001 7.001 0 006 6.93V17H6a1 1 0 100 2h8a1 1 0 100-2h-3v-2.07z" clip-rule="evenodd" />
 | 
				
			||||||
                                <span>Screen Recording</span>
 | 
					                                    </svg>
 | 
				
			||||||
                            `}
 | 
					                                    <span>Microphone</span>
 | 
				
			||||||
 | 
					                                `}
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                            
 | 
				
			||||||
 | 
					                            <div class="permission-item ${this.screenGranted === 'granted' ? 'granted' : ''}">
 | 
				
			||||||
 | 
					                                ${this.screenGranted === 'granted' ? html`
 | 
				
			||||||
 | 
					                                    <svg class="check-icon" viewBox="0 0 20 20" fill="currentColor">
 | 
				
			||||||
 | 
					                                        <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
 | 
				
			||||||
 | 
					                                    </svg>
 | 
				
			||||||
 | 
					                                    <span>Screen ✓</span>
 | 
				
			||||||
 | 
					                                ` : html`
 | 
				
			||||||
 | 
					                                    <svg class="permission-icon" viewBox="0 0 20 20" fill="currentColor">
 | 
				
			||||||
 | 
					                                        <path fill-rule="evenodd" d="M3 5a2 2 0 012-2h10a2 2 0 012 2v8a2 2 0 01-2 2h-2.22l.123.489.804.804A1 1 0 0113 18H7a1 1 0 01-.707-1.707l.804-.804L7.22 15H5a2 2 0 01-2-2V5zm5.771 7H5V5h10v7H8.771z" clip-rule="evenodd" />
 | 
				
			||||||
 | 
					                                    </svg>
 | 
				
			||||||
 | 
					                                    <span>Screen Recording</span>
 | 
				
			||||||
 | 
					                                `}
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            ${isKeychainRequired ? html`
 | 
				
			||||||
 | 
					                                <div class="permission-item ${this.keychainGranted === 'granted' ? 'granted' : ''}">
 | 
				
			||||||
 | 
					                                    ${this.keychainGranted === 'granted' ? html`
 | 
				
			||||||
 | 
					                                        <svg class="check-icon" viewBox="0 0 20 20" fill="currentColor">
 | 
				
			||||||
 | 
					                                            <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
 | 
				
			||||||
 | 
					                                        </svg>
 | 
				
			||||||
 | 
					                                        <span>Data Encryption ✓</span>
 | 
				
			||||||
 | 
					                                    ` : html`
 | 
				
			||||||
 | 
					                                        <svg class="permission-icon" viewBox="0 0 20 20" fill="currentColor">
 | 
				
			||||||
 | 
					                                            <path fill-rule="evenodd" d="M18 8a6 6 0 01-7.744 5.668l-1.649 1.652c-.63.63-1.706.19-1.706-.742V12.18a.75.75 0 00-1.5 0v2.696c0 .932-1.075 1.372-1.706.742l-1.649-1.652A6 6 0 112 8zm-4 0a.75.75 0 00.75-.75A3.75 3.75 0 018.25 4a.75.75 0 000 1.5 2.25 2.25 0 012.25 2.25.75.75 0 00.75.75z" clip-rule="evenodd" />
 | 
				
			||||||
 | 
					                                        </svg>
 | 
				
			||||||
 | 
					                                        <span>Data Encryption</span>
 | 
				
			||||||
 | 
					                                    `}
 | 
				
			||||||
 | 
					                                </div>
 | 
				
			||||||
 | 
					                            ` : ''}
 | 
				
			||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        <div class="permission-item ${this.keychainGranted === 'granted' ? 'granted' : ''}">
 | 
					 | 
				
			||||||
                            ${this.keychainGranted === 'granted' ? html`
 | 
					 | 
				
			||||||
                                <svg class="check-icon" viewBox="0 0 20 20" fill="currentColor">
 | 
					 | 
				
			||||||
                                    <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
 | 
					 | 
				
			||||||
                                </svg>
 | 
					 | 
				
			||||||
                                <span>Keychain ✓</span>
 | 
					 | 
				
			||||||
                            ` : html`
 | 
					 | 
				
			||||||
                                <svg class="permission-icon" viewBox="0 0 20 20" fill="currentColor">
 | 
					 | 
				
			||||||
                                    <path fill-rule="evenodd" d="M18 8a6 6 0 01-7.744 5.668l-1.649 1.652c-.63.63-1.706.19-1.706-.742V12.18a.75.75 0 00-1.5 0v2.696c0 .932-1.075 1.372-1.706.742l-1.649-1.652A6 6 0 112 8zm-4 0a.75.75 0 00.75-.75A3.75 3.75 0 018.25 4a.75.75 0 000 1.5 2.25 2.25 0 012.25 2.25.75.75 0 00.75.75z" clip-rule="evenodd" />
 | 
					 | 
				
			||||||
                                </svg>
 | 
					 | 
				
			||||||
                                <span>Keychain Access</span>
 | 
					 | 
				
			||||||
                            `}
 | 
					 | 
				
			||||||
                        </div>
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    ${this.microphoneGranted !== 'granted' ? html`
 | 
					 | 
				
			||||||
                        <button 
 | 
					                        <button 
 | 
				
			||||||
                            class="action-button" 
 | 
					                            class="action-button" 
 | 
				
			||||||
                            @click=${this.handleMicrophoneClick}
 | 
					                            @click=${this.handleMicrophoneClick}
 | 
				
			||||||
 | 
					                            ?disabled=${this.microphoneGranted === 'granted'}
 | 
				
			||||||
                        >
 | 
					                        >
 | 
				
			||||||
                            Grant Microphone Access
 | 
					                            ${this.microphoneGranted === 'granted' ? 'Microphone Access Granted' : 'Grant Microphone Access'}
 | 
				
			||||||
                        </button>
 | 
					                        </button>
 | 
				
			||||||
                    ` : ''}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    ${this.screenGranted !== 'granted' ? html`
 | 
					 | 
				
			||||||
                        <button 
 | 
					                        <button 
 | 
				
			||||||
                            class="action-button" 
 | 
					                            class="action-button" 
 | 
				
			||||||
                            @click=${this.handleScreenClick}
 | 
					                            @click=${this.handleScreenClick}
 | 
				
			||||||
 | 
					                            ?disabled=${this.screenGranted === 'granted'}
 | 
				
			||||||
                        >
 | 
					                        >
 | 
				
			||||||
                            Grant Screen Recording Access
 | 
					                            ${this.screenGranted === 'granted' ? 'Screen Recording Granted' : 'Grant Screen Recording Access'}
 | 
				
			||||||
                        </button>
 | 
					                        </button>
 | 
				
			||||||
                    ` : ''}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    ${this.keychainGranted !== 'granted' ? html`
 | 
					                        ${isKeychainRequired ? html`
 | 
				
			||||||
                        <button 
 | 
					                            <button 
 | 
				
			||||||
                            class="action-button" 
 | 
					                                class="action-button" 
 | 
				
			||||||
                            @click=${this.handleKeychainClick}
 | 
					                                @click=${this.handleKeychainClick}
 | 
				
			||||||
                        >
 | 
					                                ?disabled=${this.keychainGranted === 'granted'}
 | 
				
			||||||
                            Grant Keychain Access
 | 
					                            >
 | 
				
			||||||
                        </button>
 | 
					                                ${this.keychainGranted === 'granted' ? 'Encryption Enabled' : 'Enable Encryption'}
 | 
				
			||||||
                        <div class="subtitle">System prompt will appear. Select 'Always Allow' for seamless access.</div>
 | 
					                            </button>
 | 
				
			||||||
                    ` : ''}
 | 
					                            <div class="subtitle" style="visibility: ${this.keychainGranted === 'granted' ? 'hidden' : 'visible'}">
 | 
				
			||||||
 | 
					                                Stores the key to encrypt your data. Press "<b>Always Allow</b>" to continue.
 | 
				
			||||||
                    ${allGranted ? html`
 | 
					                            </div>
 | 
				
			||||||
 | 
					                        ` : ''}
 | 
				
			||||||
 | 
					                    ` : html`
 | 
				
			||||||
                        <button 
 | 
					                        <button 
 | 
				
			||||||
                            class="continue-button" 
 | 
					                            class="continue-button" 
 | 
				
			||||||
                            @click=${this.handleContinue}
 | 
					                            @click=${this.handleContinue}
 | 
				
			||||||
                        >
 | 
					                        >
 | 
				
			||||||
                            Continue to Pickle Glass
 | 
					                            Continue to Pickle Glass
 | 
				
			||||||
                        </button>
 | 
					                        </button>
 | 
				
			||||||
                    ` : ''}
 | 
					                    `}
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
        `;
 | 
					        `;
 | 
				
			||||||
 | 
				
			|||||||
@ -918,6 +918,8 @@ export class SettingsView extends LitElement {
 | 
				
			|||||||
        this.setupIpcListeners();
 | 
					        this.setupIpcListeners();
 | 
				
			||||||
        this.setupWindowResize();
 | 
					        this.setupWindowResize();
 | 
				
			||||||
        this.loadAutoUpdateSetting();
 | 
					        this.loadAutoUpdateSetting();
 | 
				
			||||||
 | 
					        // Force one height calculation immediately (innerHeight may be 0 at first)
 | 
				
			||||||
 | 
					        setTimeout(() => this.updateScrollHeight(), 0);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    disconnectedCallback() {
 | 
					    disconnectedCallback() {
 | 
				
			||||||
@ -1030,11 +1032,13 @@ export class SettingsView extends LitElement {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    updateScrollHeight() {
 | 
					    updateScrollHeight() {
 | 
				
			||||||
        const windowHeight = window.innerHeight;
 | 
					        // Electron 일부 시점에서 window.innerHeight 가 0 으로 보고되는 버그 보호
 | 
				
			||||||
        const maxHeight = windowHeight;
 | 
					        const rawHeight = window.innerHeight || (window.screen ? window.screen.height : 0);
 | 
				
			||||||
        
 | 
					        const MIN_HEIGHT = 300; // 최소 보장 높이
 | 
				
			||||||
 | 
					        const maxHeight = Math.max(MIN_HEIGHT, rawHeight);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.style.maxHeight = `${maxHeight}px`;
 | 
					        this.style.maxHeight = `${maxHeight}px`;
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        const container = this.shadowRoot?.querySelector('.settings-container');
 | 
					        const container = this.shadowRoot?.querySelector('.settings-container');
 | 
				
			||||||
        if (container) {
 | 
					        if (container) {
 | 
				
			||||||
            container.style.maxHeight = `${maxHeight}px`;
 | 
					            container.style.maxHeight = `${maxHeight}px`;
 | 
				
			||||||
@ -1043,6 +1047,8 @@ export class SettingsView extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    handleMouseEnter = () => {
 | 
					    handleMouseEnter = () => {
 | 
				
			||||||
        window.api.settingsView.cancelHideSettingsWindow();
 | 
					        window.api.settingsView.cancelHideSettingsWindow();
 | 
				
			||||||
 | 
					        // Recalculate height in case it was set to 0 before
 | 
				
			||||||
 | 
					        this.updateScrollHeight();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    handleMouseLeave = () => {
 | 
					    handleMouseLeave = () => {
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user