keychain permission + modelStateService rely only on db

This commit is contained in:
samtiz 2025-07-15 20:16:38 +09:00
parent a27ab05fa8
commit dad74875a0
19 changed files with 667 additions and 887 deletions

View File

@ -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) => {

View File

@ -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.');
} }

View File

@ -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: [

View File

@ -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
}
} }
} }

View File

@ -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
}; };

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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)) {

View File

@ -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.');

View File

@ -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.');
} }

View File

@ -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);

View File

@ -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);
} }
} }

View File

@ -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

View File

@ -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(() => {});
} }

View File

@ -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>
`; `;

View File

@ -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 = () => {