Merge branch 'refactor/0712_ask' of https://github.com/pickle-com/glass into refactor/0712_ask
This commit is contained in:
		
						commit
						18154e221c
					
				@ -1,34 +1,90 @@
 | 
			
		||||
// src/bridge/featureBridge.js
 | 
			
		||||
const { ipcMain } = require('electron');
 | 
			
		||||
const { ipcMain, app } = require('electron');
 | 
			
		||||
const settingsService = require('../features/settings/settingsService');
 | 
			
		||||
const authService = require('../features/common/services/authService');
 | 
			
		||||
const whisperService = require('../features/common/services/whisperService');
 | 
			
		||||
const ollamaService = require('../features/common/services/ollamaService');
 | 
			
		||||
const modelStateService = require('../features/common/services/modelStateService');
 | 
			
		||||
 | 
			
		||||
const askService = require('../features/ask/askService');
 | 
			
		||||
const listenService = require('../features/listen/listenService');
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
  // Renderer로부터의 요청을 수신
 | 
			
		||||
  initialize() {
 | 
			
		||||
    ipcMain.handle('settings:getPresets', async () => {
 | 
			
		||||
      console.log('[FeatureBridge] settings:getPresets 호출됨');
 | 
			
		||||
      return await settingsService.getPresets();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('settings:get-auto-update', async () => {
 | 
			
		||||
      console.log('[FeatureBridge] settings:get-auto-update 호출됨');
 | 
			
		||||
      return await settingsService.getAutoUpdateSetting();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('settings:set-auto-update', async (event, isEnabled) => {
 | 
			
		||||
      console.log('[FeatureBridge] settings:set-auto-update 호출됨', isEnabled);
 | 
			
		||||
      return await settingsService.setAutoUpdateSetting(isEnabled);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // New IPC handler for loadInitialData
 | 
			
		||||
    ipcMain.handle('settings:loadInitialData', async () => {
 | 
			
		||||
      console.log('[FeatureBridge] settings:loadInitialData called');
 | 
			
		||||
      return await settingsService.loadInitialData();
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    console.log('[FeatureBridge] Initialized with settings handlers.');
 | 
			
		||||
    // Settings Service
 | 
			
		||||
    ipcMain.handle('settings:getPresets', async () => await settingsService.getPresets());
 | 
			
		||||
    ipcMain.handle('settings:get-auto-update', async () => await settingsService.getAutoUpdateSetting());
 | 
			
		||||
    ipcMain.handle('settings:set-auto-update', async (event, isEnabled) => await settingsService.setAutoUpdateSetting(isEnabled));  
 | 
			
		||||
    ipcMain.handle('settings:get-model-settings', async () => await settingsService.getModelSettings());
 | 
			
		||||
    ipcMain.handle('settings:validate-and-save-key', async (e, { provider, key }) => await settingsService.validateAndSaveKey(provider, key));
 | 
			
		||||
    ipcMain.handle('settings:clear-api-key', async (e, { provider }) => await settingsService.clearApiKey(provider));
 | 
			
		||||
    ipcMain.handle('settings:set-selected-model', async (e, { type, modelId }) => await settingsService.setSelectedModel(type, modelId));    
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('settings:get-ollama-status', async () => await settingsService.getOllamaStatus());
 | 
			
		||||
    ipcMain.handle('settings:ensure-ollama-ready', async () => await settingsService.ensureOllamaReady());
 | 
			
		||||
    ipcMain.handle('settings:shutdown-ollama', async () => await settingsService.shutdownOllama());
 | 
			
		||||
 | 
			
		||||
    // User/Auth
 | 
			
		||||
    ipcMain.handle('get-current-user', () => authService.getCurrentUser());
 | 
			
		||||
    ipcMain.handle('start-firebase-auth', async () => await authService.startFirebaseAuthFlow());
 | 
			
		||||
    ipcMain.handle('firebase-logout', async () => await authService.signOut());
 | 
			
		||||
 | 
			
		||||
    // App
 | 
			
		||||
    ipcMain.handle('quit-application', () => app.quit());
 | 
			
		||||
 | 
			
		||||
    // Whisper
 | 
			
		||||
    ipcMain.handle('whisper:download-model', async (event, modelId) => await whisperService.handleDownloadModel(event, modelId));
 | 
			
		||||
    ipcMain.handle('whisper:get-installed-models', async () => await whisperService.handleGetInstalledModels());
 | 
			
		||||
       
 | 
			
		||||
    // General
 | 
			
		||||
    ipcMain.handle('get-preset-templates', () => presetRepository.getPresetTemplates());
 | 
			
		||||
    ipcMain.handle('get-web-url', () => process.env.pickleglass_WEB_URL || 'http://localhost:3000');
 | 
			
		||||
 | 
			
		||||
    // Ollama
 | 
			
		||||
    ipcMain.handle('ollama:get-status', async () => await ollamaService.handleGetStatus());
 | 
			
		||||
    ipcMain.handle('ollama:install', async (event) => await ollamaService.handleInstall(event));
 | 
			
		||||
    ipcMain.handle('ollama:start-service', async (event) => await ollamaService.handleStartService(event));
 | 
			
		||||
    ipcMain.handle('ollama:ensure-ready', async () => await ollamaService.handleEnsureReady());
 | 
			
		||||
    ipcMain.handle('ollama:get-models', async () => await ollamaService.handleGetModels());
 | 
			
		||||
    ipcMain.handle('ollama:get-model-suggestions', async () => await ollamaService.handleGetModelSuggestions());
 | 
			
		||||
    ipcMain.handle('ollama:pull-model', async (event, modelName) => await ollamaService.handlePullModel(event, modelName));
 | 
			
		||||
    ipcMain.handle('ollama:is-model-installed', async (event, modelName) => await ollamaService.handleIsModelInstalled(modelName));
 | 
			
		||||
    ipcMain.handle('ollama:warm-up-model', async (event, modelName) => await ollamaService.handleWarmUpModel(modelName));
 | 
			
		||||
    ipcMain.handle('ollama:auto-warm-up', async () => await ollamaService.handleAutoWarmUp());
 | 
			
		||||
    ipcMain.handle('ollama:get-warm-up-status', async () => await ollamaService.handleGetWarmUpStatus());
 | 
			
		||||
    ipcMain.handle('ollama:shutdown', async (event, force = false) => await ollamaService.handleShutdown(event, force));
 | 
			
		||||
 | 
			
		||||
    // Ask
 | 
			
		||||
    ipcMain.handle('ask:sendMessage', async (event, userPrompt, conversationHistoryRaw = []) => await askService.sendMessage(userPrompt, conversationHistoryRaw));
 | 
			
		||||
  
 | 
			
		||||
    // Listen
 | 
			
		||||
    ipcMain.handle('send-audio-content', async (event, { data, mimeType }) => await listenService.handleSendAudioContent(data, mimeType));
 | 
			
		||||
    ipcMain.handle('send-system-audio-content', async (event, { data, mimeType }) => {
 | 
			
		||||
        const result = await listenService.sttService.sendSystemAudioContent(data, mimeType);
 | 
			
		||||
        if(result.success) {
 | 
			
		||||
            listenService.sendToRenderer('system-audio-data', { data });
 | 
			
		||||
        }
 | 
			
		||||
        return result;
 | 
			
		||||
    });
 | 
			
		||||
    ipcMain.handle('start-macos-audio', async () => await listenService.handleStartMacosAudio());
 | 
			
		||||
    ipcMain.handle('stop-macos-audio', async () => await listenService.handleStopMacosAudio());
 | 
			
		||||
    ipcMain.handle('update-google-search-setting', async (event, enabled) => await listenService.handleUpdateGoogleSearchSetting(enabled));
 | 
			
		||||
 | 
			
		||||
     // ModelStateService
 | 
			
		||||
    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: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:get-selected-models', () => modelStateService.getSelectedModels());
 | 
			
		||||
    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:are-providers-configured', () => modelStateService.areProvidersConfigured());
 | 
			
		||||
    ipcMain.handle('model:get-current-model-info', (e, { type }) => modelStateService.getCurrentModelInfo(type));
 | 
			
		||||
    ipcMain.handle('model:get-provider-config', () => modelStateService.getProviderConfig());
 | 
			
		||||
 | 
			
		||||
    console.log('[FeatureBridge] Initialized with all feature handlers.');
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    ipcMain.handle('listen:changeSession', async (event, listenButtonText) => {
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,155 @@
 | 
			
		||||
// src/bridge/windowBridge.js
 | 
			
		||||
const { ipcMain, BrowserWindow } = require('electron');
 | 
			
		||||
const { windowPool, settingsHideTimer, app, shell } = require('../window/windowManager');  // 필요 변수 require
 | 
			
		||||
const { ipcMain, BrowserWindow, globalShortcut } = require('electron');
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
  // Renderer로부터의 요청을 수신
 | 
			
		||||
  initialize() {
 | 
			
		||||
  // windowManager에서 필요한 변수들을 매개변수로 받도록 수정
 | 
			
		||||
  initialize(windowPool, app, shell, getCurrentDisplay, createFeatureWindows, movementManager, getContentProtectionStatus, setContentProtection, updateLayout) {
 | 
			
		||||
    let settingsHideTimer = null;
 | 
			
		||||
 | 
			
		||||
    // 기존
 | 
			
		||||
    ipcMain.on('window:hide', (e) => BrowserWindow.fromWebContents(e.sender)?.hide());
 | 
			
		||||
 | 
			
		||||
    // windowManager 관련 추가
 | 
			
		||||
    ipcMain.handle('toggle-content-protection', () => {
 | 
			
		||||
      // windowManager의 toggle-content-protection 로직
 | 
			
		||||
      const newStatus = !getContentProtectionStatus();
 | 
			
		||||
      setContentProtection(newStatus);
 | 
			
		||||
      return newStatus;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('resize-header-window', (event, { width, height }) => {
 | 
			
		||||
      const header = windowPool.get('header');
 | 
			
		||||
      if (header) {
 | 
			
		||||
        console.log(`[WindowBridge] Resize request: ${width}x${height}`);
 | 
			
		||||
        
 | 
			
		||||
        // Prevent resizing during animations or if already at target size
 | 
			
		||||
        if (movementManager && movementManager.isAnimating) {
 | 
			
		||||
          console.log('[WindowBridge] Skipping resize during animation');
 | 
			
		||||
          return { success: false, error: 'Cannot resize during animation' };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const currentBounds = header.getBounds();
 | 
			
		||||
        console.log(`[WindowBridge] Current bounds: ${currentBounds.width}x${currentBounds.height} at (${currentBounds.x}, ${currentBounds.y})`);
 | 
			
		||||
        
 | 
			
		||||
        // Skip if already at target size to prevent unnecessary operations
 | 
			
		||||
        if (currentBounds.width === width && currentBounds.height === height) {
 | 
			
		||||
          console.log('[WindowBridge] Already at target size, skipping resize');
 | 
			
		||||
          return { success: true };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const wasResizable = header.isResizable();
 | 
			
		||||
        if (!wasResizable) {
 | 
			
		||||
          header.setResizable(true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Calculate the center point of the current window
 | 
			
		||||
        const centerX = currentBounds.x + currentBounds.width / 2;
 | 
			
		||||
        // Calculate new X position to keep the window centered
 | 
			
		||||
        const newX = Math.round(centerX - width / 2);
 | 
			
		||||
 | 
			
		||||
        // Get the current display to ensure we stay within bounds
 | 
			
		||||
        const display = getCurrentDisplay(header);
 | 
			
		||||
        const { x: workAreaX, width: workAreaWidth } = display.workArea;
 | 
			
		||||
        
 | 
			
		||||
        // Clamp the new position to stay within display bounds
 | 
			
		||||
        const clampedX = Math.max(workAreaX, Math.min(workAreaX + workAreaWidth - width, newX));
 | 
			
		||||
 | 
			
		||||
        header.setBounds({ x: clampedX, y: currentBounds.y, width, height });
 | 
			
		||||
 | 
			
		||||
        if (!wasResizable) {
 | 
			
		||||
          header.setResizable(false);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Update layout after resize
 | 
			
		||||
        if (updateLayout) {
 | 
			
		||||
          updateLayout();
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        return { success: true };
 | 
			
		||||
      }
 | 
			
		||||
      return { success: false, error: 'Header window not found' };
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('get-content-protection-status', () => {
 | 
			
		||||
      return getContentProtectionStatus();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('open-shortcut-editor', () => {
 | 
			
		||||
      // open-shortcut-editor 로직
 | 
			
		||||
      const header = windowPool.get('header');
 | 
			
		||||
      if (!header) return;
 | 
			
		||||
      globalShortcut.unregisterAll();
 | 
			
		||||
      createFeatureWindows(header, 'shortcut-settings');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // 추가: show-settings-window
 | 
			
		||||
    ipcMain.on('show-settings-window', (event, bounds) => {
 | 
			
		||||
      if (!bounds) return;
 | 
			
		||||
      const win = windowPool.get('settings');
 | 
			
		||||
      if (win && !win.isDestroyed()) {
 | 
			
		||||
        if (settingsHideTimer) {
 | 
			
		||||
          clearTimeout(settingsHideTimer);
 | 
			
		||||
          settingsHideTimer = null;
 | 
			
		||||
        }
 | 
			
		||||
        // 위치 조정 로직
 | 
			
		||||
        const header = windowPool.get('header');
 | 
			
		||||
        const headerBounds = header?.getBounds() ?? { x: 0, y: 0 };
 | 
			
		||||
        const settingsBounds = win.getBounds();
 | 
			
		||||
        const disp = getCurrentDisplay(header);
 | 
			
		||||
        const { x: waX, y: waY, width: waW, height: waH } = disp.workArea;
 | 
			
		||||
        let x = Math.round(headerBounds.x + (bounds?.x ?? 0) + (bounds?.width ?? 0) / 2 - settingsBounds.width / 2);
 | 
			
		||||
        let y = Math.round(headerBounds.y + (bounds?.y ?? 0) + (bounds?.height ?? 0) + 31);
 | 
			
		||||
        x = Math.max(waX + 10, Math.min(waX + waW - settingsBounds.width - 10, x));
 | 
			
		||||
        y = Math.max(waY + 10, Math.min(waY + waH - settingsBounds.height - 10, y));
 | 
			
		||||
        win.setBounds({ x, y });
 | 
			
		||||
        win.__lockedByButton = true;
 | 
			
		||||
        win.show();
 | 
			
		||||
        win.moveTop();
 | 
			
		||||
        win.setAlwaysOnTop(true);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.on('hide-settings-window', (event) => {
 | 
			
		||||
      const window = windowPool.get("settings");
 | 
			
		||||
      if (window && !window.isDestroyed()) {
 | 
			
		||||
        if (settingsHideTimer) {
 | 
			
		||||
          clearTimeout(settingsHideTimer);
 | 
			
		||||
        }
 | 
			
		||||
        settingsHideTimer = setTimeout(() => {
 | 
			
		||||
          if (window && !window.isDestroyed()) {
 | 
			
		||||
            window.setAlwaysOnTop(false);
 | 
			
		||||
            window.hide();
 | 
			
		||||
          }
 | 
			
		||||
          settingsHideTimer = null;
 | 
			
		||||
        }, 200);
 | 
			
		||||
        
 | 
			
		||||
        window.__lockedByButton = false;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.on('cancel-hide-settings-window', (event) => {
 | 
			
		||||
      if (settingsHideTimer) {
 | 
			
		||||
        clearTimeout(settingsHideTimer);
 | 
			
		||||
        settingsHideTimer = null;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 로그인 페이지 열기
 | 
			
		||||
    ipcMain.handle('open-login-page', () => {
 | 
			
		||||
      const webUrl = process.env.pickleglass_WEB_URL || 'http://localhost:3000';
 | 
			
		||||
      const personalizeUrl = `${webUrl}/personalize?desktop=true`;
 | 
			
		||||
      shell.openExternal(personalizeUrl);
 | 
			
		||||
      console.log('Opening personalization page:', personalizeUrl);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 윈도우 이동
 | 
			
		||||
    ipcMain.handle('move-window-step', (event, direction) => {
 | 
			
		||||
      if (movementManager) {
 | 
			
		||||
        movementManager.moveStep(direction);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  // Renderer로 상태를 전송
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
const { ipcMain, BrowserWindow } = require('electron');
 | 
			
		||||
const { BrowserWindow } = require('electron');
 | 
			
		||||
const { createStreamingLLM } = require('../common/ai/factory');
 | 
			
		||||
const { getCurrentModelInfo, windowPool, captureScreenshot } = require('../../window/windowManager');
 | 
			
		||||
const sessionRepository = require('../common/repositories/session');
 | 
			
		||||
@ -17,17 +17,6 @@ class AskService {
 | 
			
		||||
        console.log('[AskService] Service instance created.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * IPC 리스너를 등록하여 렌더러 프로세스로부터의 요청을 처리합니다.
 | 
			
		||||
     * Electron 애플리케이션의 메인 프로세스에서 한 번만 호출되어야 합니다.
 | 
			
		||||
     */
 | 
			
		||||
    initialize() {
 | 
			
		||||
        ipcMain.handle('ask:sendMessage', async (event, userPrompt, conversationHistoryRaw=[]) => {
 | 
			
		||||
            return this.sendMessage(userPrompt, conversationHistoryRaw);
 | 
			
		||||
        });
 | 
			
		||||
        console.log('[AskService] Initialized and ready.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 대화 기록 배열을 프롬프트에 적합한 단일 문자열로 변환합니다.
 | 
			
		||||
     * @param {string[]} conversationTexts - 대화 내용 문자열의 배열
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
const { onAuthStateChanged, signInWithCustomToken, signOut } = require('firebase/auth');
 | 
			
		||||
const { BrowserWindow } = require('electron');
 | 
			
		||||
const { BrowserWindow, shell } = require('electron');
 | 
			
		||||
const { getFirebaseAuth } = require('./firebaseClient');
 | 
			
		||||
const fetch = require('node-fetch');
 | 
			
		||||
const encryptionService = require('./encryptionService');
 | 
			
		||||
@ -131,6 +131,19 @@ class AuthService {
 | 
			
		||||
        return this.initializationPromise;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async startFirebaseAuthFlow() {
 | 
			
		||||
        try {
 | 
			
		||||
            const webUrl = process.env.pickleglass_WEB_URL || 'http://localhost:3000';
 | 
			
		||||
            const authUrl = `${webUrl}/login?mode=electron`;
 | 
			
		||||
            console.log(`[AuthService] Opening Firebase auth URL in browser: ${authUrl}`);
 | 
			
		||||
            await shell.openExternal(authUrl);
 | 
			
		||||
            return { success: true };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[AuthService] Failed to open Firebase auth URL:', error);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async signInWithCustomToken(token) {
 | 
			
		||||
        const auth = getFirebaseAuth();
 | 
			
		||||
        try {
 | 
			
		||||
 | 
			
		||||
@ -2,10 +2,10 @@ export class LocalProgressTracker {
 | 
			
		||||
    constructor(serviceName) {
 | 
			
		||||
        this.serviceName = serviceName;
 | 
			
		||||
        this.activeOperations = new Map(); // operationId -> { controller, onProgress }
 | 
			
		||||
        this.ipcRenderer = window.require?.('electron')?.ipcRenderer;
 | 
			
		||||
        
 | 
			
		||||
        if (!this.ipcRenderer) {
 | 
			
		||||
            throw new Error(`${serviceName} requires Electron environment`);
 | 
			
		||||
        // Check if we're in renderer process with window.api available
 | 
			
		||||
        if (!window.api) {
 | 
			
		||||
            throw new Error(`${serviceName} requires Electron environment with contextBridge`);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        this.globalProgressHandler = (event, data) => {
 | 
			
		||||
@ -15,14 +15,14 @@ export class LocalProgressTracker {
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        
 | 
			
		||||
        const progressEvents = {
 | 
			
		||||
            'ollama': 'ollama:pull-progress',
 | 
			
		||||
            'whisper': 'whisper:download-progress'
 | 
			
		||||
        };
 | 
			
		||||
        // Set up progress listeners based on service name
 | 
			
		||||
        if (serviceName.toLowerCase() === 'ollama') {
 | 
			
		||||
            window.api.settingsView.onOllamaPullProgress(this.globalProgressHandler);
 | 
			
		||||
        } else if (serviceName.toLowerCase() === 'whisper') {
 | 
			
		||||
            window.api.settingsView.onWhisperDownloadProgress(this.globalProgressHandler);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        const eventName = progressEvents[serviceName.toLowerCase()] || `${serviceName}:progress`;
 | 
			
		||||
        this.progressEvent = eventName;
 | 
			
		||||
        this.ipcRenderer.on(eventName, this.globalProgressHandler);
 | 
			
		||||
        this.progressEvent = serviceName.toLowerCase();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async trackOperation(operationId, operationType, onProgress) {
 | 
			
		||||
@ -35,15 +35,16 @@ export class LocalProgressTracker {
 | 
			
		||||
        this.activeOperations.set(operationId, operation);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const ipcChannels = {
 | 
			
		||||
                'ollama': { install: 'ollama:pull-model' },
 | 
			
		||||
                'whisper': { download: 'whisper:download-model' }
 | 
			
		||||
            };
 | 
			
		||||
            let result;
 | 
			
		||||
            
 | 
			
		||||
            const channel = ipcChannels[this.serviceName.toLowerCase()]?.[operationType] || 
 | 
			
		||||
                           `${this.serviceName}:${operationType}`;
 | 
			
		||||
            
 | 
			
		||||
            const result = await this.ipcRenderer.invoke(channel, operationId);
 | 
			
		||||
            // Use appropriate API call based on service and operation
 | 
			
		||||
            if (this.serviceName.toLowerCase() === 'ollama' && operationType === 'install') {
 | 
			
		||||
                result = await window.api.settingsView.pullOllamaModel(operationId);
 | 
			
		||||
            } else if (this.serviceName.toLowerCase() === 'whisper' && operationType === 'download') {
 | 
			
		||||
                result = await window.api.settingsView.downloadWhisperModel(operationId);
 | 
			
		||||
            } else {
 | 
			
		||||
                throw new Error(`Unsupported operation: ${this.serviceName}:${operationType}`);
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            if (!result.success) {
 | 
			
		||||
                throw new Error(result.error || `${operationType} failed`);
 | 
			
		||||
@ -93,8 +94,12 @@ export class LocalProgressTracker {
 | 
			
		||||
 | 
			
		||||
    destroy() {
 | 
			
		||||
        this.cancelAllOperations();
 | 
			
		||||
        if (this.ipcRenderer) {
 | 
			
		||||
            this.ipcRenderer.removeListener(this.progressEvent, this.globalProgressHandler);
 | 
			
		||||
        
 | 
			
		||||
        // Remove progress listeners based on service name
 | 
			
		||||
        if (this.progressEvent === 'ollama') {
 | 
			
		||||
            window.api.settingsView.removeOnOllamaPullProgress(this.globalProgressHandler);
 | 
			
		||||
        } else if (this.progressEvent === 'whisper') {
 | 
			
		||||
            window.api.settingsView.removeOnWhisperDownloadProgress(this.globalProgressHandler);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -24,7 +24,6 @@ class ModelStateService {
 | 
			
		||||
    async initialize() {
 | 
			
		||||
        console.log('[ModelStateService] Initializing...');
 | 
			
		||||
        await this._loadStateForCurrentUser();
 | 
			
		||||
        this.setupIpcHandlers();
 | 
			
		||||
        console.log('[ModelStateService] Initialization complete');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -37,15 +36,17 @@ class ModelStateService {
 | 
			
		||||
        console.log(`[ModelStateService] Current Selection -> LLM: ${llmModel || 'None'} (Provider: ${llmProvider}), STT: ${sttModel || 'None'} (Provider: ${sttProvider})`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _autoSelectAvailableModels() {
 | 
			
		||||
        console.log('[ModelStateService] Running auto-selection for models...');
 | 
			
		||||
    _autoSelectAvailableModels(forceReselectionForTypes = []) {
 | 
			
		||||
        console.log(`[ModelStateService] Running auto-selection for models. Force re-selection for: [${forceReselectionForTypes.join(', ')}]`);
 | 
			
		||||
        const types = ['llm', 'stt'];
 | 
			
		||||
 | 
			
		||||
        types.forEach(type => {
 | 
			
		||||
            const currentModelId = this.state.selectedModels[type];
 | 
			
		||||
            let isCurrentModelValid = false;
 | 
			
		||||
 | 
			
		||||
            if (currentModelId) {
 | 
			
		||||
            const forceReselection = forceReselectionForTypes.includes(type);
 | 
			
		||||
 | 
			
		||||
            if (currentModelId && !forceReselection) {
 | 
			
		||||
                const provider = this.getProviderForModel(type, currentModelId);
 | 
			
		||||
                const apiKey = this.getApiKey(provider);
 | 
			
		||||
                // For Ollama, 'local' is a valid API key
 | 
			
		||||
@ -55,7 +56,7 @@ class ModelStateService {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!isCurrentModelValid) {
 | 
			
		||||
                console.log(`[ModelStateService] No valid ${type.toUpperCase()} model selected. Finding an alternative...`);
 | 
			
		||||
                console.log(`[ModelStateService] No valid ${type.toUpperCase()} model selected or re-selection forced. Finding an alternative...`);
 | 
			
		||||
                const availableModels = this.getAvailableModels(type);
 | 
			
		||||
                if (availableModels.length > 0) {
 | 
			
		||||
                    // Prefer API providers over local providers for auto-selection
 | 
			
		||||
@ -329,10 +330,20 @@ class ModelStateService {
 | 
			
		||||
        this._logCurrentSelection();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setApiKey(provider, key) {
 | 
			
		||||
    async setApiKey(provider, key) {
 | 
			
		||||
        if (provider in this.state.apiKeys) {
 | 
			
		||||
            this.state.apiKeys[provider] = key;
 | 
			
		||||
            this._saveState();
 | 
			
		||||
 | 
			
		||||
            const supportedTypes = [];
 | 
			
		||||
            if (PROVIDERS[provider]?.llmModels.length > 0 || provider === 'ollama') {
 | 
			
		||||
                supportedTypes.push('llm');
 | 
			
		||||
            }
 | 
			
		||||
            if (PROVIDERS[provider]?.sttModels.length > 0 || provider === 'whisper') {
 | 
			
		||||
                supportedTypes.push('stt');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this._autoSelectAvailableModels(supportedTypes);
 | 
			
		||||
            await this._saveState();
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
@ -395,6 +406,8 @@ class ModelStateService {
 | 
			
		||||
    areProvidersConfigured() {
 | 
			
		||||
        if (this.isLoggedInWithFirebase()) return true;
 | 
			
		||||
        
 | 
			
		||||
        console.log('[DEBUG] Checking configured providers with apiKeys state:', JSON.stringify(this.state.apiKeys, (key, value) => (value ? '***' : null), 2));
 | 
			
		||||
 | 
			
		||||
        // LLM과 STT 모델을 제공하는 Provider 중 하나라도 API 키가 설정되었는지 확인
 | 
			
		||||
        const hasLlmKey = Object.entries(this.state.apiKeys).some(([provider, key]) => {
 | 
			
		||||
            if (provider === 'ollama') {
 | 
			
		||||
@ -523,10 +536,7 @@ class ModelStateService {
 | 
			
		||||
        if (result.success) {
 | 
			
		||||
            // Use 'local' as placeholder for local services
 | 
			
		||||
            const finalKey = (provider === 'ollama' || provider === 'whisper') ? 'local' : key;
 | 
			
		||||
            this.setApiKey(provider, finalKey);
 | 
			
		||||
            // After setting the key, auto-select models
 | 
			
		||||
            this._autoSelectAvailableModels();
 | 
			
		||||
            this._saveState(); // Ensure state is saved after model selection
 | 
			
		||||
            await this.setApiKey(provider, finalKey);
 | 
			
		||||
        }
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
@ -569,26 +579,6 @@ class ModelStateService {
 | 
			
		||||
        return { provider, model, apiKey };
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    setupIpcHandlers() {
 | 
			
		||||
        ipcMain.handle('model:validate-key', async (e, { provider, key }) => this.handleValidateKey(provider, key));
 | 
			
		||||
        ipcMain.handle('model:get-all-keys', () => this.getAllApiKeys());
 | 
			
		||||
        ipcMain.handle('model:set-api-key', async (e, { provider, key }) => {
 | 
			
		||||
            const success = this.setApiKey(provider, key);
 | 
			
		||||
            if (success) {
 | 
			
		||||
                this._autoSelectAvailableModels();
 | 
			
		||||
                await this._saveState();
 | 
			
		||||
            }
 | 
			
		||||
            return success;
 | 
			
		||||
        });
 | 
			
		||||
        ipcMain.handle('model:remove-api-key', async (e, { provider }) => this.handleRemoveApiKey(provider));
 | 
			
		||||
        ipcMain.handle('model:get-selected-models', () => this.getSelectedModels());
 | 
			
		||||
        ipcMain.handle('model:set-selected-model', async (e, { type, modelId }) => this.handleSetSelectedModel(type, modelId));
 | 
			
		||||
        ipcMain.handle('model:get-available-models', (e, { type }) => this.getAvailableModels(type));
 | 
			
		||||
        ipcMain.handle('model:are-providers-configured', () => this.areProvidersConfigured());
 | 
			
		||||
        ipcMain.handle('model:get-current-model-info', (e, { type }) => this.getCurrentModelInfo(type));
 | 
			
		||||
 | 
			
		||||
        ipcMain.handle('model:get-provider-config', () => this.getProviderConfig());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Export singleton instance
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,7 @@ const { app } = require('electron');
 | 
			
		||||
const LocalAIServiceBase = require('./localAIServiceBase');
 | 
			
		||||
const { spawnAsync } = require('../utils/spawnHelper');
 | 
			
		||||
const { DOWNLOAD_CHECKSUMS } = require('../config/checksums');
 | 
			
		||||
const ollamaModelRepository = require('../repositories/ollamaModel');
 | 
			
		||||
 | 
			
		||||
class OllamaService extends LocalAIServiceBase {
 | 
			
		||||
    constructor() {
 | 
			
		||||
@ -822,6 +823,183 @@ class OllamaService extends LocalAIServiceBase {
 | 
			
		||||
        
 | 
			
		||||
        return models;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleGetStatus() {
 | 
			
		||||
        try {
 | 
			
		||||
            const installed = await this.isInstalled();
 | 
			
		||||
            if (!installed) {
 | 
			
		||||
                return { success: true, installed: false, running: false, models: [] };
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const running = await this.isServiceRunning();
 | 
			
		||||
            if (!running) {
 | 
			
		||||
                return { success: true, installed: true, running: false, models: [] };
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const models = await this.getAllModelsWithStatus();
 | 
			
		||||
            return { success: true, installed: true, running: true, models };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[OllamaService] Error getting status:', error);
 | 
			
		||||
            return { success: false, error: error.message, installed: false, running: false, models: [] };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleInstall(event) {
 | 
			
		||||
        try {
 | 
			
		||||
            const onProgress = (data) => {
 | 
			
		||||
                event.sender.send('ollama:install-progress', data);
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            await this.autoInstall(onProgress);
 | 
			
		||||
 | 
			
		||||
            if (!await this.isServiceRunning()) {
 | 
			
		||||
                onProgress({ stage: 'starting', message: 'Starting Ollama service...', progress: 0 });
 | 
			
		||||
                await this.startService();
 | 
			
		||||
                onProgress({ stage: 'starting', message: 'Ollama service started.', progress: 100 });
 | 
			
		||||
            }
 | 
			
		||||
            event.sender.send('ollama:install-complete', { success: true });
 | 
			
		||||
            return { success: true };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[OllamaService] Failed to install:', error);
 | 
			
		||||
            event.sender.send('ollama:install-complete', { success: false, error: error.message });
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleStartService(event) {
 | 
			
		||||
        try {
 | 
			
		||||
            if (!await this.isServiceRunning()) {
 | 
			
		||||
                console.log('[OllamaService] Starting Ollama service...');
 | 
			
		||||
                await this.startService();
 | 
			
		||||
            }
 | 
			
		||||
            event.sender.send('ollama:install-complete', { success: true });
 | 
			
		||||
            return { success: true };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[OllamaService] Failed to start service:', error);
 | 
			
		||||
            event.sender.send('ollama:install-complete', { success: false, error: error.message });
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleEnsureReady() {
 | 
			
		||||
        try {
 | 
			
		||||
            if (await this.isInstalled() && !await this.isServiceRunning()) {
 | 
			
		||||
                console.log('[OllamaService] Ollama installed but not running, starting service...');
 | 
			
		||||
                await this.startService();
 | 
			
		||||
            }
 | 
			
		||||
            return { success: true };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[OllamaService] Failed to ensure ready:', error);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleGetModels() {
 | 
			
		||||
        try {
 | 
			
		||||
            const models = await this.getAllModelsWithStatus();
 | 
			
		||||
            return { success: true, models };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[OllamaService] Failed to get models:', error);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleGetModelSuggestions() {
 | 
			
		||||
        try {
 | 
			
		||||
            const suggestions = await this.getModelSuggestions();
 | 
			
		||||
            return { success: true, suggestions };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[OllamaService] Failed to get model suggestions:', error);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handlePullModel(event, modelName) {
 | 
			
		||||
        try {
 | 
			
		||||
            console.log(`[OllamaService] Starting model pull: ${modelName}`);
 | 
			
		||||
 | 
			
		||||
            await ollamaModelRepository.updateInstallStatus(modelName, false, true);
 | 
			
		||||
 | 
			
		||||
            const progressHandler = (data) => {
 | 
			
		||||
                if (data.model === modelName) {
 | 
			
		||||
                    event.sender.send('ollama:pull-progress', data);
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            const completeHandler = (data) => {
 | 
			
		||||
                if (data.model === modelName) {
 | 
			
		||||
                    console.log(`[OllamaService] Model ${modelName} pull completed`);
 | 
			
		||||
                    this.removeListener('pull-progress', progressHandler);
 | 
			
		||||
                    this.removeListener('pull-complete', completeHandler);
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            this.on('pull-progress', progressHandler);
 | 
			
		||||
            this.on('pull-complete', completeHandler);
 | 
			
		||||
 | 
			
		||||
            await this.pullModel(modelName);
 | 
			
		||||
 | 
			
		||||
            await ollamaModelRepository.updateInstallStatus(modelName, true, false);
 | 
			
		||||
 | 
			
		||||
            console.log(`[OllamaService] Model ${modelName} pull successful`);
 | 
			
		||||
            return { success: true };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[OllamaService] Failed to pull model:', error);
 | 
			
		||||
            await ollamaModelRepository.updateInstallStatus(modelName, false, false);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleIsModelInstalled(modelName) {
 | 
			
		||||
        try {
 | 
			
		||||
            const installed = await this.isModelInstalled(modelName);
 | 
			
		||||
            return { success: true, installed };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[OllamaService] Failed to check model installation:', error);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleWarmUpModel(modelName) {
 | 
			
		||||
        try {
 | 
			
		||||
            const success = await this.warmUpModel(modelName);
 | 
			
		||||
            return { success };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[OllamaService] Failed to warm up model:', error);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleAutoWarmUp() {
 | 
			
		||||
        try {
 | 
			
		||||
            const success = await this.autoWarmUpSelectedModel();
 | 
			
		||||
            return { success };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[OllamaService] Failed to auto warm-up:', error);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleGetWarmUpStatus() {
 | 
			
		||||
        try {
 | 
			
		||||
            const status = this.getWarmUpStatus();
 | 
			
		||||
            return { success: true, status };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[OllamaService] Failed to get warm-up status:', error);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleShutdown(event, force = false) {
 | 
			
		||||
        try {
 | 
			
		||||
            console.log(`[OllamaService] Manual shutdown requested (force: ${force})`);
 | 
			
		||||
            const success = await this.shutdown(force);
 | 
			
		||||
            return { success };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[OllamaService] Failed to shutdown Ollama:', error);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Export singleton instance
 | 
			
		||||
 | 
			
		||||
@ -169,6 +169,47 @@ class WhisperService extends LocalAIServiceBase {
 | 
			
		||||
        console.log(`[WhisperService] Model ${modelId} downloaded successfully`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleDownloadModel(event, modelId) {
 | 
			
		||||
        try {
 | 
			
		||||
            console.log(`[WhisperService] Handling download for model: ${modelId}`);
 | 
			
		||||
 | 
			
		||||
            if (!this.isInitialized) {
 | 
			
		||||
                await this.initialize();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const progressHandler = (data) => {
 | 
			
		||||
                if (data.modelId === modelId && event && event.sender) {
 | 
			
		||||
                    event.sender.send('whisper:download-progress', data);
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
            
 | 
			
		||||
            this.on('downloadProgress', progressHandler);
 | 
			
		||||
            
 | 
			
		||||
            try {
 | 
			
		||||
                await this.ensureModelAvailable(modelId);
 | 
			
		||||
            } finally {
 | 
			
		||||
                this.removeListener('downloadProgress', progressHandler);
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            return { success: true };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error(`[WhisperService] Failed to handle download for model ${modelId}:`, error);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleGetInstalledModels() {
 | 
			
		||||
        try {
 | 
			
		||||
            if (!this.isInitialized) {
 | 
			
		||||
                await this.initialize();
 | 
			
		||||
            }
 | 
			
		||||
            const models = await this.getInstalledModels();
 | 
			
		||||
            return { success: true, models };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[WhisperService] Failed to get installed models:', error);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getModelPath(modelId) {
 | 
			
		||||
        if (!this.isInitialized || !this.modelsDir) {
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
const { ipcMain, BrowserWindow } = require('electron');
 | 
			
		||||
const { BrowserWindow } = require('electron');
 | 
			
		||||
const SttService = require('./stt/sttService');
 | 
			
		||||
const SummaryService = require('./summary/summaryService');
 | 
			
		||||
const authService = require('../common/services/authService');
 | 
			
		||||
@ -263,70 +263,57 @@ class ListenService {
 | 
			
		||||
        return this.summaryService.getConversationHistory();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setupIpcHandlers() {
 | 
			
		||||
        ipcMain.handle('send-audio-content', async (event, { data, mimeType }) => {
 | 
			
		||||
    _createHandler(asyncFn, successMessage, errorMessage) {
 | 
			
		||||
        return async (...args) => {
 | 
			
		||||
            try {
 | 
			
		||||
                await this.sendAudioContent(data, mimeType);
 | 
			
		||||
                return { success: true };
 | 
			
		||||
                const result = await asyncFn.apply(this, args);
 | 
			
		||||
                if (successMessage) console.log(successMessage);
 | 
			
		||||
                // `startMacOSAudioCapture`는 성공 시 { success, error } 객체를 반환하지 않으므로,
 | 
			
		||||
                // 핸들러가 일관된 응답을 보내도록 여기서 success 객체를 반환합니다.
 | 
			
		||||
                // 다른 함수들은 이미 success 객체를 반환합니다.
 | 
			
		||||
                return result && typeof result.success !== 'undefined' ? result : { success: true };
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
                console.error('Error sending user audio:', e);
 | 
			
		||||
                console.error(errorMessage, e);
 | 
			
		||||
                return { success: false, error: e.message };
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        ipcMain.handle('send-system-audio-content', async (event, { data, mimeType }) => {
 | 
			
		||||
            try {
 | 
			
		||||
                await this.sttService.sendSystemAudioContent(data, mimeType);
 | 
			
		||||
                
 | 
			
		||||
                // Send system audio data back to renderer for AEC reference (like macOS does)
 | 
			
		||||
                this.sendToRenderer('system-audio-data', { data });
 | 
			
		||||
                
 | 
			
		||||
                return { success: true };
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                console.error('Error sending system audio:', error);
 | 
			
		||||
                return { success: false, error: error.message };
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    // `_createHandler`를 사용하여 핸들러들을 동적으로 생성합니다.
 | 
			
		||||
    handleSendAudioContent = this._createHandler(
 | 
			
		||||
        this.sendAudioContent,
 | 
			
		||||
        null,
 | 
			
		||||
        'Error sending user audio:'
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
        ipcMain.handle('start-macos-audio', async () => {
 | 
			
		||||
    handleStartMacosAudio = this._createHandler(
 | 
			
		||||
        async () => {
 | 
			
		||||
            if (process.platform !== 'darwin') {
 | 
			
		||||
                return { success: false, error: 'macOS audio capture only available on macOS' };
 | 
			
		||||
            }
 | 
			
		||||
            if (this.sttService.isMacOSAudioRunning?.()) {
 | 
			
		||||
                return { success: false, error: 'already_running' };
 | 
			
		||||
            }
 | 
			
		||||
            await this.startMacOSAudioCapture();
 | 
			
		||||
            return { success: true, error: null };
 | 
			
		||||
        },
 | 
			
		||||
        'macOS audio capture started.',
 | 
			
		||||
        'Error starting macOS audio capture:'
 | 
			
		||||
    );
 | 
			
		||||
    
 | 
			
		||||
    handleStopMacosAudio = this._createHandler(
 | 
			
		||||
        this.stopMacOSAudioCapture,
 | 
			
		||||
        'macOS audio capture stopped.',
 | 
			
		||||
        'Error stopping macOS audio capture:'
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                const success = await this.startMacOSAudioCapture();
 | 
			
		||||
                return { success, error: null };
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                console.error('Error starting macOS audio capture:', error);
 | 
			
		||||
                return { success: false, error: error.message };
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        ipcMain.handle('stop-macos-audio', async () => {
 | 
			
		||||
            try {
 | 
			
		||||
                this.stopMacOSAudioCapture();
 | 
			
		||||
                return { success: true };
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                console.error('Error stopping macOS audio capture:', error);
 | 
			
		||||
                return { success: false, error: error.message };
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        ipcMain.handle('update-google-search-setting', async (event, enabled) => {
 | 
			
		||||
            try {
 | 
			
		||||
                console.log('Google Search setting updated to:', enabled);
 | 
			
		||||
                return { success: true };
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                console.error('Error updating Google Search setting:', error);
 | 
			
		||||
                return { success: false, error: error.message };
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        console.log('✅ Listen service IPC handlers registered');
 | 
			
		||||
    }
 | 
			
		||||
    handleUpdateGoogleSearchSetting = this._createHandler(
 | 
			
		||||
        async (enabled) => {
 | 
			
		||||
            console.log('Google Search setting updated to:', enabled);
 | 
			
		||||
        },
 | 
			
		||||
        null,
 | 
			
		||||
        'Error updating Google Search setting:'
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const listenService = new ListenService();
 | 
			
		||||
 | 
			
		||||
@ -41,6 +41,17 @@ class SttService {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleSendSystemAudioContent(data, mimeType) {
 | 
			
		||||
        try {
 | 
			
		||||
            await this.sendSystemAudioContent(data, mimeType);
 | 
			
		||||
            this.sendToRenderer('system-audio-data', { data });
 | 
			
		||||
            return { success: true };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('Error sending system audio:', error);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    flushMyCompletion() {
 | 
			
		||||
        const finalText = (this.myCompletionBuffer + this.myCurrentUtterance).trim();
 | 
			
		||||
        if (!this.modelInfo || !finalText) return;
 | 
			
		||||
 | 
			
		||||
@ -423,17 +423,6 @@ function initialize() {
 | 
			
		||||
    // cleanup 
 | 
			
		||||
    windowNotificationManager.cleanup();
 | 
			
		||||
    
 | 
			
		||||
    // IPC handlers for model settings
 | 
			
		||||
    ipcMain.handle('settings:get-model-settings', getModelSettings);
 | 
			
		||||
    ipcMain.handle('settings:validate-and-save-key', (e, { provider, key }) => validateAndSaveKey(provider, key));
 | 
			
		||||
    ipcMain.handle('settings:clear-api-key', (e, { provider }) => clearApiKey(provider));
 | 
			
		||||
    ipcMain.handle('settings:set-selected-model', (e, { type, modelId }) => setSelectedModel(type, modelId));
 | 
			
		||||
 | 
			
		||||
    // IPC handlers for Ollama management
 | 
			
		||||
    ipcMain.handle('settings:get-ollama-status', getOllamaStatus);
 | 
			
		||||
    ipcMain.handle('settings:ensure-ollama-ready', ensureOllamaReady);
 | 
			
		||||
    ipcMain.handle('settings:shutdown-ollama', shutdownOllama);
 | 
			
		||||
    
 | 
			
		||||
    console.log('[SettingsService] Initialized and ready.');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										303
									
								
								src/index.js
									
									
									
									
									
								
							
							
						
						
									
										303
									
								
								src/index.js
									
									
									
									
									
								
							@ -26,7 +26,6 @@ const askService = require('./features/ask/askService');
 | 
			
		||||
const settingsService = require('./features/settings/settingsService');
 | 
			
		||||
const sessionRepository = require('./features/common/repositories/session');
 | 
			
		||||
const modelStateService = require('./features/common/services/modelStateService');
 | 
			
		||||
const sqliteClient = require('./features/common/services/sqliteClient');
 | 
			
		||||
const featureBridge = require('./bridge/featureBridge');
 | 
			
		||||
 | 
			
		||||
// Global variables
 | 
			
		||||
@ -198,13 +197,8 @@ app.whenReady().then(async () => {
 | 
			
		||||
        await modelStateService.initialize();
 | 
			
		||||
        //////// after_modelStateService ////////
 | 
			
		||||
 | 
			
		||||
        listenService.initialize();
 | 
			
		||||
        askService.initialize();
 | 
			
		||||
        settingsService.initialize();
 | 
			
		||||
        featureBridge.initialize();  // 추가: featureBridge 초기화
 | 
			
		||||
        setupGeneralIpcHandlers();
 | 
			
		||||
        setupOllamaIpcHandlers();
 | 
			
		||||
        setupWhisperIpcHandlers();
 | 
			
		||||
        setupWebDataHandlers();
 | 
			
		||||
 | 
			
		||||
        // Initialize Ollama models in database
 | 
			
		||||
        await ollamaModelRepository.initializeDefaultModels();
 | 
			
		||||
@ -318,301 +312,6 @@ app.on('activate', () => {
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function setupWhisperIpcHandlers() {
 | 
			
		||||
    const whisperService = require('./features/common/services/whisperService');
 | 
			
		||||
    
 | 
			
		||||
    // Forward download progress events to renderer
 | 
			
		||||
    whisperService.on('downloadProgress', (data) => {
 | 
			
		||||
        const windows = BrowserWindow.getAllWindows();
 | 
			
		||||
        windows.forEach(window => {
 | 
			
		||||
            window.webContents.send('whisper:download-progress', data);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // IPC handlers for Whisper operations
 | 
			
		||||
    ipcMain.handle('whisper:download-model', async (event, modelId) => {
 | 
			
		||||
        try {
 | 
			
		||||
            console.log(`[Whisper IPC] Starting download for model: ${modelId}`);
 | 
			
		||||
            
 | 
			
		||||
            // Ensure WhisperService is initialized first
 | 
			
		||||
            if (!whisperService.isInitialized) {
 | 
			
		||||
                console.log('[Whisper IPC] Initializing WhisperService...');
 | 
			
		||||
                await whisperService.initialize();
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // Set up progress listener
 | 
			
		||||
            const progressHandler = (data) => {
 | 
			
		||||
                if (data.modelId === modelId) {
 | 
			
		||||
                    event.sender.send('whisper:download-progress', data);
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
            
 | 
			
		||||
            whisperService.on('downloadProgress', progressHandler);
 | 
			
		||||
            
 | 
			
		||||
            try {
 | 
			
		||||
                await whisperService.ensureModelAvailable(modelId);
 | 
			
		||||
                console.log(`[Whisper IPC] Model ${modelId} download completed successfully`);
 | 
			
		||||
            } finally {
 | 
			
		||||
                // Cleanup listener
 | 
			
		||||
                whisperService.removeListener('downloadProgress', progressHandler);
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            return { success: true };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error(`[Whisper IPC] Failed to download model ${modelId}:`, error);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    ipcMain.handle('whisper:get-installed-models', async () => {
 | 
			
		||||
        try {
 | 
			
		||||
            // Ensure WhisperService is initialized first
 | 
			
		||||
            if (!whisperService.isInitialized) {
 | 
			
		||||
                console.log('[Whisper IPC] Initializing WhisperService for model list...');
 | 
			
		||||
                await whisperService.initialize();
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            const models = await whisperService.getInstalledModels();
 | 
			
		||||
            return { success: true, models };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[Whisper IPC] Failed to get installed models:', error);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setupGeneralIpcHandlers() {
 | 
			
		||||
    const userRepository = require('./features/common/repositories/user');
 | 
			
		||||
    const presetRepository = require('./features/common/repositories/preset');
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('get-user-presets', () => {
 | 
			
		||||
        // The adapter injects the UID.
 | 
			
		||||
        return presetRepository.getPresets();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('get-preset-templates', () => {
 | 
			
		||||
        return presetRepository.getPresetTemplates();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('start-firebase-auth', async () => {
 | 
			
		||||
        try {
 | 
			
		||||
            const authUrl = `http://localhost:${WEB_PORT}/login?mode=electron`;
 | 
			
		||||
            console.log(`[Auth] Opening Firebase auth URL in browser: ${authUrl}`);
 | 
			
		||||
            await shell.openExternal(authUrl);
 | 
			
		||||
            return { success: true };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[Auth] Failed to open Firebase auth URL:', error);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('get-web-url', () => {
 | 
			
		||||
        return process.env.pickleglass_WEB_URL || 'http://localhost:3000';
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('get-current-user', () => {
 | 
			
		||||
        return authService.getCurrentUser();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // --- Web UI Data Handlers (New) ---
 | 
			
		||||
    setupWebDataHandlers();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setupOllamaIpcHandlers() {
 | 
			
		||||
    // Ollama status and installation
 | 
			
		||||
    ipcMain.handle('ollama:get-status', async () => {
 | 
			
		||||
        try {
 | 
			
		||||
            const installed = await ollamaService.isInstalled();
 | 
			
		||||
            const running = installed ? await ollamaService.isServiceRunning() : false;
 | 
			
		||||
            const models = await ollamaService.getAllModelsWithStatus();
 | 
			
		||||
            
 | 
			
		||||
            return { 
 | 
			
		||||
                installed, 
 | 
			
		||||
                running, 
 | 
			
		||||
                models,
 | 
			
		||||
                success: true 
 | 
			
		||||
            };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[Ollama IPC] Failed to get status:', error);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('ollama:install', async (event) => {
 | 
			
		||||
        try {
 | 
			
		||||
            const onProgress = (data) => {
 | 
			
		||||
                event.sender.send('ollama:install-progress', data);
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            await ollamaService.autoInstall(onProgress);
 | 
			
		||||
            
 | 
			
		||||
            if (!await ollamaService.isServiceRunning()) {
 | 
			
		||||
                onProgress({ stage: 'starting', message: 'Starting Ollama service...', progress: 0 });
 | 
			
		||||
                await ollamaService.startService();
 | 
			
		||||
                onProgress({ stage: 'starting', message: 'Ollama service started.', progress: 100 });
 | 
			
		||||
            }
 | 
			
		||||
            event.sender.send('ollama:install-complete', { success: true });
 | 
			
		||||
            return { success: true };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[Ollama IPC] Failed to install:', error);
 | 
			
		||||
            event.sender.send('ollama:install-complete', { success: false, error: error.message });
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('ollama:start-service', async (event) => {
 | 
			
		||||
        try {
 | 
			
		||||
            if (!await ollamaService.isServiceRunning()) {
 | 
			
		||||
                console.log('[Ollama IPC] Starting Ollama service...');
 | 
			
		||||
                await ollamaService.startService();
 | 
			
		||||
            }
 | 
			
		||||
            event.sender.send('ollama:install-complete', { success: true });
 | 
			
		||||
            return { success: true };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[Ollama IPC] Failed to start service:', error);
 | 
			
		||||
            event.sender.send('ollama:install-complete', { success: false, error: error.message });
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Ensure Ollama is ready (starts service if installed but not running)
 | 
			
		||||
    ipcMain.handle('ollama:ensure-ready', async () => {
 | 
			
		||||
        try {
 | 
			
		||||
            if (await ollamaService.isInstalled() && !await ollamaService.isServiceRunning()) {
 | 
			
		||||
                console.log('[Ollama IPC] Ollama installed but not running, starting service...');
 | 
			
		||||
                await ollamaService.startService();
 | 
			
		||||
            }
 | 
			
		||||
            return { success: true };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[Ollama IPC] Failed to ensure ready:', error);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Get all models with their status
 | 
			
		||||
    ipcMain.handle('ollama:get-models', async () => {
 | 
			
		||||
        try {
 | 
			
		||||
            const models = await ollamaService.getAllModelsWithStatus();
 | 
			
		||||
            return { success: true, models };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[Ollama IPC] Failed to get models:', error);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Get model suggestions for autocomplete
 | 
			
		||||
    ipcMain.handle('ollama:get-model-suggestions', async () => {
 | 
			
		||||
        try {
 | 
			
		||||
            const suggestions = await ollamaService.getModelSuggestions();
 | 
			
		||||
            return { success: true, suggestions };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[Ollama IPC] Failed to get model suggestions:', error);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Pull/install a specific model
 | 
			
		||||
    ipcMain.handle('ollama:pull-model', async (event, modelName) => {
 | 
			
		||||
        try {
 | 
			
		||||
            console.log(`[Ollama IPC] Starting model pull: ${modelName}`);
 | 
			
		||||
            
 | 
			
		||||
            // Update DB status to installing
 | 
			
		||||
            await ollamaModelRepository.updateInstallStatus(modelName, false, true);
 | 
			
		||||
            
 | 
			
		||||
            // Set up progress listener for real-time updates
 | 
			
		||||
            const progressHandler = (data) => {
 | 
			
		||||
                if (data.model === modelName) {
 | 
			
		||||
                    event.sender.send('ollama:pull-progress', data);
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
            
 | 
			
		||||
            const completeHandler = (data) => {
 | 
			
		||||
                if (data.model === modelName) {
 | 
			
		||||
                    console.log(`[Ollama IPC] Model ${modelName} pull completed`);
 | 
			
		||||
                    // Clean up listeners
 | 
			
		||||
                    ollamaService.removeListener('pull-progress', progressHandler);
 | 
			
		||||
                    ollamaService.removeListener('pull-complete', completeHandler);
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
            
 | 
			
		||||
            ollamaService.on('pull-progress', progressHandler);
 | 
			
		||||
            ollamaService.on('pull-complete', completeHandler);
 | 
			
		||||
            
 | 
			
		||||
            // Pull the model using REST API
 | 
			
		||||
            await ollamaService.pullModel(modelName);
 | 
			
		||||
            
 | 
			
		||||
            // Update DB status to installed
 | 
			
		||||
            await ollamaModelRepository.updateInstallStatus(modelName, true, false);
 | 
			
		||||
            
 | 
			
		||||
            console.log(`[Ollama IPC] Model ${modelName} pull successful`);
 | 
			
		||||
            return { success: true };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[Ollama IPC] Failed to pull model:', error);
 | 
			
		||||
            // Reset status on error
 | 
			
		||||
            await ollamaModelRepository.updateInstallStatus(modelName, false, false);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Check if a specific model is installed
 | 
			
		||||
    ipcMain.handle('ollama:is-model-installed', async (event, modelName) => {
 | 
			
		||||
        try {
 | 
			
		||||
            const installed = await ollamaService.isModelInstalled(modelName);
 | 
			
		||||
            return { success: true, installed };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[Ollama IPC] Failed to check model installation:', error);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Warm up a specific model
 | 
			
		||||
    ipcMain.handle('ollama:warm-up-model', async (event, modelName) => {
 | 
			
		||||
        try {
 | 
			
		||||
            const success = await ollamaService.warmUpModel(modelName);
 | 
			
		||||
            return { success };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[Ollama IPC] Failed to warm up model:', error);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Auto warm-up currently selected model
 | 
			
		||||
    ipcMain.handle('ollama:auto-warm-up', async () => {
 | 
			
		||||
        try {
 | 
			
		||||
            const success = await ollamaService.autoWarmUpSelectedModel();
 | 
			
		||||
            return { success };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[Ollama IPC] Failed to auto warm-up:', error);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Get warm-up status for debugging
 | 
			
		||||
    ipcMain.handle('ollama:get-warm-up-status', async () => {
 | 
			
		||||
        try {
 | 
			
		||||
            const status = ollamaService.getWarmUpStatus();
 | 
			
		||||
            return { success: true, status };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[Ollama IPC] Failed to get warm-up status:', error);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Shutdown Ollama service manually
 | 
			
		||||
    ipcMain.handle('ollama:shutdown', async (event, force = false) => {
 | 
			
		||||
        try {
 | 
			
		||||
            console.log(`[Ollama IPC] Manual shutdown requested (force: ${force})`);
 | 
			
		||||
            const success = await ollamaService.shutdown(force);
 | 
			
		||||
            return { success };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[Ollama IPC] Failed to shutdown Ollama:', error);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    console.log('[Ollama IPC] Handlers registered');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setupWebDataHandlers() {
 | 
			
		||||
    const sessionRepository = require('./features/common/repositories/session');
 | 
			
		||||
    const sttRepository = require('./features/listen/stt/repositories');
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										326
									
								
								src/preload.js
									
									
									
									
									
								
							
							
						
						
									
										326
									
								
								src/preload.js
									
									
									
									
									
								
							@ -2,72 +2,231 @@
 | 
			
		||||
const { contextBridge, ipcRenderer } = require('electron');
 | 
			
		||||
 | 
			
		||||
contextBridge.exposeInMainWorld('api', {
 | 
			
		||||
  feature: {
 | 
			
		||||
    // 기존 ask 관련 유지
 | 
			
		||||
    submitAsk: (query) => ipcRenderer.invoke('feature:ask', query),
 | 
			
		||||
    onAskProgress: (callback) => ipcRenderer.on('feature:ask:progress', (e, p) => callback(p)),
 | 
			
		||||
 | 
			
		||||
    settings: {
 | 
			
		||||
      // invoke methods
 | 
			
		||||
      getCurrentUser: () => ipcRenderer.invoke('get-current-user'),
 | 
			
		||||
      getProviderConfig: () => ipcRenderer.invoke('model:get-provider-config'),
 | 
			
		||||
      getAllKeys: () => ipcRenderer.invoke('model:get-all-keys'),
 | 
			
		||||
      getAvailableModels: (type) => ipcRenderer.invoke('model:get-available-models', type),
 | 
			
		||||
      getSelectedModels: () => ipcRenderer.invoke('model:get-selected-models'),
 | 
			
		||||
      getPresets: () => ipcRenderer.invoke('settings:getPresets'),
 | 
			
		||||
      getContentProtectionStatus: () => ipcRenderer.invoke('get-content-protection-status'),
 | 
			
		||||
      getCurrentShortcuts: () => ipcRenderer.invoke('get-current-shortcuts'),
 | 
			
		||||
      getOllamaStatus: () => ipcRenderer.invoke('ollama:get-status'),
 | 
			
		||||
      getWhisperInstalledModels: () => ipcRenderer.invoke('whisper:get-installed-models'),
 | 
			
		||||
      ollamaEnsureReady: () => ipcRenderer.invoke('ollama:ensure-ready'),
 | 
			
		||||
      validateKey: (data) => ipcRenderer.invoke('model:validate-key', data),
 | 
			
		||||
      getAutoUpdate: () => ipcRenderer.invoke('settings:get-auto-update'),
 | 
			
		||||
      setAutoUpdate: (isEnabled) => ipcRenderer.invoke('settings:set-auto-update', isEnabled),
 | 
			
		||||
      removeApiKey: (provider) => ipcRenderer.invoke('model:remove-api-key', provider),
 | 
			
		||||
      setSelectedModel: (data) => ipcRenderer.invoke('model:set-selected-model', data),
 | 
			
		||||
      downloadWhisperModel: (modelId) => ipcRenderer.invoke('whisper:download-model', modelId),
 | 
			
		||||
      openLoginPage: () => ipcRenderer.invoke('open-login-page'),
 | 
			
		||||
      toggleContentProtection: () => ipcRenderer.invoke('toggle-content-protection'),
 | 
			
		||||
      openShortcutEditor: () => ipcRenderer.invoke('open-shortcut-editor'),
 | 
			
		||||
  // Platform information for renderer processes
 | 
			
		||||
  platform: {
 | 
			
		||||
    isLinux: process.platform === 'linux',
 | 
			
		||||
    isMacOS: process.platform === 'darwin',
 | 
			
		||||
    isWindows: process.platform === 'win32',
 | 
			
		||||
    platform: process.platform
 | 
			
		||||
  },
 | 
			
		||||
  
 | 
			
		||||
  // Common utilities used across multiple components
 | 
			
		||||
  common: {
 | 
			
		||||
    // User & Auth
 | 
			
		||||
    getCurrentUser: () => ipcRenderer.invoke('get-current-user'),
 | 
			
		||||
    startFirebaseAuth: () => ipcRenderer.invoke('start-firebase-auth'),
 | 
			
		||||
    firebaseLogout: () => ipcRenderer.invoke('firebase-logout'),
 | 
			
		||||
    
 | 
			
		||||
    // App Control
 | 
			
		||||
      quitApplication: () => ipcRenderer.invoke('quit-application'),
 | 
			
		||||
      firebaseLogout: () => ipcRenderer.invoke('firebase-logout'),
 | 
			
		||||
      ollamaShutdown: (graceful) => ipcRenderer.invoke('ollama:shutdown', graceful),
 | 
			
		||||
      startFirebaseAuth: () => ipcRenderer.invoke('start-firebase-auth'),
 | 
			
		||||
 | 
			
		||||
      // on methods (listeners)
 | 
			
		||||
    // User state listener (used by multiple components)
 | 
			
		||||
      onUserStateChanged: (callback) => ipcRenderer.on('user-state-changed', callback),
 | 
			
		||||
      removeOnUserStateChanged: (callback) => ipcRenderer.removeListener('user-state-changed', callback),
 | 
			
		||||
      onSettingsUpdated: (callback) => ipcRenderer.on('settings-updated', callback),
 | 
			
		||||
      removeOnSettingsUpdated: (callback) => ipcRenderer.removeListener('settings-updated', callback),
 | 
			
		||||
      onPresetsUpdated: (callback) => ipcRenderer.on('presets-updated', callback),
 | 
			
		||||
      removeOnPresetsUpdated: (callback) => ipcRenderer.removeListener('presets-updated', callback),
 | 
			
		||||
      onShortcutsUpdated: (callback) => ipcRenderer.on('shortcuts-updated', callback),
 | 
			
		||||
      removeOnShortcutsUpdated: (callback) => ipcRenderer.removeListener('shortcuts-updated', callback),
 | 
			
		||||
      onWhisperDownloadProgress: (callback) => ipcRenderer.on('whisper:download-progress', callback),
 | 
			
		||||
      removeOnWhisperDownloadProgress: (callback) => ipcRenderer.removeListener('whisper:download-progress', callback),
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
      // send methods
 | 
			
		||||
      cancelHideSettingsWindow: () => ipcRenderer.send('cancel-hide-settings-window'),
 | 
			
		||||
      hideSettingsWindow: () => ipcRenderer.send('hide-settings-window')
 | 
			
		||||
  // UI Component specific namespaces
 | 
			
		||||
  // src/ui/app/ApiKeyHeader.js
 | 
			
		||||
  apiKeyHeader: {
 | 
			
		||||
    // Model & Provider Management
 | 
			
		||||
    getProviderConfig: () => ipcRenderer.invoke('model:get-provider-config'),
 | 
			
		||||
    getOllamaStatus: () => ipcRenderer.invoke('ollama:get-status'),
 | 
			
		||||
    getModelSuggestions: () => ipcRenderer.invoke('ollama:get-model-suggestions'),
 | 
			
		||||
    ensureOllamaReady: () => ipcRenderer.invoke('ollama:ensure-ready'),
 | 
			
		||||
    installOllama: () => ipcRenderer.invoke('ollama:install'),
 | 
			
		||||
    startOllamaService: () => ipcRenderer.invoke('ollama:start-service'),
 | 
			
		||||
    pullOllamaModel: (modelName) => ipcRenderer.invoke('ollama:pull-model', modelName),
 | 
			
		||||
    downloadWhisperModel: (modelId) => ipcRenderer.invoke('whisper:download-model', modelId),
 | 
			
		||||
    validateKey: (data) => ipcRenderer.invoke('model:validate-key', data),
 | 
			
		||||
    setSelectedModel: (data) => ipcRenderer.invoke('model:set-selected-model', data),
 | 
			
		||||
    areProvidersConfigured: () => ipcRenderer.invoke('model:are-providers-configured'),
 | 
			
		||||
    
 | 
			
		||||
    // Window Management
 | 
			
		||||
    getHeaderPosition: () => ipcRenderer.invoke('get-header-position'),
 | 
			
		||||
    moveHeaderTo: (x, y) => ipcRenderer.invoke('move-header-to', x, y),
 | 
			
		||||
    
 | 
			
		||||
    // Listeners
 | 
			
		||||
    onOllamaInstallProgress: (callback) => ipcRenderer.on('ollama:install-progress', callback),
 | 
			
		||||
    removeOnOllamaInstallProgress: (callback) => ipcRenderer.removeListener('ollama:install-progress', callback),
 | 
			
		||||
    onceOllamaInstallComplete: (callback) => ipcRenderer.once('ollama:install-complete', callback),
 | 
			
		||||
    removeOnceOllamaInstallComplete: (callback) => ipcRenderer.removeListener('ollama:install-complete', callback),
 | 
			
		||||
    onOllamaPullProgress: (callback) => ipcRenderer.on('ollama:pull-progress', callback),
 | 
			
		||||
    removeOnOllamaPullProgress: (callback) => ipcRenderer.removeListener('ollama:pull-progress', callback),
 | 
			
		||||
    onWhisperDownloadProgress: (callback) => ipcRenderer.on('whisper:download-progress', callback),
 | 
			
		||||
    removeOnWhisperDownloadProgress: (callback) => ipcRenderer.removeListener('whisper:download-progress', callback),
 | 
			
		||||
 | 
			
		||||
    // Remove all listeners (for cleanup)
 | 
			
		||||
    removeAllListeners: () => {
 | 
			
		||||
      ipcRenderer.removeAllListeners('whisper:download-progress');
 | 
			
		||||
      ipcRenderer.removeAllListeners('ollama:install-progress');
 | 
			
		||||
      ipcRenderer.removeAllListeners('ollama:pull-progress');
 | 
			
		||||
      ipcRenderer.removeAllListeners('ollama:install-complete');
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  // 기존 window 유지
 | 
			
		||||
  window: {
 | 
			
		||||
    // 기존
 | 
			
		||||
    hide: () => ipcRenderer.send('window:hide'),
 | 
			
		||||
    onFocusChange: (callback) => ipcRenderer.on('window:focus-change', (e, f) => callback(f)),
 | 
			
		||||
 | 
			
		||||
    // 추가
 | 
			
		||||
  // src/ui/app/HeaderController.js
 | 
			
		||||
  headerController: {
 | 
			
		||||
    // State Management
 | 
			
		||||
    sendHeaderStateChanged: (state) => ipcRenderer.send('header-state-changed', state),
 | 
			
		||||
    
 | 
			
		||||
    // Window Management
 | 
			
		||||
    resizeHeaderWindow: (dimensions) => ipcRenderer.invoke('resize-header-window', dimensions),
 | 
			
		||||
    
 | 
			
		||||
    // Permissions
 | 
			
		||||
    checkSystemPermissions: () => ipcRenderer.invoke('check-system-permissions'),
 | 
			
		||||
    checkPermissionsCompleted: () => ipcRenderer.invoke('check-permissions-completed'),
 | 
			
		||||
    
 | 
			
		||||
    // Listeners
 | 
			
		||||
    onUserStateChanged: (callback) => ipcRenderer.on('user-state-changed', callback),
 | 
			
		||||
    removeOnUserStateChanged: (callback) => ipcRenderer.removeListener('user-state-changed', callback),
 | 
			
		||||
    onAuthFailed: (callback) => ipcRenderer.on('auth-failed', callback),
 | 
			
		||||
    removeOnAuthFailed: (callback) => ipcRenderer.removeListener('auth-failed', callback),
 | 
			
		||||
    onForceShowApiKeyHeader: (callback) => ipcRenderer.on('force-show-apikey-header', callback),
 | 
			
		||||
    removeOnForceShowApiKeyHeader: (callback) => ipcRenderer.removeListener('force-show-apikey-header', callback)
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  // src/ui/app/MainHeader.js
 | 
			
		||||
  mainHeader: {
 | 
			
		||||
    // Window Management
 | 
			
		||||
    getHeaderPosition: () => ipcRenderer.invoke('get-header-position'),
 | 
			
		||||
    moveHeaderTo: (x, y) => ipcRenderer.invoke('move-header-to', x, y),
 | 
			
		||||
    sendHeaderAnimationFinished: (state) => ipcRenderer.send('header-animation-finished', state),
 | 
			
		||||
 | 
			
		||||
    // Settings Window Management
 | 
			
		||||
    cancelHideSettingsWindow: () => ipcRenderer.send('cancel-hide-settings-window'),
 | 
			
		||||
    showSettingsWindow: (bounds) => ipcRenderer.send('show-settings-window', bounds),
 | 
			
		||||
    hideSettingsWindow: () => ipcRenderer.send('hide-settings-window'),
 | 
			
		||||
    cancelHideSettingsWindow: () => ipcRenderer.send('cancel-hide-settings-window'),
 | 
			
		||||
    moveWindowStep: (direction) => ipcRenderer.invoke('move-window-step', direction),
 | 
			
		||||
    
 | 
			
		||||
    // Generic invoke (for dynamic channel names)
 | 
			
		||||
    invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
 | 
			
		||||
    
 | 
			
		||||
    // Listeners
 | 
			
		||||
    onSessionStateText: (callback) => ipcRenderer.on('session-state-text', callback),
 | 
			
		||||
    removeOnSessionStateText: (callback) => ipcRenderer.removeListener('session-state-text', callback),
 | 
			
		||||
    onShortcutsUpdated: (callback) => ipcRenderer.on('shortcuts-updated', callback),
 | 
			
		||||
    removeOnShortcutsUpdated: (callback) => ipcRenderer.removeListener('shortcuts-updated', callback)
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  // src/ui/app/PermissionHeader.js
 | 
			
		||||
  permissionHeader: {
 | 
			
		||||
    // Permission Management
 | 
			
		||||
    checkSystemPermissions: () => ipcRenderer.invoke('check-system-permissions'),
 | 
			
		||||
    requestMicrophonePermission: () => ipcRenderer.invoke('request-microphone-permission'),
 | 
			
		||||
    openSystemPreferences: (preference) => ipcRenderer.invoke('open-system-preferences', preference),
 | 
			
		||||
    markPermissionsCompleted: () => ipcRenderer.invoke('mark-permissions-completed')
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  // src/ui/app/PickleGlassApp.js
 | 
			
		||||
  pickleGlassApp: {
 | 
			
		||||
    // Listeners
 | 
			
		||||
    onClickThroughToggled: (callback) => ipcRenderer.on('click-through-toggled', callback),
 | 
			
		||||
    removeOnClickThroughToggled: (callback) => ipcRenderer.removeListener('click-through-toggled', callback),
 | 
			
		||||
    removeAllClickThroughListeners: () => ipcRenderer.removeAllListeners('click-through-toggled')
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  // src/ui/ask/AskView.js
 | 
			
		||||
  askView: {
 | 
			
		||||
    // Window Management
 | 
			
		||||
    closeAskWindow: () => ipcRenderer.invoke('ask:closeAskWindow'),
 | 
			
		||||
    adjustWindowHeight: (height) => ipcRenderer.invoke('adjust-window-height', height),
 | 
			
		||||
    
 | 
			
		||||
    // Message Handling
 | 
			
		||||
    sendMessage: (text) => ipcRenderer.invoke('ask:sendMessage', text),
 | 
			
		||||
    
 | 
			
		||||
    // Listeners
 | 
			
		||||
    onSendQuestionToRenderer: (callback) => ipcRenderer.on('ask:sendQuestionToRenderer', callback),
 | 
			
		||||
    removeOnSendQuestionToRenderer: (callback) => ipcRenderer.removeListener('ask:sendQuestionToRenderer', callback),
 | 
			
		||||
    onHideTextInput: (callback) => ipcRenderer.on('hide-text-input', callback),
 | 
			
		||||
    removeOnHideTextInput: (callback) => ipcRenderer.removeListener('hide-text-input', callback),
 | 
			
		||||
    onShowTextInput: (callback) => ipcRenderer.on('ask:showTextInput', callback),
 | 
			
		||||
    removeOnShowTextInput: (callback) => ipcRenderer.removeListener('ask:showTextInput', callback),
 | 
			
		||||
    onResponseChunk: (callback) => ipcRenderer.on('ask-response-chunk', callback),
 | 
			
		||||
    removeOnResponseChunk: (callback) => ipcRenderer.removeListener('ask-response-chunk', callback),
 | 
			
		||||
    onResponseStreamEnd: (callback) => ipcRenderer.on('ask-response-stream-end', callback),
 | 
			
		||||
    removeOnResponseStreamEnd: (callback) => ipcRenderer.removeListener('ask-response-stream-end', callback),
 | 
			
		||||
    onScrollResponseUp: (callback) => ipcRenderer.on('scroll-response-up', callback),
 | 
			
		||||
    removeOnScrollResponseUp: (callback) => ipcRenderer.removeListener('scroll-response-up', callback),
 | 
			
		||||
    onScrollResponseDown: (callback) => ipcRenderer.on('scroll-response-down', callback),
 | 
			
		||||
    removeOnScrollResponseDown: (callback) => ipcRenderer.removeListener('scroll-response-down', callback)
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  // src/ui/listen/ListenView.js
 | 
			
		||||
  listenView: {
 | 
			
		||||
    // Window Management
 | 
			
		||||
    adjustWindowHeight: (height) => ipcRenderer.invoke('adjust-window-height', height),
 | 
			
		||||
    
 | 
			
		||||
    // Listeners
 | 
			
		||||
    onSessionStateChanged: (callback) => ipcRenderer.on('session-state-changed', callback),
 | 
			
		||||
    removeOnSessionStateChanged: (callback) => ipcRenderer.removeListener('session-state-changed', callback)
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  // src/ui/listen/stt/SttView.js
 | 
			
		||||
  sttView: {
 | 
			
		||||
    // Listeners
 | 
			
		||||
    onSttUpdate: (callback) => ipcRenderer.on('stt-update', callback),
 | 
			
		||||
    removeOnSttUpdate: (callback) => ipcRenderer.removeListener('stt-update', callback)
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  // src/ui/listen/summary/SummaryView.js
 | 
			
		||||
  summaryView: {
 | 
			
		||||
    // Message Handling
 | 
			
		||||
    sendQuestionToMain: (text) => ipcRenderer.invoke('ask:sendQuestionToMain', text),
 | 
			
		||||
    
 | 
			
		||||
    // Listeners
 | 
			
		||||
    onSummaryUpdate: (callback) => ipcRenderer.on('summary-update', callback),
 | 
			
		||||
    removeOnSummaryUpdate: (callback) => ipcRenderer.removeListener('summary-update', callback),
 | 
			
		||||
    removeAllSummaryUpdateListeners: () => ipcRenderer.removeAllListeners('summary-update')
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  // src/ui/settings/SettingsView.js
 | 
			
		||||
  settingsView: {
 | 
			
		||||
    // User & Auth
 | 
			
		||||
    getCurrentUser: () => ipcRenderer.invoke('get-current-user'),
 | 
			
		||||
    openLoginPage: () => ipcRenderer.invoke('open-login-page'),
 | 
			
		||||
    firebaseLogout: () => ipcRenderer.invoke('firebase-logout'),
 | 
			
		||||
    ollamaShutdown: (graceful) => ipcRenderer.invoke('ollama:shutdown', graceful),
 | 
			
		||||
    startFirebaseAuth: () => ipcRenderer.invoke('start-firebase-auth'),
 | 
			
		||||
 | 
			
		||||
    // on methods (listeners)
 | 
			
		||||
    // Model & Provider Management
 | 
			
		||||
    getModelSettings: () => ipcRenderer.invoke('settings:get-model-settings'), // Facade call
 | 
			
		||||
    getProviderConfig: () => ipcRenderer.invoke('model:get-provider-config'),
 | 
			
		||||
    getAllKeys: () => ipcRenderer.invoke('model:get-all-keys'),
 | 
			
		||||
    getAvailableModels: (type) => ipcRenderer.invoke('model:get-available-models', type),
 | 
			
		||||
    getSelectedModels: () => ipcRenderer.invoke('model:get-selected-models'),
 | 
			
		||||
    validateKey: (data) => ipcRenderer.invoke('model:validate-key', data),
 | 
			
		||||
    saveApiKey: (key) => ipcRenderer.invoke('model:save-api-key', key),
 | 
			
		||||
    removeApiKey: (provider) => ipcRenderer.invoke('model:remove-api-key', provider),
 | 
			
		||||
    setSelectedModel: (data) => ipcRenderer.invoke('model:set-selected-model', data),
 | 
			
		||||
    
 | 
			
		||||
    // Ollama Management
 | 
			
		||||
    getOllamaStatus: () => ipcRenderer.invoke('ollama:get-status'),
 | 
			
		||||
    ensureOllamaReady: () => ipcRenderer.invoke('ollama:ensure-ready'),
 | 
			
		||||
    shutdownOllama: (graceful) => ipcRenderer.invoke('ollama:shutdown', graceful),
 | 
			
		||||
    
 | 
			
		||||
    // Whisper Management
 | 
			
		||||
    getWhisperInstalledModels: () => ipcRenderer.invoke('whisper:get-installed-models'),
 | 
			
		||||
    downloadWhisperModel: (modelId) => ipcRenderer.invoke('whisper:download-model', modelId),
 | 
			
		||||
    
 | 
			
		||||
    // Settings Management
 | 
			
		||||
    getPresets: () => ipcRenderer.invoke('settings:getPresets'),
 | 
			
		||||
    getAutoUpdate: () => ipcRenderer.invoke('settings:get-auto-update'),
 | 
			
		||||
    setAutoUpdate: (isEnabled) => ipcRenderer.invoke('settings:set-auto-update', isEnabled),
 | 
			
		||||
    getContentProtectionStatus: () => ipcRenderer.invoke('get-content-protection-status'),
 | 
			
		||||
    toggleContentProtection: () => ipcRenderer.invoke('toggle-content-protection'),
 | 
			
		||||
    getCurrentShortcuts: () => ipcRenderer.invoke('get-current-shortcuts'),
 | 
			
		||||
    openShortcutEditor: () => ipcRenderer.invoke('open-shortcut-editor'),
 | 
			
		||||
    
 | 
			
		||||
    // Window Management
 | 
			
		||||
    moveWindowStep: (direction) => ipcRenderer.invoke('move-window-step', direction),
 | 
			
		||||
    cancelHideSettingsWindow: () => ipcRenderer.send('cancel-hide-settings-window'),
 | 
			
		||||
    hideSettingsWindow: () => ipcRenderer.send('hide-settings-window'),
 | 
			
		||||
    
 | 
			
		||||
    // App Control
 | 
			
		||||
    quitApplication: () => ipcRenderer.invoke('quit-application'),
 | 
			
		||||
    
 | 
			
		||||
    // Progress Tracking
 | 
			
		||||
    pullOllamaModel: (modelName) => ipcRenderer.invoke('ollama:pull-model', modelName),
 | 
			
		||||
    
 | 
			
		||||
    // Listeners
 | 
			
		||||
    onUserStateChanged: (callback) => ipcRenderer.on('user-state-changed', callback),
 | 
			
		||||
    removeOnUserStateChanged: (callback) => ipcRenderer.removeListener('user-state-changed', callback),
 | 
			
		||||
    onSettingsUpdated: (callback) => ipcRenderer.on('settings-updated', callback),
 | 
			
		||||
@ -78,9 +237,66 @@ contextBridge.exposeInMainWorld('api', {
 | 
			
		||||
    removeOnShortcutsUpdated: (callback) => ipcRenderer.removeListener('shortcuts-updated', callback),
 | 
			
		||||
    onWhisperDownloadProgress: (callback) => ipcRenderer.on('whisper:download-progress', callback),
 | 
			
		||||
    removeOnWhisperDownloadProgress: (callback) => ipcRenderer.removeListener('whisper:download-progress', callback),
 | 
			
		||||
    onOllamaPullProgress: (callback) => ipcRenderer.on('ollama:pull-progress', callback),
 | 
			
		||||
    removeOnOllamaPullProgress: (callback) => ipcRenderer.removeListener('ollama:pull-progress', callback)
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
    // send methods
 | 
			
		||||
    cancelHideSettingsWindow: () => ipcRenderer.send('cancel-hide-settings-window'),
 | 
			
		||||
    hideSettingsWindow: () => ipcRenderer.send('hide-settings-window')
 | 
			
		||||
  // src/ui/settings/ShortCutSettingsView.js
 | 
			
		||||
  shortcutSettingsView: {
 | 
			
		||||
    // Shortcut Management
 | 
			
		||||
    saveShortcuts: (shortcuts) => ipcRenderer.invoke('save-shortcuts', shortcuts),
 | 
			
		||||
    getDefaultShortcuts: () => ipcRenderer.invoke('get-default-shortcuts'),
 | 
			
		||||
    closeShortcutEditor: () => ipcRenderer.send('close-shortcut-editor'),
 | 
			
		||||
    
 | 
			
		||||
    // Listeners
 | 
			
		||||
    onLoadShortcuts: (callback) => ipcRenderer.on('load-shortcuts', callback),
 | 
			
		||||
    removeOnLoadShortcuts: (callback) => ipcRenderer.removeListener('load-shortcuts', callback)
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  // src/ui/app/content.html inline scripts
 | 
			
		||||
  content: {
 | 
			
		||||
    // Animation Management
 | 
			
		||||
    sendAnimationFinished: () => ipcRenderer.send('animation-finished'),
 | 
			
		||||
    
 | 
			
		||||
    // Listeners
 | 
			
		||||
    onWindowShowAnimation: (callback) => ipcRenderer.on('window-show-animation', callback),
 | 
			
		||||
    removeOnWindowShowAnimation: (callback) => ipcRenderer.removeListener('window-show-animation', callback),
 | 
			
		||||
    onWindowHideAnimation: (callback) => ipcRenderer.on('window-hide-animation', callback),
 | 
			
		||||
    removeOnWindowHideAnimation: (callback) => ipcRenderer.removeListener('window-hide-animation', callback),
 | 
			
		||||
    onSettingsWindowHideAnimation: (callback) => ipcRenderer.on('settings-window-hide-animation', callback),
 | 
			
		||||
    removeOnSettingsWindowHideAnimation: (callback) => ipcRenderer.removeListener('settings-window-hide-animation', callback),
 | 
			
		||||
    onListenWindowMoveToCenter: (callback) => ipcRenderer.on('listen-window-move-to-center', callback),
 | 
			
		||||
    removeOnListenWindowMoveToCenter: (callback) => ipcRenderer.removeListener('listen-window-move-to-center', callback),
 | 
			
		||||
    onListenWindowMoveToLeft: (callback) => ipcRenderer.on('listen-window-move-to-left', callback),
 | 
			
		||||
    removeOnListenWindowMoveToLeft: (callback) => ipcRenderer.removeListener('listen-window-move-to-left', callback)
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  // src/ui/listen/audioCore/listenCapture.js
 | 
			
		||||
  listenCapture: {
 | 
			
		||||
    // Audio Management
 | 
			
		||||
    sendAudioContent: (data) => ipcRenderer.invoke('send-audio-content', data),
 | 
			
		||||
    sendSystemAudioContent: (data) => ipcRenderer.invoke('send-system-audio-content', data),
 | 
			
		||||
    startMacosAudio: () => ipcRenderer.invoke('start-macos-audio'),
 | 
			
		||||
    stopMacosAudio: () => ipcRenderer.invoke('stop-macos-audio'),
 | 
			
		||||
    
 | 
			
		||||
    // Screen Capture
 | 
			
		||||
    captureScreenshot: (options) => ipcRenderer.invoke('capture-screenshot', options),
 | 
			
		||||
    getCurrentScreenshot: () => ipcRenderer.invoke('get-current-screenshot'),
 | 
			
		||||
    startScreenCapture: () => ipcRenderer.invoke('start-screen-capture'),
 | 
			
		||||
    stopScreenCapture: () => ipcRenderer.invoke('stop-screen-capture'),
 | 
			
		||||
    
 | 
			
		||||
    // Session Management
 | 
			
		||||
    isSessionActive: () => ipcRenderer.invoke('is-session-active'),
 | 
			
		||||
    
 | 
			
		||||
    // Listeners
 | 
			
		||||
    onSystemAudioData: (callback) => ipcRenderer.on('system-audio-data', callback),
 | 
			
		||||
    removeOnSystemAudioData: (callback) => ipcRenderer.removeListener('system-audio-data', callback)
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  // src/ui/listen/audioCore/renderer.js
 | 
			
		||||
  renderer: {
 | 
			
		||||
    // Listeners
 | 
			
		||||
    onChangeListenCaptureState: (callback) => ipcRenderer.on('change-listen-capture-state', callback),
 | 
			
		||||
    removeOnChangeListenCaptureState: (callback) => ipcRenderer.removeListener('change-listen-capture-state', callback)
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
@ -370,13 +370,12 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async loadProviderConfig() {
 | 
			
		||||
    if (!window.require) return;
 | 
			
		||||
    const { ipcRenderer } = window.require('electron');
 | 
			
		||||
    if (!window.api) return;
 | 
			
		||||
    
 | 
			
		||||
    try {
 | 
			
		||||
        const [config, ollamaStatus] = await Promise.all([
 | 
			
		||||
            ipcRenderer.invoke('model:get-provider-config'),
 | 
			
		||||
            ipcRenderer.invoke('ollama:get-status')
 | 
			
		||||
            window.api.apiKeyHeader.getProviderConfig(),
 | 
			
		||||
            window.api.apiKeyHeader.getOllamaStatus()
 | 
			
		||||
        ]);
 | 
			
		||||
        
 | 
			
		||||
        const llmProviders = [];
 | 
			
		||||
@ -428,8 +427,7 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
 | 
			
		||||
    e.preventDefault()
 | 
			
		||||
 | 
			
		||||
    const { ipcRenderer } = window.require("electron")
 | 
			
		||||
    const initialPosition = await ipcRenderer.invoke("get-header-position")
 | 
			
		||||
    const initialPosition = await window.api.apiKeyHeader.getHeaderPosition()
 | 
			
		||||
 | 
			
		||||
    this.dragState = {
 | 
			
		||||
      initialMouseX: e.screenX,
 | 
			
		||||
@ -456,8 +454,7 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
    const newWindowX = this.dragState.initialWindowX + (e.screenX - this.dragState.initialMouseX)
 | 
			
		||||
    const newWindowY = this.dragState.initialWindowY + (e.screenY - this.dragState.initialMouseY)
 | 
			
		||||
 | 
			
		||||
    const { ipcRenderer } = window.require("electron")
 | 
			
		||||
    ipcRenderer.invoke("move-header-to", newWindowX, newWindowY)
 | 
			
		||||
    window.api.apiKeyHeader.moveHeaderTo(newWindowX, newWindowY)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleMouseUp(e) {
 | 
			
		||||
@ -652,9 +649,8 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
    try {
 | 
			
		||||
      // Lightweight health check - just ping the service
 | 
			
		||||
      const isHealthy = await this._executeOperation('health_check', async () => {
 | 
			
		||||
        if (!window.require) return false;
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        const result = await ipcRenderer.invoke('ollama:get-status');
 | 
			
		||||
        if (!window.api) return false;
 | 
			
		||||
        const result = await window.api.apiKeyHeader.getOllamaStatus();
 | 
			
		||||
        return result?.success && result?.running;
 | 
			
		||||
      }, { timeout: 5000, priority: 'low' });
 | 
			
		||||
      
 | 
			
		||||
@ -928,14 +924,13 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  async refreshOllamaStatus() {
 | 
			
		||||
    if (!window.require) return;
 | 
			
		||||
    if (!window.api) return;
 | 
			
		||||
    
 | 
			
		||||
    try {
 | 
			
		||||
      this._updateConnectionState('connecting', 'Checking Ollama status');
 | 
			
		||||
      
 | 
			
		||||
      const result = await this._executeOperation('ollama_status', async () => {
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        return await ipcRenderer.invoke('ollama:get-status');
 | 
			
		||||
        return await window.api.apiKeyHeader.getOllamaStatus();
 | 
			
		||||
      });
 | 
			
		||||
      
 | 
			
		||||
      if (result?.success) {
 | 
			
		||||
@ -960,12 +955,11 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  async loadModelSuggestions() {
 | 
			
		||||
    if (!window.require) return;
 | 
			
		||||
    if (!window.api) return;
 | 
			
		||||
    
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await this._executeOperation('model_suggestions', async () => {
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        return await ipcRenderer.invoke('ollama:get-model-suggestions');
 | 
			
		||||
        return await window.api.apiKeyHeader.getModelSuggestions();
 | 
			
		||||
      });
 | 
			
		||||
      
 | 
			
		||||
      if (result?.success) {
 | 
			
		||||
@ -988,14 +982,13 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async ensureOllamaReady() {
 | 
			
		||||
    if (!window.require) return false;
 | 
			
		||||
    if (!window.api) return false;
 | 
			
		||||
    
 | 
			
		||||
    try {
 | 
			
		||||
      this._updateConnectionState('connecting', 'Ensuring Ollama is ready');
 | 
			
		||||
      
 | 
			
		||||
      const result = await this._executeOperation('ollama_ensure_ready', async () => {
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        return await ipcRenderer.invoke('ollama:ensure-ready');
 | 
			
		||||
        return await window.api.apiKeyHeader.ensureOllamaReady();
 | 
			
		||||
      }, { timeout: this.operationTimeout });
 | 
			
		||||
      
 | 
			
		||||
      if (result?.success) {
 | 
			
		||||
@ -1015,8 +1008,7 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async ensureOllamaReadyWithUI() {
 | 
			
		||||
    if (!window.require) return false;
 | 
			
		||||
    const { ipcRenderer } = window.require("electron");
 | 
			
		||||
    if (!window.api) return false;
 | 
			
		||||
 | 
			
		||||
    this.installingModel = "Setting up Ollama";
 | 
			
		||||
    this.installProgress = 0;
 | 
			
		||||
@ -1074,21 +1066,21 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
      operationCompleted = true;
 | 
			
		||||
      clearTimeout(completionTimeout);
 | 
			
		||||
      
 | 
			
		||||
      ipcRenderer.removeListener("ollama:install-progress", progressHandler);
 | 
			
		||||
      window.api.apiKeyHeader.removeOnOllamaInstallProgress(progressHandler);
 | 
			
		||||
      await this._handleOllamaSetupCompletion(result.success, result.error);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    ipcRenderer.once("ollama:install-complete", completionHandler);
 | 
			
		||||
    ipcRenderer.on("ollama:install-progress", progressHandler);
 | 
			
		||||
    window.api.apiKeyHeader.onceOllamaInstallComplete(completionHandler);
 | 
			
		||||
    window.api.apiKeyHeader.onOllamaInstallProgress(progressHandler);
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      let result;
 | 
			
		||||
      if (!this.ollamaStatus.installed) {
 | 
			
		||||
        console.log("[ApiKeyHeader] Ollama not installed. Starting installation.");
 | 
			
		||||
        result = await ipcRenderer.invoke("ollama:install");
 | 
			
		||||
        result = await window.api.apiKeyHeader.installOllama();
 | 
			
		||||
      } else {
 | 
			
		||||
        console.log("[ApiKeyHeader] Ollama installed. Starting service.");
 | 
			
		||||
        result = await ipcRenderer.invoke("ollama:start-service");
 | 
			
		||||
        result = await window.api.apiKeyHeader.startOllamaService();
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      // If IPC call succeeds but no event received, handle completion manually
 | 
			
		||||
@ -1106,8 +1098,8 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
      operationCompleted = true;
 | 
			
		||||
      clearTimeout(completionTimeout);
 | 
			
		||||
      console.error("[ApiKeyHeader] Ollama setup failed:", error);
 | 
			
		||||
      ipcRenderer.removeListener("ollama:install-progress", progressHandler);
 | 
			
		||||
      ipcRenderer.removeListener("ollama:install-complete", completionHandler);
 | 
			
		||||
      window.api.apiKeyHeader.removeOnOllamaInstallProgress(progressHandler);
 | 
			
		||||
      window.api.apiKeyHeader.removeOnceOllamaInstallComplete(completionHandler);
 | 
			
		||||
      await this._handleOllamaSetupCompletion(false, error.message);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@ -1229,7 +1221,6 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
    this.clearMessages();
 | 
			
		||||
    this.requestUpdate();
 | 
			
		||||
    
 | 
			
		||||
    const { ipcRenderer } = window.require('electron');
 | 
			
		||||
    let progressHandler = null;
 | 
			
		||||
    
 | 
			
		||||
    try {
 | 
			
		||||
@ -1249,10 +1240,10 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
      };
 | 
			
		||||
      
 | 
			
		||||
      // Set up progress tracking
 | 
			
		||||
      ipcRenderer.on('ollama:pull-progress', progressHandler);
 | 
			
		||||
      window.api.apiKeyHeader.onOllamaPullProgress(progressHandler);
 | 
			
		||||
      
 | 
			
		||||
      // Execute the model pull with timeout
 | 
			
		||||
      const installPromise = ipcRenderer.invoke('ollama:pull-model', modelName);
 | 
			
		||||
      const installPromise = window.api.apiKeyHeader.pullOllamaModel(modelName);
 | 
			
		||||
      const timeoutPromise = new Promise((_, reject) => 
 | 
			
		||||
        setTimeout(() => reject(new Error('Installation timeout after 10 minutes')), 600000)
 | 
			
		||||
      );
 | 
			
		||||
@ -1281,7 +1272,7 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
    } finally {
 | 
			
		||||
      // Comprehensive cleanup
 | 
			
		||||
      if (progressHandler) {
 | 
			
		||||
        ipcRenderer.removeListener('ollama:pull-progress', progressHandler);
 | 
			
		||||
        window.api.apiKeyHeader.removeOnOllamaPullProgress(progressHandler);
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      this.installingModel = null;
 | 
			
		||||
@ -1307,7 +1298,6 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
    this.clearMessages();
 | 
			
		||||
    this.requestUpdate();
 | 
			
		||||
    
 | 
			
		||||
    const { ipcRenderer } = window.require('electron');
 | 
			
		||||
    let progressHandler = null;
 | 
			
		||||
    
 | 
			
		||||
    try {
 | 
			
		||||
@ -1321,10 +1311,10 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
      
 | 
			
		||||
      ipcRenderer.on('whisper:download-progress', progressHandler);
 | 
			
		||||
      window.api.apiKeyHeader.onWhisperDownloadProgress(progressHandler);
 | 
			
		||||
      
 | 
			
		||||
      // Start download with timeout protection
 | 
			
		||||
      const downloadPromise = ipcRenderer.invoke('whisper:download-model', modelId);
 | 
			
		||||
      const downloadPromise = window.api.apiKeyHeader.downloadWhisperModel(modelId);
 | 
			
		||||
      const timeoutPromise = new Promise((_, reject) => 
 | 
			
		||||
        setTimeout(() => reject(new Error('Download timeout after 10 minutes')), 600000)
 | 
			
		||||
      );
 | 
			
		||||
@ -1351,7 +1341,7 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
    } finally {
 | 
			
		||||
      // Cleanup
 | 
			
		||||
      if (progressHandler) {
 | 
			
		||||
        ipcRenderer.removeListener('whisper:download-progress', progressHandler);
 | 
			
		||||
        window.api.apiKeyHeader.removeOnWhisperDownloadProgress(progressHandler);
 | 
			
		||||
      }
 | 
			
		||||
      delete this.whisperInstallingModels[modelId];
 | 
			
		||||
      this.requestUpdate();
 | 
			
		||||
@ -1411,8 +1401,6 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
    this.isLoading = true;
 | 
			
		||||
    this.clearMessages();
 | 
			
		||||
    this.requestUpdate();
 | 
			
		||||
 | 
			
		||||
    const { ipcRenderer } = window.require('electron');
 | 
			
		||||
    
 | 
			
		||||
    try {
 | 
			
		||||
        // Handle LLM provider
 | 
			
		||||
@ -1436,14 +1424,14 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // Validate Ollama is working
 | 
			
		||||
            llmResult = await ipcRenderer.invoke('model:validate-key', { 
 | 
			
		||||
            llmResult = await window.api.apiKeyHeader.validateKey({ 
 | 
			
		||||
                provider: 'ollama', 
 | 
			
		||||
                key: 'local' 
 | 
			
		||||
            });
 | 
			
		||||
            
 | 
			
		||||
            if (llmResult.success) {
 | 
			
		||||
                // Set the selected model
 | 
			
		||||
                await ipcRenderer.invoke('model:set-selected-model', { 
 | 
			
		||||
                await window.api.apiKeyHeader.setSelectedModel({ 
 | 
			
		||||
                    type: 'llm', 
 | 
			
		||||
                    modelId: this.selectedLlmModel 
 | 
			
		||||
                });
 | 
			
		||||
@ -1454,7 +1442,7 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
                throw new Error('Please enter LLM API key');
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            llmResult = await ipcRenderer.invoke('model:validate-key', { 
 | 
			
		||||
            llmResult = await window.api.apiKeyHeader.validateKey({ 
 | 
			
		||||
                provider: this.llmProvider, 
 | 
			
		||||
                key: this.llmApiKey.trim() 
 | 
			
		||||
            });
 | 
			
		||||
@ -1467,14 +1455,14 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
            sttResult = { success: true };
 | 
			
		||||
        } else if (this.sttProvider === 'whisper') {
 | 
			
		||||
            // For Whisper, just validate it's enabled (model download already handled in handleSttModelChange)
 | 
			
		||||
            sttResult = await ipcRenderer.invoke('model:validate-key', { 
 | 
			
		||||
            sttResult = await window.api.apiKeyHeader.validateKey({ 
 | 
			
		||||
                provider: 'whisper', 
 | 
			
		||||
                key: 'local' 
 | 
			
		||||
            });
 | 
			
		||||
            
 | 
			
		||||
            if (sttResult.success && this.selectedSttModel) {
 | 
			
		||||
                // Set the selected model
 | 
			
		||||
                await ipcRenderer.invoke('model:set-selected-model', { 
 | 
			
		||||
                await window.api.apiKeyHeader.setSelectedModel({ 
 | 
			
		||||
                    type: 'stt', 
 | 
			
		||||
                    modelId: this.selectedSttModel 
 | 
			
		||||
                });
 | 
			
		||||
@ -1485,7 +1473,7 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
                throw new Error('Please enter STT API key');
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            sttResult = await ipcRenderer.invoke('model:validate-key', { 
 | 
			
		||||
            sttResult = await window.api.apiKeyHeader.validateKey({ 
 | 
			
		||||
                provider: this.sttProvider, 
 | 
			
		||||
                key: this.sttApiKey.trim() 
 | 
			
		||||
            });
 | 
			
		||||
@ -1522,15 +1510,15 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
    e.preventDefault()
 | 
			
		||||
 | 
			
		||||
    console.log("Requesting Firebase authentication from main process...")
 | 
			
		||||
    if (window.require) {
 | 
			
		||||
      window.require("electron").ipcRenderer.invoke("start-firebase-auth")
 | 
			
		||||
    if (window.api) {
 | 
			
		||||
      window.api.common.startFirebaseAuth()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleClose() {
 | 
			
		||||
    console.log("Close button clicked")
 | 
			
		||||
    if (window.require) {
 | 
			
		||||
      window.require("electron").ipcRenderer.invoke("quit-application")
 | 
			
		||||
    if (window.api) {
 | 
			
		||||
      window.api.common.quitApplication()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -1541,10 +1529,10 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
    this.classList.remove("sliding-out");
 | 
			
		||||
    this.classList.add("hidden");
 | 
			
		||||
    
 | 
			
		||||
    console.log('[ApiKeyHeader] handleAnimationEnd: Animation completed, transitioning to next state...');
 | 
			
		||||
    console.log('[ApiKeyHeader] handleAnimationEnd: Transition completed, transitioning to next state...');
 | 
			
		||||
    
 | 
			
		||||
    if (!window.require) {
 | 
			
		||||
      console.error('[ApiKeyHeader] handleAnimationEnd: window.require not available');
 | 
			
		||||
    if (!window.api) {
 | 
			
		||||
      console.error('[ApiKeyHeader] handleAnimationEnd: window.api not available');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
@ -1553,14 +1541,12 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    const { ipcRenderer } = window.require('electron');
 | 
			
		||||
    
 | 
			
		||||
    ipcRenderer.invoke('get-current-user')
 | 
			
		||||
    window.api.common.getCurrentUser()
 | 
			
		||||
      .then(userState => {
 | 
			
		||||
        console.log('[ApiKeyHeader] handleAnimationEnd: User state retrieved:', userState);
 | 
			
		||||
        
 | 
			
		||||
        // Additional validation for local providers
 | 
			
		||||
        return ipcRenderer.invoke('model:are-providers-configured').then(isConfigured => {
 | 
			
		||||
        return window.api.apiKeyHeader.areProvidersConfigured().then(isConfigured => {
 | 
			
		||||
          console.log('[ApiKeyHeader] handleAnimationEnd: Providers configured check:', isConfigured);
 | 
			
		||||
          
 | 
			
		||||
          if (!isConfigured) {
 | 
			
		||||
@ -1585,7 +1571,8 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
 | 
			
		||||
  connectedCallback() {
 | 
			
		||||
    super.connectedCallback()
 | 
			
		||||
    this.addEventListener("animationend", this.handleAnimationEnd)
 | 
			
		||||
    // this.addEventListener("animationend", this.handleAnimationEnd)
 | 
			
		||||
    this.addEventListener("transitionend", this.handleAnimationEnd)
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  handleMessageFadeEnd(e) {
 | 
			
		||||
@ -1603,8 +1590,8 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
 | 
			
		||||
  disconnectedCallback() {
 | 
			
		||||
    super.disconnectedCallback()
 | 
			
		||||
    this.removeEventListener("animationend", this.handleAnimationEnd)
 | 
			
		||||
    
 | 
			
		||||
    // this.removeEventListener("animationend", this.handleAnimationEnd)
 | 
			
		||||
    this.removeEventListener("transitionend", this.handleAnimationEnd)
 | 
			
		||||
    // Professional cleanup of all resources
 | 
			
		||||
    this._performCompleteCleanup();
 | 
			
		||||
  }
 | 
			
		||||
@ -1624,12 +1611,8 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Cleanup event listeners
 | 
			
		||||
    if (window.require) {
 | 
			
		||||
      const { ipcRenderer } = window.require('electron');
 | 
			
		||||
      ipcRenderer.removeAllListeners('whisper:download-progress');
 | 
			
		||||
      ipcRenderer.removeAllListeners('ollama:install-progress');
 | 
			
		||||
      ipcRenderer.removeAllListeners('ollama:pull-progress');
 | 
			
		||||
      ipcRenderer.removeAllListeners('ollama:install-complete');
 | 
			
		||||
    if (window.api) {
 | 
			
		||||
      window.api.apiKeyHeader.removeAllListeners();
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Cancel any ongoing downloads
 | 
			
		||||
 | 
			
		||||
@ -32,6 +32,7 @@ class HeaderTransitionManager {
 | 
			
		||||
                this.apiKeyHeader = document.createElement('apikey-header');
 | 
			
		||||
                this.apiKeyHeader.stateUpdateCallback = (userState) => this.handleStateUpdate(userState);
 | 
			
		||||
                this.headerContainer.appendChild(this.apiKeyHeader);
 | 
			
		||||
                console.log('[HeaderController] ensureHeader: Header of type:', type, 'created.');
 | 
			
		||||
            } else if (type === 'permission') {
 | 
			
		||||
                this.permissionHeader = document.createElement('permission-setup');
 | 
			
		||||
                this.permissionHeader.continueCallback = () => this.transitionToMainHeader();
 | 
			
		||||
@ -50,41 +51,39 @@ class HeaderTransitionManager {
 | 
			
		||||
 | 
			
		||||
        this._bootstrap();
 | 
			
		||||
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
 | 
			
		||||
            ipcRenderer.on('user-state-changed', (event, userState) => {
 | 
			
		||||
        if (window.api) {
 | 
			
		||||
            window.api.headerController.onUserStateChanged((event, userState) => {
 | 
			
		||||
                console.log('[HeaderController] Received user state change:', userState);
 | 
			
		||||
                this.handleStateUpdate(userState);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            ipcRenderer.on('auth-failed', (event, { message }) => {
 | 
			
		||||
            window.api.headerController.onAuthFailed((event, { message }) => {
 | 
			
		||||
                console.error('[HeaderController] Received auth failure from main process:', message);
 | 
			
		||||
                if (this.apiKeyHeader) {
 | 
			
		||||
                    this.apiKeyHeader.errorMessage = 'Authentication failed. Please try again.';
 | 
			
		||||
                    this.apiKeyHeader.isLoading = false;
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            ipcRenderer.on('force-show-apikey-header', async () => {
 | 
			
		||||
            window.api.headerController.onForceShowApiKeyHeader(async () => {
 | 
			
		||||
                console.log('[HeaderController] Received broadcast to show apikey header. Switching now.');
 | 
			
		||||
                await this._resizeForApiKey();
 | 
			
		||||
                this.ensureHeader('apikey');
 | 
			
		||||
            });
 | 
			
		||||
            });            
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    notifyHeaderState(stateOverride) {
 | 
			
		||||
        const state = stateOverride || this.currentHeaderType || 'apikey';
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            window.require('electron').ipcRenderer.send('header-state-changed', state);
 | 
			
		||||
        if (window.api) {
 | 
			
		||||
            window.api.headerController.sendHeaderStateChanged(state);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async _bootstrap() {
 | 
			
		||||
        // The initial state will be sent by the main process via 'user-state-changed'
 | 
			
		||||
        // We just need to request it.
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const userState = await window.require('electron').ipcRenderer.invoke('get-current-user');
 | 
			
		||||
        if (window.api) {
 | 
			
		||||
            const userState = await window.api.common.getCurrentUser();
 | 
			
		||||
            console.log('[HeaderController] Bootstrapping with initial user state:', userState);
 | 
			
		||||
            this.handleStateUpdate(userState);
 | 
			
		||||
        } else {
 | 
			
		||||
@ -96,8 +95,7 @@ class HeaderTransitionManager {
 | 
			
		||||
 | 
			
		||||
    //////// after_modelStateService ////////
 | 
			
		||||
    async handleStateUpdate(userState) {
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        const isConfigured = await ipcRenderer.invoke('model:are-providers-configured');
 | 
			
		||||
        const isConfigured = await window.api.apiKeyHeader.areProvidersConfigured();
 | 
			
		||||
 | 
			
		||||
        if (isConfigured) {
 | 
			
		||||
            const { isLoggedIn } = userState;
 | 
			
		||||
@ -126,10 +124,9 @@ class HeaderTransitionManager {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check if permissions were previously completed
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        if (window.api) {
 | 
			
		||||
            try {
 | 
			
		||||
                const permissionsCompleted = await ipcRenderer.invoke('check-permissions-completed');
 | 
			
		||||
                const permissionsCompleted = await window.api.headerController.checkPermissionsCompleted();
 | 
			
		||||
                if (permissionsCompleted) {
 | 
			
		||||
                    console.log('[HeaderController] Permissions were previously completed, checking current status...');
 | 
			
		||||
                    
 | 
			
		||||
@ -161,39 +158,33 @@ class HeaderTransitionManager {
 | 
			
		||||
        this.ensureHeader('main');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _resizeForMain() {
 | 
			
		||||
        if (!window.require) return;
 | 
			
		||||
        return window
 | 
			
		||||
            .require('electron')
 | 
			
		||||
            .ipcRenderer.invoke('resize-header-window', { width: 353, height: 47 })
 | 
			
		||||
    async _resizeForMain() {
 | 
			
		||||
        if (!window.api) return;
 | 
			
		||||
        console.log('[HeaderController] _resizeForMain: Resizing window to 353x47');
 | 
			
		||||
        return window.api.headerController.resizeHeaderWindow({ width: 353, height: 47 })
 | 
			
		||||
            .catch(() => {});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async _resizeForApiKey() {
 | 
			
		||||
        if (!window.require) return;
 | 
			
		||||
        return window
 | 
			
		||||
            .require('electron')
 | 
			
		||||
            .ipcRenderer.invoke('resize-header-window', { width: 350, height: 300 })
 | 
			
		||||
        if (!window.api) return;
 | 
			
		||||
        console.log('[HeaderController] _resizeForApiKey: Resizing window to 350x300');
 | 
			
		||||
        return window.api.headerController.resizeHeaderWindow({ width: 350, height: 300 })
 | 
			
		||||
            .catch(() => {});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async _resizeForPermissionHeader() {
 | 
			
		||||
        if (!window.require) return;
 | 
			
		||||
        return window
 | 
			
		||||
            .require('electron')
 | 
			
		||||
            .ipcRenderer.invoke('resize-header-window', { width: 285, height: 220 })
 | 
			
		||||
        if (!window.api) return;
 | 
			
		||||
        return window.api.headerController.resizeHeaderWindow({ width: 285, height: 220 })
 | 
			
		||||
            .catch(() => {});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async checkPermissions() {
 | 
			
		||||
        if (!window.require) {
 | 
			
		||||
        if (!window.api) {
 | 
			
		||||
            return { success: true };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        
 | 
			
		||||
        try {
 | 
			
		||||
            const permissions = await ipcRenderer.invoke('check-system-permissions');
 | 
			
		||||
            const permissions = await window.api.headerController.checkSystemPermissions();
 | 
			
		||||
            console.log('[HeaderController] Current permissions:', permissions);
 | 
			
		||||
            
 | 
			
		||||
            if (!permissions.needsSetup) {
 | 
			
		||||
 | 
			
		||||
@ -370,8 +370,7 @@ export class MainHeader extends LitElement {
 | 
			
		||||
    async handleMouseDown(e) {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        const initialPosition = await ipcRenderer.invoke('get-header-position');
 | 
			
		||||
        const initialPosition = await window.api.mainHeader.getHeaderPosition();
 | 
			
		||||
 | 
			
		||||
        this.dragState = {
 | 
			
		||||
            initialMouseX: e.screenX,
 | 
			
		||||
@ -398,8 +397,7 @@ export class MainHeader extends LitElement {
 | 
			
		||||
        const newWindowX = this.dragState.initialWindowX + (e.screenX - this.dragState.initialMouseX);
 | 
			
		||||
        const newWindowY = this.dragState.initialWindowY + (e.screenY - this.dragState.initialMouseY);
 | 
			
		||||
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        ipcRenderer.invoke('move-header-to', newWindowX, newWindowY);
 | 
			
		||||
        window.api.mainHeader.moveHeaderTo(newWindowX, newWindowY);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleMouseUp(e) {
 | 
			
		||||
@ -455,12 +453,12 @@ export class MainHeader extends LitElement {
 | 
			
		||||
    
 | 
			
		||||
        if (this.classList.contains('hiding')) {
 | 
			
		||||
            this.classList.add('hidden');
 | 
			
		||||
            if (window.require) {
 | 
			
		||||
                window.require('electron').ipcRenderer.send('header-animation-finished', 'hidden');
 | 
			
		||||
            if (window.api) {
 | 
			
		||||
                window.api.mainHeader.sendHeaderAnimationFinished('hidden');
 | 
			
		||||
            }
 | 
			
		||||
        } else if (this.classList.contains('showing')) {
 | 
			
		||||
            if (window.require) {
 | 
			
		||||
                window.require('electron').ipcRenderer.send('header-animation-finished', 'visible');
 | 
			
		||||
            if (window.api) {
 | 
			
		||||
                window.api.mainHeader.sendHeaderAnimationFinished('visible');
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -474,8 +472,7 @@ export class MainHeader extends LitElement {
 | 
			
		||||
        super.connectedCallback();
 | 
			
		||||
        this.addEventListener('animationend', this.handleAnimationEnd);
 | 
			
		||||
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        if (window.api) {
 | 
			
		||||
 | 
			
		||||
            this._sessionStateTextListener = (event, { success }) => {
 | 
			
		||||
                if (success) {
 | 
			
		||||
@ -489,13 +486,14 @@ export class MainHeader extends LitElement {
 | 
			
		||||
                }
 | 
			
		||||
                this.isTogglingSession = false; // ✨ 로딩 상태만 해제
 | 
			
		||||
            };
 | 
			
		||||
            // window.api.mainHeader.onSessionStateText(this._sessionStateTextListener);
 | 
			
		||||
            ipcRenderer.on('listen:changeSessionResult', this._sessionStateTextListener);
 | 
			
		||||
 | 
			
		||||
            this._shortcutListener = (event, keybinds) => {
 | 
			
		||||
                console.log('[MainHeader] Received updated shortcuts:', keybinds);
 | 
			
		||||
                this.shortcuts = keybinds;
 | 
			
		||||
            };
 | 
			
		||||
            ipcRenderer.on('shortcuts-updated', this._shortcutListener);
 | 
			
		||||
            window.api.mainHeader.onShortcutsUpdated(this._shortcutListener);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -508,36 +506,35 @@ export class MainHeader extends LitElement {
 | 
			
		||||
            this.animationEndTimer = null;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        if (window.api) {
 | 
			
		||||
            if (this._sessionStateTextListener) {
 | 
			
		||||
                // window.api.mainHeader.removeOnSessionStateText(this._sessionStateTextListener);
 | 
			
		||||
                ipcRenderer.removeListener('listen:changeSessionResult', this._sessionStateTextListener);
 | 
			
		||||
            }
 | 
			
		||||
            if (this._shortcutListener) {
 | 
			
		||||
                ipcRenderer.removeListener('shortcuts-updated', this._shortcutListener);
 | 
			
		||||
                window.api.mainHeader.removeOnShortcutsUpdated(this._shortcutListener);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    invoke(channel, ...args) {
 | 
			
		||||
        if (this.wasJustDragged) return;
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            window.require('electron').ipcRenderer.invoke(channel, ...args);
 | 
			
		||||
        if (window.api) {
 | 
			
		||||
            window.api.mainHeader.invoke(channel, ...args);
 | 
			
		||||
        }
 | 
			
		||||
        // return Promise.resolve();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    showSettingsWindow(element) {
 | 
			
		||||
        if (this.wasJustDragged) return;
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        if (window.api) {
 | 
			
		||||
            console.log(`[MainHeader] showSettingsWindow called at ${Date.now()}`);
 | 
			
		||||
            
 | 
			
		||||
            ipcRenderer.send('cancel-hide-settings-window');
 | 
			
		||||
            window.api.mainHeader.cancelHideSettingsWindow();
 | 
			
		||||
 | 
			
		||||
            if (element) {
 | 
			
		||||
                const { left, top, width, height } = element.getBoundingClientRect();
 | 
			
		||||
                ipcRenderer.send('show-settings-window', {
 | 
			
		||||
                window.api.mainHeader.showSettingsWindow({
 | 
			
		||||
                    x: left,
 | 
			
		||||
                    y: top,
 | 
			
		||||
                    width,
 | 
			
		||||
@ -549,9 +546,9 @@ export class MainHeader extends LitElement {
 | 
			
		||||
 | 
			
		||||
    hideSettingsWindow() {
 | 
			
		||||
        if (this.wasJustDragged) return;
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
        if (window.api) {
 | 
			
		||||
            console.log(`[MainHeader] hideSettingsWindow called at ${Date.now()}`);
 | 
			
		||||
            window.require('electron').ipcRenderer.send('hide-settings-window');
 | 
			
		||||
            window.api.mainHeader.hideSettingsWindow();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -288,13 +288,12 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async checkPermissions() {
 | 
			
		||||
        if (!window.require || this.isChecking) return;
 | 
			
		||||
        if (!window.api || this.isChecking) return;
 | 
			
		||||
        
 | 
			
		||||
        this.isChecking = true;
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        
 | 
			
		||||
        try {
 | 
			
		||||
            const permissions = await ipcRenderer.invoke('check-system-permissions');
 | 
			
		||||
            const permissions = await window.api.permissionHeader.checkSystemPermissions();
 | 
			
		||||
            console.log('[PermissionHeader] Permission check result:', permissions);
 | 
			
		||||
            
 | 
			
		||||
            const prevMic = this.microphoneGranted;
 | 
			
		||||
@ -324,13 +323,12 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleMicrophoneClick() {
 | 
			
		||||
        if (!window.require || this.microphoneGranted === 'granted') return;
 | 
			
		||||
        if (!window.api || this.microphoneGranted === 'granted') return;
 | 
			
		||||
        
 | 
			
		||||
        console.log('[PermissionHeader] Requesting microphone permission...');
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        
 | 
			
		||||
        try {
 | 
			
		||||
            const result = await ipcRenderer.invoke('check-system-permissions');
 | 
			
		||||
            const result = await window.api.permissionHeader.checkSystemPermissions();
 | 
			
		||||
            console.log('[PermissionHeader] Microphone permission result:', result);
 | 
			
		||||
            
 | 
			
		||||
            if (result.microphone === 'granted') {
 | 
			
		||||
@ -340,7 +338,7 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
              }
 | 
			
		||||
            
 | 
			
		||||
              if (result.microphone === 'not-determined' || result.microphone === 'denied' || result.microphone === 'unknown' || result.microphone === 'restricted') {
 | 
			
		||||
                const res = await ipcRenderer.invoke('request-microphone-permission');
 | 
			
		||||
                const res = await window.api.permissionHeader.requestMicrophonePermission();
 | 
			
		||||
                if (res.status === 'granted' || res.success === true) {
 | 
			
		||||
                    this.microphoneGranted = 'granted';
 | 
			
		||||
                    this.requestUpdate();
 | 
			
		||||
@ -357,13 +355,12 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleScreenClick() {
 | 
			
		||||
        if (!window.require || this.screenGranted === 'granted') return;
 | 
			
		||||
        if (!window.api || this.screenGranted === 'granted') return;
 | 
			
		||||
        
 | 
			
		||||
        console.log('[PermissionHeader] Checking screen recording permission...');
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        
 | 
			
		||||
        try {
 | 
			
		||||
            const permissions = await ipcRenderer.invoke('check-system-permissions');
 | 
			
		||||
            const permissions = await window.api.permissionHeader.checkSystemPermissions();
 | 
			
		||||
            console.log('[PermissionHeader] Screen permission check result:', permissions);
 | 
			
		||||
            
 | 
			
		||||
            if (permissions.screen === 'granted') {
 | 
			
		||||
@ -373,7 +370,7 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
            }
 | 
			
		||||
            if (permissions.screen === 'not-determined' || permissions.screen === 'denied' || permissions.screen === 'unknown' || permissions.screen === 'restricted') {
 | 
			
		||||
            console.log('[PermissionHeader] Opening screen recording preferences...');
 | 
			
		||||
            await ipcRenderer.invoke('open-system-preferences', 'screen-recording');
 | 
			
		||||
            await window.api.permissionHeader.openSystemPreferences('screen-recording');
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // Check permissions again after a delay
 | 
			
		||||
@ -389,10 +386,9 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
            this.microphoneGranted === 'granted' && 
 | 
			
		||||
            this.screenGranted === 'granted') {
 | 
			
		||||
            // Mark permissions as completed
 | 
			
		||||
            if (window.require) {
 | 
			
		||||
                const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            if (window.api) {
 | 
			
		||||
                try {
 | 
			
		||||
                    await ipcRenderer.invoke('mark-permissions-completed');
 | 
			
		||||
                    await window.api.permissionHeader.markPermissionsCompleted();
 | 
			
		||||
                    console.log('[PermissionHeader] Marked permissions as completed');
 | 
			
		||||
                } catch (error) {
 | 
			
		||||
                    console.error('[PermissionHeader] Error marking permissions as completed:', error);
 | 
			
		||||
@ -405,8 +401,8 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
 | 
			
		||||
    handleClose() {
 | 
			
		||||
        console.log('Close button clicked');
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            window.require('electron').ipcRenderer.invoke('quit-application');
 | 
			
		||||
        if (window.api) {
 | 
			
		||||
            window.api.common.quitApplication();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -74,10 +74,8 @@ export class PickleGlassApp extends LitElement {
 | 
			
		||||
    connectedCallback() {
 | 
			
		||||
        super.connectedCallback();
 | 
			
		||||
        
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            
 | 
			
		||||
            ipcRenderer.on('click-through-toggled', (_, isEnabled) => {
 | 
			
		||||
        if (window.api) {
 | 
			
		||||
            window.api.pickleGlassApp.onClickThroughToggled((_, isEnabled) => {
 | 
			
		||||
                this._isClickThrough = isEnabled;
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
@ -85,9 +83,8 @@ export class PickleGlassApp extends LitElement {
 | 
			
		||||
 | 
			
		||||
    disconnectedCallback() {
 | 
			
		||||
        super.disconnectedCallback();
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            ipcRenderer.removeAllListeners('click-through-toggled');
 | 
			
		||||
        if (window.api) {
 | 
			
		||||
            window.api.pickleGlassApp.removeAllClickThroughListeners();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -121,9 +118,8 @@ export class PickleGlassApp extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleClose() {
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            await ipcRenderer.invoke('quit-application');
 | 
			
		||||
        if (window.api) {
 | 
			
		||||
            await window.api.common.quitApplication();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
    <head>
 | 
			
		||||
        <meta http-equiv="content-security-policy" content="script-src 'self' 'unsafe-inline'" />
 | 
			
		||||
        <meta http-equiv="content-security-policy" content="script-src 'self' 'unsafe-inline' 'unsafe-eval'" />
 | 
			
		||||
        <title>Pickle Glass Content</title>
 | 
			
		||||
        <style>
 | 
			
		||||
            :root {
 | 
			
		||||
@ -238,15 +238,13 @@
 | 
			
		||||
            window.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
                const app = document.getElementById('pickle-glass');
 | 
			
		||||
        
 | 
			
		||||
                if (window.require) {
 | 
			
		||||
                    const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        
 | 
			
		||||
                if (window.api) {
 | 
			
		||||
                    // --- REFACTORED: Event-driven animation handling ---
 | 
			
		||||
                    app.addEventListener('animationend', (event) => {
 | 
			
		||||
                        // 숨김 애니메이션이 끝나면 main 프로세스에 알려 창을 실제로 숨깁니다.
 | 
			
		||||
                        if (event.animationName === 'slideUpToHeader' || event.animationName === 'settingsCollapseToButton') {
 | 
			
		||||
                            console.log(`Animation finished: ${event.animationName}. Notifying main process.`);
 | 
			
		||||
                            ipcRenderer.send('animation-finished');
 | 
			
		||||
                            window.api.content.sendAnimationFinished();
 | 
			
		||||
        
 | 
			
		||||
                            // 완료 후 애니메이션 클래스 정리
 | 
			
		||||
                            app.classList.remove('window-sliding-up', 'settings-window-hide');
 | 
			
		||||
@ -257,26 +255,26 @@
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
        
 | 
			
		||||
                    ipcRenderer.on('window-show-animation', () => {
 | 
			
		||||
                    window.api.content.onWindowShowAnimation(() => {
 | 
			
		||||
                        console.log('Starting window show animation');
 | 
			
		||||
                        app.classList.remove('window-hidden', 'window-sliding-up', 'settings-window-hide');
 | 
			
		||||
                        app.classList.add('window-sliding-down');
 | 
			
		||||
                    });
 | 
			
		||||
        
 | 
			
		||||
                    ipcRenderer.on('window-hide-animation', () => {
 | 
			
		||||
                    window.api.content.onWindowHideAnimation(() => {
 | 
			
		||||
                        console.log('Starting window hide animation');
 | 
			
		||||
                        app.classList.remove('window-sliding-down', 'settings-window-show');
 | 
			
		||||
                        app.classList.add('window-sliding-up');
 | 
			
		||||
                    });
 | 
			
		||||
        
 | 
			
		||||
                    ipcRenderer.on('settings-window-hide-animation', () => {
 | 
			
		||||
                    window.api.content.onSettingsWindowHideAnimation(() => {
 | 
			
		||||
                        console.log('Starting settings window hide animation');
 | 
			
		||||
                        app.classList.remove('window-sliding-down', 'settings-window-show');
 | 
			
		||||
                        app.classList.add('settings-window-hide');
 | 
			
		||||
                    });
 | 
			
		||||
        
 | 
			
		||||
                    // --- UNCHANGED: Existing logic for listen window movement ---
 | 
			
		||||
                    ipcRenderer.on('listen-window-move-to-center', () => {
 | 
			
		||||
                    window.api.content.onListenWindowMoveToCenter(() => {
 | 
			
		||||
                        console.log('Moving listen window to center');
 | 
			
		||||
                        app.classList.add('listen-window-moving');
 | 
			
		||||
                        app.classList.remove('listen-window-left');
 | 
			
		||||
@ -287,7 +285,7 @@
 | 
			
		||||
                        }, 350);
 | 
			
		||||
                    });
 | 
			
		||||
        
 | 
			
		||||
                    ipcRenderer.on('listen-window-move-to-left', () => {
 | 
			
		||||
                    window.api.content.onListenWindowMoveToLeft(() => {
 | 
			
		||||
                        console.log('Moving listen window to left');
 | 
			
		||||
                        app.classList.add('listen-window-moving');
 | 
			
		||||
                        app.classList.remove('listen-window-center');
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
    <head>
 | 
			
		||||
        <meta http-equiv="content-security-policy" content="script-src 'self' 'unsafe-inline'" />
 | 
			
		||||
        <meta http-equiv="content-security-policy" content="script-src 'self' 'unsafe-inline' 'unsafe-eval'" />
 | 
			
		||||
        <title>Pickle Glass Header</title>
 | 
			
		||||
        <style>
 | 
			
		||||
            html,
 | 
			
		||||
 | 
			
		||||
@ -769,15 +769,14 @@ export class AskView extends LitElement {
 | 
			
		||||
            this.handleSendText(null, question);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            ipcRenderer.on('ask:sendQuestionToRenderer', this.handleQuestionFromAssistant);
 | 
			
		||||
            ipcRenderer.on('hide-text-input', () => {
 | 
			
		||||
        if (window.api) {
 | 
			
		||||
            window.api.askView.onSendQuestionToRenderer(this.handleQuestionFromAssistant);
 | 
			
		||||
            window.api.askView.onHideTextInput(() => {
 | 
			
		||||
                console.log('📤 Hide text input signal received');
 | 
			
		||||
                this.showTextInput = false;
 | 
			
		||||
                this.requestUpdate();
 | 
			
		||||
            });
 | 
			
		||||
            ipcRenderer.on('ask:showTextInput', () => {
 | 
			
		||||
            window.api.askView.onShowTextInput(() => {
 | 
			
		||||
                console.log('📤 Show text input signal received');
 | 
			
		||||
                if (!this.showTextInput) {
 | 
			
		||||
                    this.showTextInput = true;
 | 
			
		||||
@ -785,11 +784,11 @@ export class AskView extends LitElement {
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            ipcRenderer.on('ask-response-chunk', this.handleStreamChunk);
 | 
			
		||||
            ipcRenderer.on('ask-response-stream-end', this.handleStreamEnd);
 | 
			
		||||
            window.api.askView.onResponseChunk(this.handleStreamChunk);
 | 
			
		||||
            window.api.askView.onResponseStreamEnd(this.handleStreamEnd);
 | 
			
		||||
 | 
			
		||||
            ipcRenderer.on('scroll-response-up', () => this.handleScroll('up'));
 | 
			
		||||
            ipcRenderer.on('scroll-response-down', () => this.handleScroll('down'));
 | 
			
		||||
            window.api.askView.onScrollResponseUp(() => this.handleScroll('up'));
 | 
			
		||||
            window.api.askView.onScrollResponseDown(() => this.handleScroll('down'));
 | 
			
		||||
            console.log('✅ AskView: IPC 이벤트 리스너 등록 완료');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -816,17 +815,11 @@ export class AskView extends LitElement {
 | 
			
		||||
 | 
			
		||||
        Object.values(this.lineCopyTimeouts).forEach(timeout => clearTimeout(timeout));
 | 
			
		||||
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            ipcRenderer.removeListener('hide-text-input', () => { });
 | 
			
		||||
            ipcRenderer.removeListener('ask:showTextInput', () => { });
 | 
			
		||||
 | 
			
		||||
            ipcRenderer.removeListener('ask-response-chunk', this.handleStreamChunk);
 | 
			
		||||
            ipcRenderer.removeListener('ask-response-stream-end', this.handleStreamEnd);
 | 
			
		||||
 | 
			
		||||
            ipcRenderer.removeListener('scroll-response-up', () => this.handleScroll('up'));
 | 
			
		||||
            ipcRenderer.removeListener('scroll-response-down', () => this.handleScroll('down'));
 | 
			
		||||
            console.log('✅ AskView: IPC 이벤트 리스너 제거 완료');
 | 
			
		||||
        if (window.api) {
 | 
			
		||||
            // Note: We need to keep references to the actual callbacks used in connectedCallback
 | 
			
		||||
            // For now, we'll just log that removal is needed
 | 
			
		||||
            // TODO: Store callback references for proper removal
 | 
			
		||||
            console.log('✅ AskView: IPC 이벤트 리스너 제거 필요');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -889,7 +882,7 @@ export class AskView extends LitElement {
 | 
			
		||||
 | 
			
		||||
    handleCloseAskWindow() {
 | 
			
		||||
        this.clearResponseContent();
 | 
			
		||||
        ipcRenderer.invoke('ask:closeAskWindow');
 | 
			
		||||
        window.api.askView.closeAskWindow();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleCloseIfNoContent() {
 | 
			
		||||
@ -1113,9 +1106,8 @@ export class AskView extends LitElement {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    requestWindowResize(targetHeight) {
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            ipcRenderer.invoke('adjust-window-height', targetHeight);
 | 
			
		||||
        if (window.api) {
 | 
			
		||||
            window.api.askView.adjustWindowHeight(targetHeight);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1279,9 +1271,8 @@ export class AskView extends LitElement {
 | 
			
		||||
        this.requestUpdate();
 | 
			
		||||
        this.renderContent();
 | 
			
		||||
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            ipcRenderer.invoke('ask:sendMessage', text).catch(error => {
 | 
			
		||||
        if (window.api) {
 | 
			
		||||
            window.api.askView.sendMessage(text).catch(error => {
 | 
			
		||||
                console.error('Error sending text:', error);
 | 
			
		||||
                this.isLoading = false;
 | 
			
		||||
                this.isStreaming = false;
 | 
			
		||||
@ -1410,7 +1401,7 @@ export class AskView extends LitElement {
 | 
			
		||||
 | 
			
		||||
    // Dynamically resize the BrowserWindow to fit current content
 | 
			
		||||
    adjustWindowHeight() {
 | 
			
		||||
        if (!window.require) return;
 | 
			
		||||
        if (!window.api) return;
 | 
			
		||||
 | 
			
		||||
        this.updateComplete.then(() => {
 | 
			
		||||
            const headerEl = this.shadowRoot.querySelector('.response-header');
 | 
			
		||||
@ -1427,8 +1418,7 @@ export class AskView extends LitElement {
 | 
			
		||||
 | 
			
		||||
            const targetHeight = Math.min(700, idealHeight);
 | 
			
		||||
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            ipcRenderer.invoke('adjust-window-height', targetHeight);
 | 
			
		||||
            window.api.askView.adjustWindowHeight(targetHeight);
 | 
			
		||||
 | 
			
		||||
        }).catch(err => console.error('AskView adjustWindowHeight error:', err));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -453,9 +453,8 @@ export class ListenView extends LitElement {
 | 
			
		||||
        if (this.isSessionActive) {
 | 
			
		||||
            this.startTimer();
 | 
			
		||||
        }
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            ipcRenderer.on('session-state-changed', (event, { isActive }) => {
 | 
			
		||||
        if (window.api) {
 | 
			
		||||
            window.api.listenView.onSessionStateChanged((event, { isActive }) => {
 | 
			
		||||
                const wasActive = this.isSessionActive;
 | 
			
		||||
                this.isSessionActive = isActive;
 | 
			
		||||
 | 
			
		||||
@ -514,7 +513,7 @@ export class ListenView extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    adjustWindowHeight() {
 | 
			
		||||
        if (!window.require) return;
 | 
			
		||||
        if (!window.api) return;
 | 
			
		||||
 | 
			
		||||
        this.updateComplete
 | 
			
		||||
            .then(() => {
 | 
			
		||||
@ -537,8 +536,7 @@ export class ListenView extends LitElement {
 | 
			
		||||
                    `[Height Adjusted] Mode: ${this.viewMode}, TopBar: ${topBarHeight}px, Content: ${contentHeight}px, Ideal: ${idealHeight}px, Target: ${targetHeight}px`
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                const { ipcRenderer } = window.require('electron');
 | 
			
		||||
                ipcRenderer.invoke('adjust-window-height', targetHeight);
 | 
			
		||||
                window.api.listenView.adjustWindowHeight(targetHeight);
 | 
			
		||||
            })
 | 
			
		||||
            .catch(error => {
 | 
			
		||||
                console.error('Error in adjustWindowHeight:', error);
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,3 @@
 | 
			
		||||
const { ipcRenderer } = require('electron');
 | 
			
		||||
const createAecModule = require('./aec.js');
 | 
			
		||||
 | 
			
		||||
let aecModPromise = null;     // 한 번만 로드
 | 
			
		||||
@ -34,8 +33,8 @@ const SAMPLE_RATE = 24000;
 | 
			
		||||
const AUDIO_CHUNK_DURATION = 0.1;
 | 
			
		||||
const BUFFER_SIZE = 4096;
 | 
			
		||||
 | 
			
		||||
const isLinux = process.platform === 'linux';
 | 
			
		||||
const isMacOS = process.platform === 'darwin';
 | 
			
		||||
const isLinux = window.api.platform.isLinux;
 | 
			
		||||
const isMacOS = window.api.platform.isMacOS;
 | 
			
		||||
 | 
			
		||||
let mediaStream = null;
 | 
			
		||||
let micMediaStream = null;
 | 
			
		||||
@ -198,7 +197,7 @@ function runAecSync(micF32, sysF32) {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// System audio data handler
 | 
			
		||||
ipcRenderer.on('system-audio-data', (event, { data }) => {
 | 
			
		||||
window.api.listenCapture.onSystemAudioData((event, { data }) => {
 | 
			
		||||
    systemAudioBuffer.push({
 | 
			
		||||
        data: data,
 | 
			
		||||
        timestamp: Date.now(),
 | 
			
		||||
@ -336,7 +335,7 @@ async function setupMicProcessing(micStream) {
 | 
			
		||||
            const pcm16 = convertFloat32ToInt16(processedChunk);
 | 
			
		||||
            const b64 = arrayBufferToBase64(pcm16.buffer);
 | 
			
		||||
 | 
			
		||||
            ipcRenderer.invoke('send-audio-content', {
 | 
			
		||||
            window.api.listenCapture.sendAudioContent({
 | 
			
		||||
                data: b64,
 | 
			
		||||
                mimeType: 'audio/pcm;rate=24000',
 | 
			
		||||
            });
 | 
			
		||||
@ -369,7 +368,7 @@ function setupLinuxMicProcessing(micStream) {
 | 
			
		||||
            const pcmData16 = convertFloat32ToInt16(chunk);
 | 
			
		||||
            const base64Data = arrayBufferToBase64(pcmData16.buffer);
 | 
			
		||||
 | 
			
		||||
            await ipcRenderer.invoke('send-audio-content', {
 | 
			
		||||
            await window.api.listenCapture.sendAudioContent({
 | 
			
		||||
                data: base64Data,
 | 
			
		||||
                mimeType: 'audio/pcm;rate=24000',
 | 
			
		||||
            });
 | 
			
		||||
@ -403,7 +402,7 @@ function setupSystemAudioProcessing(systemStream) {
 | 
			
		||||
            const base64Data = arrayBufferToBase64(pcmData16.buffer);
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                await ipcRenderer.invoke('send-system-audio-content', {
 | 
			
		||||
                await window.api.listenCapture.sendSystemAudioContent({
 | 
			
		||||
                    data: base64Data,
 | 
			
		||||
                    mimeType: 'audio/pcm;rate=24000',
 | 
			
		||||
                });
 | 
			
		||||
@ -427,13 +426,13 @@ async function captureScreenshot(imageQuality = 'medium', isManual = false) {
 | 
			
		||||
 | 
			
		||||
    // Check rate limiting for automated screenshots only
 | 
			
		||||
    if (!isManual && tokenTracker.shouldThrottle()) {
 | 
			
		||||
        console.log('⚠️ Automated screenshot skipped due to rate limiting');
 | 
			
		||||
        console.log('Automated screenshot skipped due to rate limiting');
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        // Request screenshot from main process
 | 
			
		||||
        const result = await ipcRenderer.invoke('capture-screenshot', {
 | 
			
		||||
        const result = await window.api.listenCapture.captureScreenshot({
 | 
			
		||||
            quality: imageQuality,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@ -470,16 +469,16 @@ async function captureManualScreenshot(imageQuality = null) {
 | 
			
		||||
async function getCurrentScreenshot() {
 | 
			
		||||
    try {
 | 
			
		||||
        // First try to get a fresh screenshot from main process
 | 
			
		||||
        const result = await ipcRenderer.invoke('get-current-screenshot');
 | 
			
		||||
        const result = await window.api.listenCapture.getCurrentScreenshot();
 | 
			
		||||
 | 
			
		||||
        if (result.success && result.base64) {
 | 
			
		||||
            console.log('📸 Got fresh screenshot from main process');
 | 
			
		||||
            console.log('Got fresh screenshot from main process');
 | 
			
		||||
            return result.base64;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // If no screenshot available, capture one now
 | 
			
		||||
        console.log('📸 No screenshot available, capturing new one');
 | 
			
		||||
        const captureResult = await ipcRenderer.invoke('capture-screenshot', {
 | 
			
		||||
        console.log('No screenshot available, capturing new one');
 | 
			
		||||
        const captureResult = await window.api.listenCapture.captureScreenshot({
 | 
			
		||||
            quality: currentImageQuality,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@ -490,7 +489,7 @@ async function getCurrentScreenshot() {
 | 
			
		||||
 | 
			
		||||
        // Fallback to last stored screenshot
 | 
			
		||||
        if (lastScreenshotBase64) {
 | 
			
		||||
            console.log('📸 Using cached screenshot');
 | 
			
		||||
            console.log('Using cached screenshot');
 | 
			
		||||
            return lastScreenshotBase64;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -518,15 +517,15 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
 | 
			
		||||
            console.log('Starting macOS capture with SystemAudioDump...');
 | 
			
		||||
 | 
			
		||||
            // Start macOS audio capture
 | 
			
		||||
            const audioResult = await ipcRenderer.invoke('start-macos-audio');
 | 
			
		||||
            const audioResult = await window.api.listenCapture.startMacosAudio();
 | 
			
		||||
            if (!audioResult.success) {
 | 
			
		||||
                console.warn('[listenCapture] macOS audio start failed:', audioResult.error);
 | 
			
		||||
 | 
			
		||||
                // 이미 실행 중 → stop 후 재시도
 | 
			
		||||
                if (audioResult.error === 'already_running') {
 | 
			
		||||
                    await ipcRenderer.invoke('stop-macos-audio');
 | 
			
		||||
                    await window.api.listenCapture.stopMacosAudio();
 | 
			
		||||
                    await new Promise(r => setTimeout(r, 500));
 | 
			
		||||
                    const retry = await ipcRenderer.invoke('start-macos-audio');
 | 
			
		||||
                    const retry = await window.api.listenCapture.startMacosAudio();
 | 
			
		||||
                    if (!retry.success) {
 | 
			
		||||
                        throw new Error('Retry failed: ' + retry.error);
 | 
			
		||||
                    }
 | 
			
		||||
@ -536,7 +535,7 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Initialize screen capture in main process
 | 
			
		||||
            const screenResult = await ipcRenderer.invoke('start-screen-capture');
 | 
			
		||||
            const screenResult = await window.api.listenCapture.startScreenCapture();
 | 
			
		||||
            if (!screenResult.success) {
 | 
			
		||||
                throw new Error('Failed to start screen capture: ' + screenResult.error);
 | 
			
		||||
            }
 | 
			
		||||
@ -604,13 +603,13 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
 | 
			
		||||
            console.log('Starting Windows capture with native loopback audio...');
 | 
			
		||||
 | 
			
		||||
            // Start screen capture in main process for screenshots
 | 
			
		||||
            const screenResult = await ipcRenderer.invoke('start-screen-capture');
 | 
			
		||||
            const screenResult = await window.api.listenCapture.startScreenCapture();
 | 
			
		||||
            if (!screenResult.success) {
 | 
			
		||||
                throw new Error('Failed to start screen capture: ' + screenResult.error);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Ensure STT sessions are initialized before starting audio capture
 | 
			
		||||
            const sessionActive = await ipcRenderer.invoke('is-session-active');
 | 
			
		||||
            const sessionActive = await window.api.listenCapture.isSessionActive();
 | 
			
		||||
            if (!sessionActive) {
 | 
			
		||||
                throw new Error('STT sessions not initialized - please wait for initialization to complete');
 | 
			
		||||
            }
 | 
			
		||||
@ -715,13 +714,13 @@ function stopCapture() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Stop screen capture in main process
 | 
			
		||||
    ipcRenderer.invoke('stop-screen-capture').catch(err => {
 | 
			
		||||
    window.api.listenCapture.stopScreenCapture().catch(err => {
 | 
			
		||||
        console.error('Error stopping screen capture:', err);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Stop macOS audio capture if running
 | 
			
		||||
    if (isMacOS) {
 | 
			
		||||
        ipcRenderer.invoke('stop-macos-audio').catch(err => {
 | 
			
		||||
        window.api.listenCapture.stopMacosAudio().catch(err => {
 | 
			
		||||
            console.error('Error stopping macOS audio:', err);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,4 @@
 | 
			
		||||
// renderer.js
 | 
			
		||||
const { ipcRenderer } = require('electron');
 | 
			
		||||
const listenCapture = require('./listenCapture.js');
 | 
			
		||||
const params        = new URLSearchParams(window.location.search);
 | 
			
		||||
const isListenView  = params.get('view') === 'listen';
 | 
			
		||||
@ -15,7 +14,7 @@ window.pickleGlass = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
ipcRenderer.on('change-listen-capture-state', (_event, { status }) => {
 | 
			
		||||
window.api.renderer.onChangeListenCaptureState((_event, { status }) => {
 | 
			
		||||
    if (!isListenView) {
 | 
			
		||||
        console.log('[Renderer] Non-listen view: ignoring capture-state change');
 | 
			
		||||
        return;
 | 
			
		||||
 | 
			
		||||
@ -95,17 +95,15 @@ export class SttView extends LitElement {
 | 
			
		||||
 | 
			
		||||
    connectedCallback() {
 | 
			
		||||
        super.connectedCallback();
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            ipcRenderer.on('stt-update', this.handleSttUpdate);
 | 
			
		||||
        if (window.api) {
 | 
			
		||||
            window.api.sttView.onSttUpdate(this.handleSttUpdate);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    disconnectedCallback() {
 | 
			
		||||
        super.disconnectedCallback();
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            ipcRenderer.removeListener('stt-update', this.handleSttUpdate);
 | 
			
		||||
        if (window.api) {
 | 
			
		||||
            window.api.sttView.removeOnSttUpdate(this.handleSttUpdate);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -262,9 +262,8 @@ export class SummaryView extends LitElement {
 | 
			
		||||
 | 
			
		||||
    connectedCallback() {
 | 
			
		||||
        super.connectedCallback();
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            ipcRenderer.on('summary-update', (event, data) => {
 | 
			
		||||
        if (window.api) {
 | 
			
		||||
            window.api.summaryView.onSummaryUpdate((event, data) => {
 | 
			
		||||
                this.structuredData = data;
 | 
			
		||||
                this.requestUpdate();
 | 
			
		||||
            });
 | 
			
		||||
@ -273,9 +272,8 @@ export class SummaryView extends LitElement {
 | 
			
		||||
 | 
			
		||||
    disconnectedCallback() {
 | 
			
		||||
        super.disconnectedCallback();
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            ipcRenderer.removeAllListeners('summary-update');
 | 
			
		||||
        if (window.api) {
 | 
			
		||||
            window.api.summaryView.removeAllSummaryUpdateListeners();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -408,11 +406,9 @@ export class SummaryView extends LitElement {
 | 
			
		||||
    async handleRequestClick(requestText) {
 | 
			
		||||
        console.log('🔥 Analysis request clicked:', requestText);
 | 
			
		||||
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
 | 
			
		||||
        if (window.api) {
 | 
			
		||||
            try {
 | 
			
		||||
                const result = await ipcRenderer.invoke('ask:sendQuestionToMain', requestText);
 | 
			
		||||
                const result = await window.api.summaryView.sendQuestionToMain(requestText);
 | 
			
		||||
 | 
			
		||||
                if (result.success) {
 | 
			
		||||
                    console.log('✅ Question sent to AskView successfully');
 | 
			
		||||
 | 
			
		||||
@ -543,11 +543,10 @@ export class SettingsView extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async loadAutoUpdateSetting() {
 | 
			
		||||
        if (!window.require) return;
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        if (!window.api) return;
 | 
			
		||||
        this.autoUpdateLoading = true;
 | 
			
		||||
        try {
 | 
			
		||||
            const enabled = await ipcRenderer.invoke('settings:get-auto-update');
 | 
			
		||||
            const enabled = await window.api.settingsView.getAutoUpdate();
 | 
			
		||||
            this.autoUpdateEnabled = enabled;
 | 
			
		||||
            console.log('Auto-update setting loaded:', enabled);
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
@ -559,13 +558,12 @@ export class SettingsView extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleToggleAutoUpdate() {
 | 
			
		||||
        if (!window.require || this.autoUpdateLoading) return;
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        if (!window.api || this.autoUpdateLoading) return;
 | 
			
		||||
        this.autoUpdateLoading = true;
 | 
			
		||||
        this.requestUpdate();
 | 
			
		||||
        try {
 | 
			
		||||
            const newValue = !this.autoUpdateEnabled;
 | 
			
		||||
            const result = await ipcRenderer.invoke('settings:set-auto-update', newValue);
 | 
			
		||||
            const result = await window.api.settingsView.setAutoUpdate(newValue);
 | 
			
		||||
            if (result && result.success) {
 | 
			
		||||
                this.autoUpdateEnabled = newValue;
 | 
			
		||||
            } else {
 | 
			
		||||
@ -580,18 +578,17 @@ export class SettingsView extends LitElement {
 | 
			
		||||
 | 
			
		||||
    //////// after_modelStateService ////////
 | 
			
		||||
    async loadInitialData() {
 | 
			
		||||
        if (!window.require) return;
 | 
			
		||||
        if (!window.api) return;
 | 
			
		||||
        this.isLoading = true;
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        try {
 | 
			
		||||
            const [userState, modelSettings, presets, contentProtection, shortcuts, ollamaStatus, whisperModelsResult] = await Promise.all([
 | 
			
		||||
                ipcRenderer.invoke('get-current-user'),
 | 
			
		||||
                ipcRenderer.invoke('settings:get-model-settings'), // Facade call
 | 
			
		||||
                ipcRenderer.invoke('settings:getPresets'),
 | 
			
		||||
                ipcRenderer.invoke('get-content-protection-status'),
 | 
			
		||||
                ipcRenderer.invoke('get-current-shortcuts'),
 | 
			
		||||
                ipcRenderer.invoke('settings:get-ollama-status'),
 | 
			
		||||
                ipcRenderer.invoke('whisper:get-installed-models')
 | 
			
		||||
                window.api.settingsView.getCurrentUser(),
 | 
			
		||||
                window.api.settingsView.getModelSettings(), // Facade call
 | 
			
		||||
                window.api.settingsView.getPresets(),
 | 
			
		||||
                window.api.settingsView.getContentProtectionStatus(),
 | 
			
		||||
                window.api.settingsView.getCurrentShortcuts(),
 | 
			
		||||
                window.api.settingsView.getOllamaStatus(),
 | 
			
		||||
                window.api.settingsView.getWhisperInstalledModels()
 | 
			
		||||
            ]);
 | 
			
		||||
            
 | 
			
		||||
            if (userState && userState.isLoggedIn) this.firebaseUser = userState;
 | 
			
		||||
@ -646,10 +643,9 @@ export class SettingsView extends LitElement {
 | 
			
		||||
        // For Ollama, we need to ensure it's ready first
 | 
			
		||||
        if (provider === 'ollama') {
 | 
			
		||||
        this.saving = true;
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            
 | 
			
		||||
            // First ensure Ollama is installed and running
 | 
			
		||||
            const ensureResult = await ipcRenderer.invoke('settings:ensure-ollama-ready');
 | 
			
		||||
            const ensureResult = await window.api.settingsView.ensureOllamaReady();
 | 
			
		||||
            if (!ensureResult.success) {
 | 
			
		||||
                alert(`Failed to setup Ollama: ${ensureResult.error}`);
 | 
			
		||||
                this.saving = false;
 | 
			
		||||
@ -657,7 +653,7 @@ export class SettingsView extends LitElement {
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // Now validate (which will check if service is running)
 | 
			
		||||
            const result = await ipcRenderer.invoke('settings:validate-and-save-key', { provider, key: 'local' });
 | 
			
		||||
            const result = await window.api.settingsView.validateKey({ provider, key: 'local' });
 | 
			
		||||
            
 | 
			
		||||
            if (result.success) {
 | 
			
		||||
                await this.refreshModelData();
 | 
			
		||||
@ -672,8 +668,7 @@ export class SettingsView extends LitElement {
 | 
			
		||||
        // For Whisper, just enable it
 | 
			
		||||
        if (provider === 'whisper') {
 | 
			
		||||
            this.saving = true;
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            const result = await ipcRenderer.invoke('settings:validate-and-save-key', { provider, key: 'local' });
 | 
			
		||||
            const result = await window.api.settingsView.validateKey({ provider, key: 'local' });
 | 
			
		||||
            
 | 
			
		||||
            if (result.success) {
 | 
			
		||||
                await this.refreshModelData();
 | 
			
		||||
@ -686,8 +681,7 @@ export class SettingsView extends LitElement {
 | 
			
		||||
        
 | 
			
		||||
        // For other providers, use the normal flow
 | 
			
		||||
        this.saving = true;
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        const result = await ipcRenderer.invoke('settings:validate-and-save-key', { provider, key });
 | 
			
		||||
        const result = await window.api.settingsView.validateKey({ provider, key });
 | 
			
		||||
        
 | 
			
		||||
        if (result.success) {
 | 
			
		||||
            await this.refreshModelData();
 | 
			
		||||
@ -700,23 +694,24 @@ export class SettingsView extends LitElement {
 | 
			
		||||
    
 | 
			
		||||
    async handleClearKey(provider) {
 | 
			
		||||
        this.saving = true;
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        await ipcRenderer.invoke('settings:clear-api-key', { provider });
 | 
			
		||||
        await window.api.settingsView.removeApiKey(provider);
 | 
			
		||||
        this.apiKeys = { ...this.apiKeys, [provider]: '' };
 | 
			
		||||
        await this.refreshModelData();
 | 
			
		||||
        this.saving = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async refreshModelData() {
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        const result = await ipcRenderer.invoke('settings:get-model-settings');
 | 
			
		||||
        if (result.success) {
 | 
			
		||||
            const { availableLlm, availableStt, selectedModels, storedKeys } = result.data;
 | 
			
		||||
            this.availableLlmModels = availableLlm;
 | 
			
		||||
            this.availableSttModels = availableStt;
 | 
			
		||||
            this.selectedLlm = selectedModels.llm;
 | 
			
		||||
            this.selectedStt = selectedModels.stt;
 | 
			
		||||
            this.apiKeys = storedKeys;
 | 
			
		||||
        }
 | 
			
		||||
        const [availableLlm, availableStt, selected, storedKeys] = await Promise.all([
 | 
			
		||||
            window.api.settingsView.getAvailableModels({ type: 'llm' }),
 | 
			
		||||
            window.api.settingsView.getAvailableModels({ type: 'stt' }),
 | 
			
		||||
            window.api.settingsView.getSelectedModels(),
 | 
			
		||||
            window.api.settingsView.getAllKeys()
 | 
			
		||||
        ]);
 | 
			
		||||
        this.availableLlmModels = availableLlm;
 | 
			
		||||
        this.availableSttModels = availableStt;
 | 
			
		||||
        this.selectedLlm = selected.llm;
 | 
			
		||||
        this.selectedStt = selected.stt;
 | 
			
		||||
        this.apiKeys = storedKeys;
 | 
			
		||||
        this.requestUpdate();
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
@ -761,8 +756,7 @@ export class SettingsView extends LitElement {
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        this.saving = true;
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        await ipcRenderer.invoke('settings:set-selected-model', { type, modelId });
 | 
			
		||||
        await window.api.settingsView.setSelectedModel({ type, modelId });
 | 
			
		||||
        if (type === 'llm') this.selectedLlm = modelId;
 | 
			
		||||
        if (type === 'stt') this.selectedStt = modelId;
 | 
			
		||||
        this.isLlmListVisible = false;
 | 
			
		||||
@ -772,8 +766,7 @@ export class SettingsView extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    async refreshOllamaStatus() {
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        const ollamaStatus = await ipcRenderer.invoke('settings:get-ollama-status');
 | 
			
		||||
        const ollamaStatus = await window.api.settingsView.getOllamaStatus();
 | 
			
		||||
        if (ollamaStatus?.success) {
 | 
			
		||||
            this.ollamaStatus = { installed: ollamaStatus.installed, running: ollamaStatus.running };
 | 
			
		||||
            this.ollamaModels = ollamaStatus.models || [];
 | 
			
		||||
@ -817,8 +810,6 @@ export class SettingsView extends LitElement {
 | 
			
		||||
        this.requestUpdate();
 | 
			
		||||
        
 | 
			
		||||
        try {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            
 | 
			
		||||
            // Set up progress listener
 | 
			
		||||
            const progressHandler = (event, { modelId: id, progress }) => {
 | 
			
		||||
                if (id === modelId) {
 | 
			
		||||
@ -827,10 +818,10 @@ export class SettingsView extends LitElement {
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
            
 | 
			
		||||
            ipcRenderer.on('whisper:download-progress', progressHandler);
 | 
			
		||||
            window.api.settingsView.onWhisperDownloadProgress(progressHandler);
 | 
			
		||||
            
 | 
			
		||||
            // Start download
 | 
			
		||||
            const result = await ipcRenderer.invoke('whisper:download-model', modelId);
 | 
			
		||||
            const result = await window.api.settingsView.downloadWhisperModel(modelId);
 | 
			
		||||
            
 | 
			
		||||
            if (result.success) {
 | 
			
		||||
                // Auto-select the model after download
 | 
			
		||||
@ -840,7 +831,7 @@ export class SettingsView extends LitElement {
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // Cleanup
 | 
			
		||||
            ipcRenderer.removeListener('whisper:download-progress', progressHandler);
 | 
			
		||||
            window.api.settingsView.removeOnWhisperDownloadProgress(progressHandler);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error(`[SettingsView] Error downloading Whisper model ${modelId}:`, error);
 | 
			
		||||
            alert(`Error downloading ${modelId}: ${error.message}`);
 | 
			
		||||
@ -872,17 +863,12 @@ export class SettingsView extends LitElement {
 | 
			
		||||
        if (this.wasJustDragged) return
 | 
			
		||||
    
 | 
			
		||||
        console.log("Requesting Firebase authentication from main process...")
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
          window.require("electron").ipcRenderer.invoke("start-firebase-auth")
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
        window.api.settingsView.startFirebaseAuth();
 | 
			
		||||
    }
 | 
			
		||||
    //////// after_modelStateService ////////
 | 
			
		||||
 | 
			
		||||
    openShortcutEditor() {
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            ipcRenderer.invoke('open-shortcut-editor');
 | 
			
		||||
        }
 | 
			
		||||
        window.api.settingsView.openShortcutEditor();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    connectedCallback() {
 | 
			
		||||
@ -920,9 +906,7 @@ export class SettingsView extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setupIpcListeners() {
 | 
			
		||||
        if (!window.require) return;
 | 
			
		||||
        
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        if (!window.api) return;
 | 
			
		||||
        
 | 
			
		||||
        this._userStateListener = (event, userState) => {
 | 
			
		||||
            console.log('[SettingsView] Received user-state-changed:', userState);
 | 
			
		||||
@ -945,7 +929,7 @@ export class SettingsView extends LitElement {
 | 
			
		||||
        this._presetsUpdatedListener = async (event) => {
 | 
			
		||||
            console.log('[SettingsView] Received presets-updated, refreshing presets');
 | 
			
		||||
            try {
 | 
			
		||||
                const presets = await ipcRenderer.invoke('settings:getPresets');
 | 
			
		||||
                const presets = await window.api.settingsView.getPresets();
 | 
			
		||||
                this.presets = presets || [];
 | 
			
		||||
                
 | 
			
		||||
                // 현재 선택된 프리셋이 삭제되었는지 확인 (사용자 프리셋만 고려)
 | 
			
		||||
@ -964,28 +948,26 @@ export class SettingsView extends LitElement {
 | 
			
		||||
            this.shortcuts = keybinds;
 | 
			
		||||
        };
 | 
			
		||||
        
 | 
			
		||||
        ipcRenderer.on('user-state-changed', this._userStateListener);
 | 
			
		||||
        ipcRenderer.on('settings-updated', this._settingsUpdatedListener);
 | 
			
		||||
        ipcRenderer.on('presets-updated', this._presetsUpdatedListener);
 | 
			
		||||
        ipcRenderer.on('shortcuts-updated', this._shortcutListener);
 | 
			
		||||
        window.api.settingsView.onUserStateChanged(this._userStateListener);
 | 
			
		||||
        window.api.settingsView.onSettingsUpdated(this._settingsUpdatedListener);
 | 
			
		||||
        window.api.settingsView.onPresetsUpdated(this._presetsUpdatedListener);
 | 
			
		||||
        window.api.settingsView.onShortcutsUpdated(this._shortcutListener);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    cleanupIpcListeners() {
 | 
			
		||||
        if (!window.require) return;
 | 
			
		||||
        
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        if (!window.api) return;
 | 
			
		||||
        
 | 
			
		||||
        if (this._userStateListener) {
 | 
			
		||||
            ipcRenderer.removeListener('user-state-changed', this._userStateListener);
 | 
			
		||||
            window.api.settingsView.removeOnUserStateChanged(this._userStateListener);
 | 
			
		||||
        }
 | 
			
		||||
        if (this._settingsUpdatedListener) {
 | 
			
		||||
            ipcRenderer.removeListener('settings-updated', this._settingsUpdatedListener);
 | 
			
		||||
            window.api.settingsView.removeOnSettingsUpdated(this._settingsUpdatedListener);
 | 
			
		||||
        }
 | 
			
		||||
        if (this._presetsUpdatedListener) {
 | 
			
		||||
            ipcRenderer.removeListener('presets-updated', this._presetsUpdatedListener);
 | 
			
		||||
            window.api.settingsView.removeOnPresetsUpdated(this._presetsUpdatedListener);
 | 
			
		||||
        }
 | 
			
		||||
        if (this._shortcutListener) {
 | 
			
		||||
            ipcRenderer.removeListener('shortcuts-updated', this._shortcutListener);
 | 
			
		||||
            window.api.settingsView.removeOnShortcutsUpdated(this._shortcutListener);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1019,17 +1001,11 @@ export class SettingsView extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleMouseEnter = () => {
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            ipcRenderer.send('cancel-hide-settings-window');
 | 
			
		||||
        }
 | 
			
		||||
        window.api.settingsView.cancelHideSettingsWindow();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleMouseLeave = () => {
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            ipcRenderer.send('hide-settings-window');
 | 
			
		||||
        }
 | 
			
		||||
        window.api.settingsView.hideSettingsWindow();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // getMainShortcuts() {
 | 
			
		||||
@ -1079,70 +1055,76 @@ export class SettingsView extends LitElement {
 | 
			
		||||
 | 
			
		||||
    handleMoveLeft() {
 | 
			
		||||
        console.log('Move Left clicked');
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            ipcRenderer.invoke('move-window-step', 'left');
 | 
			
		||||
        }
 | 
			
		||||
        window.api.settingsView.moveWindowStep('left');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleMoveRight() {
 | 
			
		||||
        console.log('Move Right clicked');
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            ipcRenderer.invoke('move-window-step', 'right');
 | 
			
		||||
        }
 | 
			
		||||
        window.api.settingsView.moveWindowStep('right');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handlePersonalize() {
 | 
			
		||||
        console.log('Personalize clicked');
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            try {
 | 
			
		||||
                await ipcRenderer.invoke('open-login-page');
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                console.error('Failed to open personalize page:', error);
 | 
			
		||||
            }
 | 
			
		||||
        try {
 | 
			
		||||
            await window.api.settingsView.openLoginPage();
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('Failed to open personalize page:', error);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleToggleInvisibility() {
 | 
			
		||||
        console.log('Toggle Invisibility clicked');
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            this.isContentProtectionOn = await ipcRenderer.invoke('toggle-content-protection');
 | 
			
		||||
            this.requestUpdate();
 | 
			
		||||
        this.isContentProtectionOn = await window.api.settingsView.toggleContentProtection();
 | 
			
		||||
        this.requestUpdate();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleSaveApiKey() {
 | 
			
		||||
        const input = this.shadowRoot.getElementById('api-key-input');
 | 
			
		||||
        if (!input || !input.value) return;
 | 
			
		||||
 | 
			
		||||
        const newApiKey = input.value;
 | 
			
		||||
        try {
 | 
			
		||||
            const result = await window.api.settingsView.saveApiKey(newApiKey);
 | 
			
		||||
            if (result.success) {
 | 
			
		||||
                console.log('API Key saved successfully via IPC.');
 | 
			
		||||
                this.apiKey = newApiKey;
 | 
			
		||||
                this.requestUpdate();
 | 
			
		||||
            } else {
 | 
			
		||||
                 console.error('Failed to save API Key via IPC:', result.error);
 | 
			
		||||
            }
 | 
			
		||||
        } catch(e) {
 | 
			
		||||
            console.error('Error invoking save-api-key IPC:', e);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleClearApiKey() {
 | 
			
		||||
        console.log('Clear API Key clicked');
 | 
			
		||||
        await window.api.settingsView.removeApiKey();
 | 
			
		||||
        this.apiKey = null;
 | 
			
		||||
        this.requestUpdate();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleQuit() {
 | 
			
		||||
        console.log('Quit clicked');
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            ipcRenderer.invoke('quit-application');
 | 
			
		||||
        }
 | 
			
		||||
        window.api.settingsView.quitApplication();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleFirebaseLogout() {
 | 
			
		||||
        console.log('Firebase Logout clicked');
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            ipcRenderer.invoke('firebase-logout');
 | 
			
		||||
        }
 | 
			
		||||
        window.api.settingsView.firebaseLogout();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleOllamaShutdown() {
 | 
			
		||||
        console.log('[SettingsView] Shutting down Ollama service...');
 | 
			
		||||
        
 | 
			
		||||
        if (!window.require) return;
 | 
			
		||||
        
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        if (!window.api) return;
 | 
			
		||||
        
 | 
			
		||||
        try {
 | 
			
		||||
            // Show loading state
 | 
			
		||||
            this.ollamaStatus = { ...this.ollamaStatus, running: false };
 | 
			
		||||
            this.requestUpdate();
 | 
			
		||||
            
 | 
			
		||||
            const result = await ipcRenderer.invoke('settings:shutdown-ollama'); // Graceful shutdown
 | 
			
		||||
            const result = await window.api.settingsView.shutdownOllama(false); // Graceful shutdown
 | 
			
		||||
            
 | 
			
		||||
            if (result.success) {
 | 
			
		||||
                console.log('[SettingsView] Ollama shut down successfully');
 | 
			
		||||
 | 
			
		||||
@ -102,23 +102,22 @@ export class ShortcutSettingsView extends LitElement {
 | 
			
		||||
        this.feedback = {};
 | 
			
		||||
        this.isLoading = true;
 | 
			
		||||
        this.capturingKey = null;
 | 
			
		||||
        this.ipcRenderer = window.require ? window.require('electron').ipcRenderer : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    connectedCallback() {
 | 
			
		||||
        super.connectedCallback();
 | 
			
		||||
        if (!this.ipcRenderer) return;
 | 
			
		||||
        if (!window.api) return;
 | 
			
		||||
        this.loadShortcutsHandler = (event, keybinds) => {
 | 
			
		||||
            this.shortcuts = keybinds;
 | 
			
		||||
            this.isLoading = false;
 | 
			
		||||
        };
 | 
			
		||||
        this.ipcRenderer.on('load-shortcuts', this.loadShortcutsHandler);
 | 
			
		||||
        window.api.shortcutSettingsView.onLoadShortcuts(this.loadShortcutsHandler);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    disconnectedCallback() {
 | 
			
		||||
        super.disconnectedCallback();
 | 
			
		||||
        if (this.ipcRenderer && this.loadShortcutsHandler) {
 | 
			
		||||
            this.ipcRenderer.removeListener('load-shortcuts', this.loadShortcutsHandler);
 | 
			
		||||
        if (window.api && this.loadShortcutsHandler) {
 | 
			
		||||
            window.api.shortcutSettingsView.removeOnLoadShortcuts(this.loadShortcutsHandler);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -171,25 +170,25 @@ export class ShortcutSettingsView extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleSave() {
 | 
			
		||||
        if (!this.ipcRenderer) return;
 | 
			
		||||
        const result = await this.ipcRenderer.invoke('save-shortcuts', this.shortcuts);
 | 
			
		||||
        if (!window.api) return;
 | 
			
		||||
        const result = await window.api.shortcutSettingsView.saveShortcuts(this.shortcuts);
 | 
			
		||||
        if (!result.success) {
 | 
			
		||||
            alert('Failed to save shortcuts: ' + result.error);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleClose() {
 | 
			
		||||
        if (!this.ipcRenderer) return;
 | 
			
		||||
        this.ipcRenderer.send('close-shortcut-editor');
 | 
			
		||||
        if (!window.api) return;
 | 
			
		||||
        window.api.shortcutSettingsView.closeShortcutEditor();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleResetToDefault() {
 | 
			
		||||
        if (!this.ipcRenderer) return;
 | 
			
		||||
        if (!window.api) return;
 | 
			
		||||
        const confirmation = confirm("Are you sure you want to reset all shortcuts to their default values?");
 | 
			
		||||
        if (!confirmation) return;
 | 
			
		||||
    
 | 
			
		||||
        try {
 | 
			
		||||
            const defaultShortcuts = await this.ipcRenderer.invoke('get-default-shortcuts');
 | 
			
		||||
            const defaultShortcuts = await window.api.shortcutSettingsView.getDefaultShortcuts();
 | 
			
		||||
            this.shortcuts = defaultShortcuts;
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            alert('Failed to load default settings.');
 | 
			
		||||
 | 
			
		||||
@ -92,7 +92,11 @@ function createFeatureWindows(header, namesToCreate) {
 | 
			
		||||
        skipTaskbar: true,
 | 
			
		||||
        hiddenInMissionControl: true,
 | 
			
		||||
        resizable: true,
 | 
			
		||||
        webPreferences: { nodeIntegration: true, contextIsolation: false },
 | 
			
		||||
        webPreferences: {
 | 
			
		||||
            nodeIntegration: false,
 | 
			
		||||
            contextIsolation: true,
 | 
			
		||||
            preload: path.join(__dirname, '../preload.js'),
 | 
			
		||||
        },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const createFeatureWindow = (name) => {
 | 
			
		||||
@ -377,8 +381,9 @@ function createWindows() {
 | 
			
		||||
        focusable: true,
 | 
			
		||||
        acceptFirstMouse: true,
 | 
			
		||||
        webPreferences: {
 | 
			
		||||
            nodeIntegration: true,
 | 
			
		||||
            contextIsolation: false,
 | 
			
		||||
            nodeIntegration: false,
 | 
			
		||||
            contextIsolation: true,
 | 
			
		||||
            preload: path.join(__dirname, '../preload.js'),
 | 
			
		||||
            backgroundThrottling: false,
 | 
			
		||||
            webSecurity: false,
 | 
			
		||||
            enableRemoteModule: false,
 | 
			
		||||
@ -417,6 +422,21 @@ function createWindows() {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    setupIpcHandlers(movementManager);
 | 
			
		||||
    
 | 
			
		||||
    // Content protection helper functions
 | 
			
		||||
    const getContentProtectionStatus = () => isContentProtectionOn;
 | 
			
		||||
    const setContentProtection = (status) => {
 | 
			
		||||
        isContentProtectionOn = status;
 | 
			
		||||
        console.log(`[Protection] Content protection toggled to: ${isContentProtectionOn}`);
 | 
			
		||||
        windowPool.forEach(win => {
 | 
			
		||||
            if (win && !win.isDestroyed()) {
 | 
			
		||||
                win.setContentProtection(isContentProtectionOn);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    // Initialize windowBridge with required dependencies
 | 
			
		||||
    windowBridge.initialize(windowPool, require('electron').app, require('electron').shell, getCurrentDisplay, createFeatureWindows, movementManager, getContentProtectionStatus, setContentProtection, updateLayout);
 | 
			
		||||
 | 
			
		||||
    if (currentHeaderState === 'main') {
 | 
			
		||||
        createFeatureWindows(header, ['listen', 'ask', 'settings', 'shortcut-settings']);
 | 
			
		||||
@ -484,9 +504,7 @@ function loadAndRegisterShortcuts(movementManager) {
 | 
			
		||||
function setupIpcHandlers(movementManager) {
 | 
			
		||||
    setupApiKeyIPC();
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('quit-application', () => {
 | 
			
		||||
        app.quit();
 | 
			
		||||
    });
 | 
			
		||||
    // quit-application handler moved to windowBridge.js to avoid duplication
 | 
			
		||||
 | 
			
		||||
    screen.on('display-added', (event, newDisplay) => {
 | 
			
		||||
        console.log('[Display] New display added:', newDisplay.id);
 | 
			
		||||
@ -506,20 +524,7 @@ function setupIpcHandlers(movementManager) {
 | 
			
		||||
        updateLayout();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('toggle-content-protection', () => {
 | 
			
		||||
        isContentProtectionOn = !isContentProtectionOn;
 | 
			
		||||
        console.log(`[Protection] Content protection toggled to: ${isContentProtectionOn}`);
 | 
			
		||||
        windowPool.forEach(win => {
 | 
			
		||||
            if (win && !win.isDestroyed()) {
 | 
			
		||||
                win.setContentProtection(isContentProtectionOn);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        return isContentProtectionOn;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('get-content-protection-status', () => {
 | 
			
		||||
        return isContentProtectionOn;
 | 
			
		||||
    });
 | 
			
		||||
    // Content protection handlers moved to windowBridge.js to avoid duplication
 | 
			
		||||
 | 
			
		||||
    ipcMain.on('header-state-changed', (event, state) => {
 | 
			
		||||
        console.log(`[WindowManager] Header state changed to: ${state}`);
 | 
			
		||||
@ -543,16 +548,7 @@ function setupIpcHandlers(movementManager) {
 | 
			
		||||
        return { ...defaultKeybinds, ...savedKeybinds };
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('open-shortcut-editor', () => {
 | 
			
		||||
        const header = windowPool.get('header');
 | 
			
		||||
        if (!header) return;
 | 
			
		||||
        
 | 
			
		||||
        // 편집기 열기 전 모든 단축키 비활성화
 | 
			
		||||
        globalShortcut.unregisterAll();
 | 
			
		||||
        console.log('[Shortcuts] Disabled for editing.');
 | 
			
		||||
 | 
			
		||||
        createFeatureWindows(header, 'shortcut-settings');
 | 
			
		||||
    });
 | 
			
		||||
    // open-shortcut-editor handler moved to windowBridge.js to avoid duplication
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('get-default-shortcuts', () => {
 | 
			
		||||
        shortCutStore.set('customKeybinds', {});
 | 
			
		||||
@ -594,64 +590,7 @@ function setupIpcHandlers(movementManager) {
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('open-login-page', () => {
 | 
			
		||||
        const webUrl = process.env.pickleglass_WEB_URL || 'http://localhost:3000';
 | 
			
		||||
        const personalizeUrl = `${webUrl}/personalize?desktop=true`;
 | 
			
		||||
        shell.openExternal(personalizeUrl);
 | 
			
		||||
        console.log('Opening personalization page:', personalizeUrl);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('resize-header-window', (event, { width, height }) => {
 | 
			
		||||
        const header = windowPool.get('header');
 | 
			
		||||
        if (header) {
 | 
			
		||||
            console.log(`[WindowManager] Resize request: ${width}x${height}`);
 | 
			
		||||
            
 | 
			
		||||
            // Prevent resizing during animations or if already at target size
 | 
			
		||||
            if (movementManager && movementManager.isAnimating) {
 | 
			
		||||
                console.log('[WindowManager] Skipping resize during animation');
 | 
			
		||||
                return { success: false, error: 'Cannot resize during animation' };
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const currentBounds = header.getBounds();
 | 
			
		||||
            console.log(`[WindowManager] Current bounds: ${currentBounds.width}x${currentBounds.height} at (${currentBounds.x}, ${currentBounds.y})`);
 | 
			
		||||
            
 | 
			
		||||
            // Skip if already at target size to prevent unnecessary operations
 | 
			
		||||
            if (currentBounds.width === width && currentBounds.height === height) {
 | 
			
		||||
                console.log('[WindowManager] Already at target size, skipping resize');
 | 
			
		||||
                return { success: true };
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const wasResizable = header.isResizable();
 | 
			
		||||
            if (!wasResizable) {
 | 
			
		||||
                header.setResizable(true);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Calculate the center point of the current window
 | 
			
		||||
            const centerX = currentBounds.x + currentBounds.width / 2;
 | 
			
		||||
            // Calculate new X position to keep the window centered
 | 
			
		||||
            const newX = Math.round(centerX - width / 2);
 | 
			
		||||
 | 
			
		||||
            // Get the current display to ensure we stay within bounds
 | 
			
		||||
            const display = getCurrentDisplay(header);
 | 
			
		||||
            const { x: workAreaX, width: workAreaWidth } = display.workArea;
 | 
			
		||||
            
 | 
			
		||||
            // Clamp the new position to stay within display bounds
 | 
			
		||||
            const clampedX = Math.max(workAreaX, Math.min(workAreaX + workAreaWidth - width, newX));
 | 
			
		||||
 | 
			
		||||
            header.setBounds({ x: clampedX, y: currentBounds.y, width, height });
 | 
			
		||||
 | 
			
		||||
            if (!wasResizable) {
 | 
			
		||||
                header.setResizable(false);
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // Update layout after resize
 | 
			
		||||
            updateLayout();
 | 
			
		||||
            
 | 
			
		||||
            return { success: true };
 | 
			
		||||
        }
 | 
			
		||||
        return { success: false, error: 'Header window not found' };
 | 
			
		||||
    });
 | 
			
		||||
    // resize-header-window handler moved to windowBridge.js to avoid duplication
 | 
			
		||||
 | 
			
		||||
    ipcMain.on('header-animation-finished', (event, state) => {
 | 
			
		||||
        const header = windowPool.get('header');
 | 
			
		||||
@ -718,11 +657,7 @@ function setupIpcHandlers(movementManager) {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('move-window-step', (event, direction) => {
 | 
			
		||||
        if (movementManager) {
 | 
			
		||||
            movementManager.moveStep(direction);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    // move-window-step handler moved to windowBridge.js to avoid duplication
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('adjust-window-height', (event, targetHeight) => {
 | 
			
		||||
        const senderWindow = BrowserWindow.fromWebContents(event.sender);
 | 
			
		||||
@ -804,11 +739,7 @@ function setupIpcHandlers(movementManager) {
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('firebase-logout', async () => {
 | 
			
		||||
        console.log('[WindowManager] Received request to log out.');
 | 
			
		||||
        
 | 
			
		||||
        await authService.signOut();
 | 
			
		||||
    });
 | 
			
		||||
    // firebase-logout handler moved to windowBridge.js to avoid duplication
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('check-system-permissions', async () => {
 | 
			
		||||
        const { systemPreferences } = require('electron');
 | 
			
		||||
@ -947,70 +878,6 @@ function setupIpcHandlers(movementManager) {
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.on('show-settings-window', (event, bounds) => {
 | 
			
		||||
        if (!bounds) return;  
 | 
			
		||||
        const win = windowPool.get('settings');
 | 
			
		||||
 | 
			
		||||
        if (win && !win.isDestroyed()) {
 | 
			
		||||
            console.log('[WindowManager] Showing settings window');
 | 
			
		||||
            if (settingsHideTimer) {
 | 
			
		||||
                clearTimeout(settingsHideTimer);
 | 
			
		||||
                settingsHideTimer = null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Adjust position based on button bounds
 | 
			
		||||
            const header = windowPool.get('header');
 | 
			
		||||
            const headerBounds = header?.getBounds() ?? { x: 0, y: 0 };
 | 
			
		||||
            const settingsBounds = win.getBounds();
 | 
			
		||||
 | 
			
		||||
            const disp = getCurrentDisplay(header);
 | 
			
		||||
            const { x: waX, y: waY, width: waW, height: waH } = disp.workArea;
 | 
			
		||||
 | 
			
		||||
            let x = Math.round(headerBounds.x + (bounds?.x ?? 0) + (bounds?.width ?? 0) / 2 - settingsBounds.width / 2);
 | 
			
		||||
            let y = Math.round(headerBounds.y + (bounds?.y ?? 0) + (bounds?.height ?? 0) + 31);
 | 
			
		||||
 | 
			
		||||
            x = Math.max(waX + 10, Math.min(waX + waW - settingsBounds.width - 10, x));
 | 
			
		||||
            y = Math.max(waY + 10, Math.min(waY + waH - settingsBounds.height - 10, y));
 | 
			
		||||
 | 
			
		||||
            win.setBounds({ x, y });
 | 
			
		||||
            win.__lockedByButton = true;
 | 
			
		||||
            console.log(`[WindowManager] Positioning settings window at (${x}, ${y}) based on button bounds.`);
 | 
			
		||||
            
 | 
			
		||||
            win.show();
 | 
			
		||||
            win.moveTop();
 | 
			
		||||
            win.setAlwaysOnTop(true);
 | 
			
		||||
            console.log('[WindowManager] Settings window shown');
 | 
			
		||||
        } else {
 | 
			
		||||
            console.log('[WindowManager] Settings window not found');
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.on('hide-settings-window', (event) => {
 | 
			
		||||
        const window = windowPool.get("settings");
 | 
			
		||||
        if (window && !window.isDestroyed()) {
 | 
			
		||||
            if (settingsHideTimer) {
 | 
			
		||||
                clearTimeout(settingsHideTimer);
 | 
			
		||||
            }
 | 
			
		||||
            settingsHideTimer = setTimeout(() => {
 | 
			
		||||
                if (window && !window.isDestroyed()) {
 | 
			
		||||
                    window.setAlwaysOnTop(false);
 | 
			
		||||
                    window.hide();
 | 
			
		||||
                }
 | 
			
		||||
                settingsHideTimer = null;
 | 
			
		||||
            }, 200);
 | 
			
		||||
            
 | 
			
		||||
            window.__lockedByButton = false;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.on('cancel-hide-settings-window', (event) => {
 | 
			
		||||
        if (settingsHideTimer) {
 | 
			
		||||
            clearTimeout(settingsHideTimer);
 | 
			
		||||
            settingsHideTimer = null;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('ask:closeAskWindow', async () => {
 | 
			
		||||
        const askWindow = windowPool.get('ask');
 | 
			
		||||
@ -1252,13 +1119,13 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementMan
 | 
			
		||||
        console.log('[Shortcuts] Broadcasted updated shortcuts to all windows.');
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // ✨ 하드코딩된 단축키 등록을 위해 변수 유지
 | 
			
		||||
    // 하드코딩된 단축키 등록을 위해 변수 유지
 | 
			
		||||
    const isMac = process.platform === 'darwin';
 | 
			
		||||
    const modifier = isMac ? 'Cmd' : 'Ctrl';
 | 
			
		||||
    const header = windowPool.get('header');
 | 
			
		||||
    const state = header?.currentHeaderState || currentHeaderState;
 | 
			
		||||
 | 
			
		||||
    // ✨ 기능 1: 사용자가 설정할 수 없는 '모니터 이동' 단축키 (기존 로직 유지)
 | 
			
		||||
    // 기능 1: 사용자가 설정할 수 없는 '모니터 이동' 단축키 (기존 로직 유지)
 | 
			
		||||
    const displays = screen.getAllDisplays();
 | 
			
		||||
    if (displays.length > 1) {
 | 
			
		||||
        displays.forEach((display, index) => {
 | 
			
		||||
@ -1285,7 +1152,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementMan
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // ✨ 기능 2: 사용자가 설정할 수 없는 '화면 가장자리 이동' 단축키 (기존 로직 유지)
 | 
			
		||||
    // 기능 2: 사용자가 설정할 수 없는 '화면 가장자리 이동' 단축키 (기존 로직 유지)
 | 
			
		||||
    const edgeDirections = [
 | 
			
		||||
        { key: `${modifier}+Shift+Left`, direction: 'left' },
 | 
			
		||||
        { key: `${modifier}+Shift+Right`, direction: 'right' },
 | 
			
		||||
@ -1303,7 +1170,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementMan
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // ✨ 기능 3: 사용자가 설정 가능한 모든 단축키를 동적으로 등록 (새로운 방식 적용)
 | 
			
		||||
    // 기능 3: 사용자가 설정 가능한 모든 단축키를 동적으로 등록 (새로운 방식 적용)
 | 
			
		||||
    for (const action in keybinds) {
 | 
			
		||||
        const accelerator = keybinds[action];
 | 
			
		||||
        if (!accelerator) continue;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user