rough refactor done
This commit is contained in:
		
							parent
							
								
									586d44e57b
								
							
						
					
					
						commit
						d936af46a3
					
				
							
								
								
									
										2
									
								
								aec
									
									
									
									
									
								
							
							
								
								
								
								
								
								
									
									
								
							
						
						
									
										2
									
								
								aec
									
									
									
									
									
								
							@ -1 +1 @@
 | 
			
		||||
Subproject commit 9e11f4f95707714464194bdfc9db0222ec5c6163
 | 
			
		||||
Subproject commit f00bb1fb948053c752b916adfee19f90644a0b2f
 | 
			
		||||
@ -9,6 +9,7 @@ const shortcutsService = require('../features/shortcuts/shortcutsService');
 | 
			
		||||
 | 
			
		||||
const askService = require('../features/ask/askService');
 | 
			
		||||
const listenService = require('../features/listen/listenService');
 | 
			
		||||
const permissionService = require('../features/common/services/permissionService');
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
  // Renderer로부터의 요청을 수신
 | 
			
		||||
@ -33,6 +34,14 @@ module.exports = {
 | 
			
		||||
    ipcMain.handle('save-shortcuts', async (event, newKeybinds) => await shortcutsService.handleSaveShortcuts(newKeybinds));
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // Permissions
 | 
			
		||||
    ipcMain.handle('check-system-permissions', async () => await permissionService.checkSystemPermissions());
 | 
			
		||||
    ipcMain.handle('request-microphone-permission', async () => await permissionService.requestMicrophonePermission());
 | 
			
		||||
    ipcMain.handle('open-system-preferences', async (event, section) => await permissionService.openSystemPreferences(section));
 | 
			
		||||
    ipcMain.handle('mark-permissions-completed', async () => await permissionService.markPermissionsAsCompleted());
 | 
			
		||||
    ipcMain.handle('check-permissions-completed', async () => await permissionService.checkPermissionsCompleted());
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    // User/Auth
 | 
			
		||||
    ipcMain.handle('get-current-user', () => authService.getCurrentUser());
 | 
			
		||||
    ipcMain.handle('start-firebase-auth', async () => await authService.startFirebaseAuthFlow());
 | 
			
		||||
@ -67,7 +76,6 @@ module.exports = {
 | 
			
		||||
    ipcMain.handle('ask:sendQuestionFromAsk', async (event, userPrompt) => await askService.sendMessage(userPrompt));
 | 
			
		||||
    ipcMain.handle('ask:sendQuestionFromSummary', async (event, userPrompt) => await askService.sendMessage(userPrompt));
 | 
			
		||||
    ipcMain.handle('ask:toggleAskButton', async () => await askService.toggleAskButton());
 | 
			
		||||
    ipcMain.handle('stop-screen-capture', async () => askService.handleStopScreenCapture());
 | 
			
		||||
 | 
			
		||||
    // Listen
 | 
			
		||||
    ipcMain.handle('listen:sendMicAudio', async (event, { data, mimeType }) => await listenService.handleSendMicAudioContent(data, mimeType));
 | 
			
		||||
@ -98,12 +106,11 @@ module.exports = {
 | 
			
		||||
    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: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());
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
// src/bridge/windowBridge.js
 | 
			
		||||
const { ipcMain } = require('electron');
 | 
			
		||||
const { ipcMain, BrowserWindow } = require('electron');
 | 
			
		||||
const windowManager = require('../window/windowManager');
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
@ -15,6 +15,16 @@ module.exports = {
 | 
			
		||||
    ipcMain.handle('move-window-step', (event, direction) => windowManager.moveWindowStep(direction));
 | 
			
		||||
    ipcMain.on('close-shortcut-editor', () => windowManager.closeWindow('shortcut-settings'));
 | 
			
		||||
 | 
			
		||||
    // Newly moved handlers from windowManager
 | 
			
		||||
    ipcMain.on('header-state-changed', (event, state) => windowManager.handleHeaderStateChanged(state));
 | 
			
		||||
    ipcMain.on('header-animation-finished', (event, state) => windowManager.handleHeaderAnimationFinished(state));
 | 
			
		||||
    ipcMain.handle('get-header-position', () => windowManager.getHeaderPosition());
 | 
			
		||||
    ipcMain.handle('move-header', (event, newX, newY) => windowManager.moveHeader(newX, newY));
 | 
			
		||||
    ipcMain.handle('move-header-to', (event, newX, newY) => windowManager.moveHeaderTo(newX, newY));
 | 
			
		||||
    ipcMain.handle('adjust-window-height', (event, targetHeight) => windowManager.adjustWindowHeight(event.sender, targetHeight));
 | 
			
		||||
    ipcMain.handle('toggle-all-windows-visibility', () => windowManager.toggleAllWindowsVisibility());
 | 
			
		||||
    ipcMain.on('animation-finished', (event) => windowManager.handleAnimationFinished(event.sender));
 | 
			
		||||
    ipcMain.handle('ask:closeAskWindow', () => windowManager.closeAskWindow());
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  notifyFocusChange(win, isFocused) {
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,18 @@
 | 
			
		||||
const { BrowserWindow } = require('electron');
 | 
			
		||||
const { createStreamingLLM } = require('../common/ai/factory');
 | 
			
		||||
const { getCurrentModelInfo, windowPool, updateLayout } = require('../../window/windowManager');
 | 
			
		||||
// Lazy require helper to avoid circular dependency issues
 | 
			
		||||
const getWindowManager = () => require('../../window/windowManager');
 | 
			
		||||
 | 
			
		||||
const getWindowPool = () => {
 | 
			
		||||
    try {
 | 
			
		||||
        return getWindowManager().windowPool;
 | 
			
		||||
    } catch {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
const updateLayout = () => getWindowManager().updateLayout();
 | 
			
		||||
const ensureAskWindowVisible = () => getWindowManager().ensureAskWindowVisible();
 | 
			
		||||
 | 
			
		||||
const sessionRepository = require('../common/repositories/session');
 | 
			
		||||
const askRepository = require('./repositories');
 | 
			
		||||
const { getSystemPrompt } = require('../common/prompts/promptBuilder');
 | 
			
		||||
@ -10,6 +22,7 @@ const os = require('os');
 | 
			
		||||
const util = require('util');
 | 
			
		||||
const execFile = util.promisify(require('child_process').execFile);
 | 
			
		||||
const { desktopCapturer } = require('electron');
 | 
			
		||||
const modelStateService = require('../common/services/modelStateService');
 | 
			
		||||
 | 
			
		||||
// Try to load sharp, but don't fail if it's not available
 | 
			
		||||
let sharp;
 | 
			
		||||
@ -126,33 +139,33 @@ class AskService {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _broadcastState() {
 | 
			
		||||
        const askWindow = windowPool.get('ask');
 | 
			
		||||
        const askWindow = getWindowPool()?.get('ask');
 | 
			
		||||
        if (askWindow && !askWindow.isDestroyed()) {
 | 
			
		||||
            askWindow.webContents.send('ask:stateUpdate', this.state);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async toggleAskButton() {
 | 
			
		||||
        const askWindow = windowPool.get('ask');
 | 
			
		||||
        const askWindow = getWindowPool()?.get('ask');
 | 
			
		||||
 | 
			
		||||
        // 답변이 있거나 스트리밍 중일 때
 | 
			
		||||
        const hasContent = this.state.isStreaming || (this.state.currentResponse && this.state.currentResponse.length > 0);
 | 
			
		||||
 | 
			
		||||
        if (askWindow.isVisible() && hasContent) {
 | 
			
		||||
        if (askWindow && askWindow.isVisible() && hasContent) {
 | 
			
		||||
            // 창을 닫는 대신, 텍스트 입력창만 토글합니다.
 | 
			
		||||
            this.state.showTextInput = !this.state.showTextInput;
 | 
			
		||||
            this._broadcastState(); // 변경된 상태 전파
 | 
			
		||||
        } else {
 | 
			
		||||
            // 기존의 창 보이기/숨기기 로직
 | 
			
		||||
            if (askWindow.isVisible()) {
 | 
			
		||||
            if (askWindow && askWindow.isVisible()) {
 | 
			
		||||
                askWindow.webContents.send('window-hide-animation');
 | 
			
		||||
                this.state.isVisible = false;
 | 
			
		||||
            } else {
 | 
			
		||||
                console.log('[AskService] Showing hidden Ask window');
 | 
			
		||||
                this.state.isVisible = true;
 | 
			
		||||
                askWindow.show();
 | 
			
		||||
                askWindow?.show();
 | 
			
		||||
                updateLayout();
 | 
			
		||||
                askWindow.webContents.send('window-show-animation');
 | 
			
		||||
                askWindow?.webContents.send('window-show-animation');
 | 
			
		||||
            }
 | 
			
		||||
            // 창이 다시 열릴 때를 대비해 상태를 초기화하고 전파합니다.
 | 
			
		||||
            if (this.state.isVisible) {
 | 
			
		||||
@ -182,6 +195,8 @@ class AskService {
 | 
			
		||||
     * @returns {Promise<{success: boolean, response?: string, error?: string}>}
 | 
			
		||||
     */
 | 
			
		||||
    async sendMessage(userPrompt, conversationHistoryRaw=[]) {
 | 
			
		||||
        ensureAskWindowVisible();
 | 
			
		||||
 | 
			
		||||
        if (this.abortController) {
 | 
			
		||||
            this.abortController.abort('New request received.');
 | 
			
		||||
        }
 | 
			
		||||
@ -212,7 +227,7 @@ class AskService {
 | 
			
		||||
            await askRepository.addAiMessage({ sessionId, role: 'user', content: userPrompt.trim() });
 | 
			
		||||
            console.log(`[AskService] DB: Saved user prompt to session ${sessionId}`);
 | 
			
		||||
            
 | 
			
		||||
            const modelInfo = await getCurrentModelInfo(null, { type: 'llm' });
 | 
			
		||||
            const modelInfo = modelStateService.getCurrentModelInfo('llm');
 | 
			
		||||
            if (!modelInfo || !modelInfo.apiKey) {
 | 
			
		||||
                throw new Error('AI model or API key not configured.');
 | 
			
		||||
            }
 | 
			
		||||
@ -252,7 +267,7 @@ class AskService {
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const response = await streamingLLM.streamChat(messages);
 | 
			
		||||
            const askWin = windowPool.get('ask');
 | 
			
		||||
            const askWin = getWindowPool()?.get('ask');
 | 
			
		||||
 | 
			
		||||
            if (!askWin || askWin.isDestroyed()) {
 | 
			
		||||
                console.error("[AskService] Ask window is not available to send stream to.");
 | 
			
		||||
@ -351,11 +366,6 @@ class AskService {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleStopScreenCapture() {
 | 
			
		||||
        lastScreenshot = null;
 | 
			
		||||
        console.log('[AskService] Stopped screen capture and cleared cache.');
 | 
			
		||||
        return { success: true };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const askService = new AskService();
 | 
			
		||||
 | 
			
		||||
@ -82,7 +82,7 @@ async function createSTT({ apiKey, language = 'en', callbacks = {}, usePortkey =
 | 
			
		||||
            silence_duration_ms: 100,
 | 
			
		||||
          },
 | 
			
		||||
          input_audio_noise_reduction: {
 | 
			
		||||
            type: 'far_field'
 | 
			
		||||
            type: 'near_field'
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
@ -359,6 +359,7 @@ class ModelStateService {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    removeApiKey(provider) {
 | 
			
		||||
        console.log(`[ModelStateService] Removing API key for provider: ${provider}`);
 | 
			
		||||
        if (provider in this.state.apiKeys) {
 | 
			
		||||
            this.state.apiKeys[provider] = null;
 | 
			
		||||
            const llmProvider = this.getProviderForModel('llm', this.state.selectedModels.llm);
 | 
			
		||||
@ -542,6 +543,7 @@ class ModelStateService {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleRemoveApiKey(provider) {
 | 
			
		||||
        console.log(`[ModelStateService] handleRemoveApiKey: ${provider}`);
 | 
			
		||||
        const success = this.removeApiKey(provider);
 | 
			
		||||
        if (success) {
 | 
			
		||||
            const selectedModels = this.getSelectedModels();
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										119
									
								
								src/features/common/services/permissionService.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								src/features/common/services/permissionService.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,119 @@
 | 
			
		||||
const { systemPreferences, shell, desktopCapturer } = require('electron');
 | 
			
		||||
const permissionRepository = require('../repositories/permission');
 | 
			
		||||
 | 
			
		||||
class PermissionService {
 | 
			
		||||
  async checkSystemPermissions() {
 | 
			
		||||
    const permissions = {
 | 
			
		||||
      microphone: 'unknown',
 | 
			
		||||
      screen: 'unknown',
 | 
			
		||||
      needsSetup: true
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      if (process.platform === 'darwin') {
 | 
			
		||||
        const micStatus = systemPreferences.getMediaAccessStatus('microphone');
 | 
			
		||||
        console.log('[Permissions] Microphone status:', micStatus);
 | 
			
		||||
        permissions.microphone = micStatus;
 | 
			
		||||
 | 
			
		||||
        const screenStatus = systemPreferences.getMediaAccessStatus('screen');
 | 
			
		||||
        console.log('[Permissions] Screen status:', screenStatus);
 | 
			
		||||
        permissions.screen = screenStatus;
 | 
			
		||||
 | 
			
		||||
        permissions.needsSetup = micStatus !== 'granted' || screenStatus !== 'granted';
 | 
			
		||||
      } else {
 | 
			
		||||
        permissions.microphone = 'granted';
 | 
			
		||||
        permissions.screen = 'granted';
 | 
			
		||||
        permissions.needsSetup = false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      console.log('[Permissions] System permissions status:', permissions);
 | 
			
		||||
      return permissions;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('[Permissions] Error checking permissions:', error);
 | 
			
		||||
      return {
 | 
			
		||||
        microphone: 'unknown',
 | 
			
		||||
        screen: 'unknown',
 | 
			
		||||
        needsSetup: true,
 | 
			
		||||
        error: error.message
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async requestMicrophonePermission() {
 | 
			
		||||
    if (process.platform !== 'darwin') {
 | 
			
		||||
      return { success: true };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const status = systemPreferences.getMediaAccessStatus('microphone');
 | 
			
		||||
      console.log('[Permissions] Microphone status:', status);
 | 
			
		||||
      if (status === 'granted') {
 | 
			
		||||
        return { success: true, status: 'granted' };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const granted = await systemPreferences.askForMediaAccess('microphone');
 | 
			
		||||
      return {
 | 
			
		||||
        success: granted,
 | 
			
		||||
        status: granted ? 'granted' : 'denied'
 | 
			
		||||
      };
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('[Permissions] Error requesting microphone permission:', error);
 | 
			
		||||
      return {
 | 
			
		||||
        success: false,
 | 
			
		||||
        error: error.message
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async openSystemPreferences(section) {
 | 
			
		||||
    if (process.platform !== 'darwin') {
 | 
			
		||||
      return { success: false, error: 'Not supported on this platform' };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      if (section === 'screen-recording') {
 | 
			
		||||
        try {
 | 
			
		||||
          console.log('[Permissions] Triggering screen capture request to register app...');
 | 
			
		||||
          await desktopCapturer.getSources({
 | 
			
		||||
            types: ['screen'],
 | 
			
		||||
            thumbnailSize: { width: 1, height: 1 }
 | 
			
		||||
          });
 | 
			
		||||
          console.log('[Permissions] App registered for screen recording');
 | 
			
		||||
        } catch (captureError) {
 | 
			
		||||
          console.log('[Permissions] Screen capture request triggered (expected to fail):', captureError.message);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture');
 | 
			
		||||
      }
 | 
			
		||||
      return { success: true };
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('[Permissions] Error opening system preferences:', error);
 | 
			
		||||
      return { success: false, error: error.message };
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async markPermissionsAsCompleted() {
 | 
			
		||||
    try {
 | 
			
		||||
      await permissionRepository.markPermissionsAsCompleted();
 | 
			
		||||
      console.log('[Permissions] Marked permissions as completed');
 | 
			
		||||
      return { success: true };
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('[Permissions] Error marking permissions as completed:', error);
 | 
			
		||||
      return { success: false, error: error.message };
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async checkPermissionsCompleted() {
 | 
			
		||||
    try {
 | 
			
		||||
      const completed = await permissionRepository.checkPermissionsCompleted();
 | 
			
		||||
      console.log('[Permissions] Permissions completed status:', completed);
 | 
			
		||||
      return completed;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('[Permissions] Error checking permissions completed status:', error);
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const permissionService = new PermissionService();
 | 
			
		||||
module.exports = permissionService; 
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
const { BrowserWindow } = require('electron');
 | 
			
		||||
const { spawn } = require('child_process');
 | 
			
		||||
const { createSTT } = require('../../common/ai/factory');
 | 
			
		||||
const modelStateService = require('../../common/services/modelStateService');
 | 
			
		||||
// const { getStoredApiKey, getStoredProvider, getCurrentModelInfo } = require('../../../window/windowManager');
 | 
			
		||||
 | 
			
		||||
const COMPLETION_DEBOUNCE_MS = 2000;
 | 
			
		||||
@ -131,8 +132,7 @@ class SttService {
 | 
			
		||||
    async initializeSttSessions(language = 'en') {
 | 
			
		||||
        const effectiveLanguage = process.env.OPENAI_TRANSCRIBE_LANG || language || 'en';
 | 
			
		||||
 | 
			
		||||
        const { getCurrentModelInfo } = require('../../../window/windowManager');
 | 
			
		||||
        const modelInfo = await getCurrentModelInfo(null, { type: 'stt' });
 | 
			
		||||
        const modelInfo = modelStateService.getCurrentModelInfo('stt');
 | 
			
		||||
        if (!modelInfo || !modelInfo.apiKey) {
 | 
			
		||||
            throw new Error('AI model or API key is not configured.');
 | 
			
		||||
        }
 | 
			
		||||
@ -144,6 +144,7 @@ class SttService {
 | 
			
		||||
                console.log('[SttService] Ignoring message - session already closed');
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            console.log('[SttService] handleMyMessage', message);
 | 
			
		||||
            
 | 
			
		||||
            if (this.modelInfo.provider === 'whisper') {
 | 
			
		||||
                // Whisper STT emits 'transcription' events with different structure
 | 
			
		||||
@ -411,8 +412,7 @@ class SttService {
 | 
			
		||||
        let modelInfo = this.modelInfo;
 | 
			
		||||
        if (!modelInfo) {
 | 
			
		||||
            console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...');
 | 
			
		||||
            const { getCurrentModelInfo } = require('../../../window/windowManager');
 | 
			
		||||
            modelInfo = await getCurrentModelInfo(null, { type: 'stt' });
 | 
			
		||||
            modelInfo = modelStateService.getCurrentModelInfo('stt');
 | 
			
		||||
        }
 | 
			
		||||
        if (!modelInfo) {
 | 
			
		||||
            throw new Error('STT model info could not be retrieved.');
 | 
			
		||||
@ -433,8 +433,7 @@ class SttService {
 | 
			
		||||
        let modelInfo = this.modelInfo;
 | 
			
		||||
        if (!modelInfo) {
 | 
			
		||||
            console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...');
 | 
			
		||||
            const { getCurrentModelInfo } = require('../../../window/windowManager');
 | 
			
		||||
            modelInfo = await getCurrentModelInfo(null, { type: 'stt' });
 | 
			
		||||
            modelInfo = modelStateService.getCurrentModelInfo('stt');
 | 
			
		||||
        }
 | 
			
		||||
        if (!modelInfo) {
 | 
			
		||||
            throw new Error('STT model info could not be retrieved.');
 | 
			
		||||
@ -515,8 +514,7 @@ class SttService {
 | 
			
		||||
        let modelInfo = this.modelInfo;
 | 
			
		||||
        if (!modelInfo) {
 | 
			
		||||
            console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...');
 | 
			
		||||
            const { getCurrentModelInfo } = require('../../../window/windowManager');
 | 
			
		||||
            modelInfo = await getCurrentModelInfo(null, { type: 'stt' });
 | 
			
		||||
            modelInfo = modelStateService.getCurrentModelInfo('stt');
 | 
			
		||||
        }
 | 
			
		||||
        if (!modelInfo) {
 | 
			
		||||
            throw new Error('STT model info could not be retrieved.');
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ const { getSystemPrompt } = require('../../common/prompts/promptBuilder.js');
 | 
			
		||||
const { createLLM } = require('../../common/ai/factory');
 | 
			
		||||
const sessionRepository = require('../../common/repositories/session');
 | 
			
		||||
const summaryRepository = require('./repositories');
 | 
			
		||||
const modelStateService = require('../../common/services/modelStateService');
 | 
			
		||||
// const { getStoredApiKey, getStoredProvider, getCurrentModelInfo } = require('../../../window/windowManager.js');
 | 
			
		||||
 | 
			
		||||
class SummaryService {
 | 
			
		||||
@ -97,8 +98,7 @@ Please build upon this context while analyzing the new conversation segments.
 | 
			
		||||
                await sessionRepository.touch(this.currentSessionId);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const { getCurrentModelInfo } = require('../../../window/windowManager');
 | 
			
		||||
            const modelInfo = await getCurrentModelInfo(null, { type: 'llm' });
 | 
			
		||||
            const modelInfo = modelStateService.getCurrentModelInfo('llm');
 | 
			
		||||
            if (!modelInfo || !modelInfo.apiKey) {
 | 
			
		||||
                throw new Error('AI model or API key is not configured.');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -374,6 +374,7 @@ async function removeApiKey() {
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        console.log('[SettingsService] API key removed for all providers');
 | 
			
		||||
        return { success: true };
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        console.error('[SettingsService] Error removing API key:', error);
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
const { globalShortcut, screen } = require('electron');
 | 
			
		||||
const shortcutsRepository = require('./repositories');
 | 
			
		||||
const internalBridge = require('../../bridge/internalBridge');
 | 
			
		||||
const askService = require('../ask/askService');
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ShortcutsService {
 | 
			
		||||
@ -210,8 +211,7 @@ class ShortcutsService {
 | 
			
		||||
                    callback = () => this.toggleAllWindowsVisibility(this.windowPool);
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'nextStep':
 | 
			
		||||
                    // Late require to prevent circular dependency
 | 
			
		||||
                    callback = () => require('../../window/windowManager').toggleFeature('ask', {ask: { targetVisibility: 'show' }});
 | 
			
		||||
                    callback = () => askService.toggleAskButton();
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'scrollUp':
 | 
			
		||||
                    callback = () => {
 | 
			
		||||
 | 
			
		||||
@ -276,12 +276,6 @@ contextBridge.exposeInMainWorld('api', {
 | 
			
		||||
    startMacosSystemAudio: () => ipcRenderer.invoke('listen:startMacosSystemAudio'),
 | 
			
		||||
    stopMacosSystemAudio: () => ipcRenderer.invoke('listen:stopMacosSystemAudio'),
 | 
			
		||||
    
 | 
			
		||||
    // 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'),
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
@ -38,13 +38,10 @@ const isMacOS = window.api.platform.isMacOS;
 | 
			
		||||
 | 
			
		||||
let mediaStream = null;
 | 
			
		||||
let micMediaStream = null;
 | 
			
		||||
let screenshotInterval = null;
 | 
			
		||||
let audioContext = null;
 | 
			
		||||
let audioProcessor = null;
 | 
			
		||||
let systemAudioContext = null;
 | 
			
		||||
let systemAudioProcessor = null;
 | 
			
		||||
let currentImageQuality = 'medium';
 | 
			
		||||
let lastScreenshotBase64 = null;
 | 
			
		||||
 | 
			
		||||
let systemAudioBuffer = [];
 | 
			
		||||
const MAX_SYSTEM_BUFFER_SIZE = 10;
 | 
			
		||||
@ -140,10 +137,6 @@ function runAecSync(micF32, sysF32) {
 | 
			
		||||
        return micF32;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼
 | 
			
		||||
    //                  새로운 프레임 단위 처리 로직
 | 
			
		||||
    // ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼
 | 
			
		||||
 | 
			
		||||
    const frameSize = 160; // AEC 모듈 초기화 시 설정한 프레임 크기
 | 
			
		||||
    const numFrames = Math.floor(micF32.length / frameSize);
 | 
			
		||||
 | 
			
		||||
@ -418,94 +411,10 @@ function setupSystemAudioProcessing(systemStream) {
 | 
			
		||||
    return { context: systemAudioContext, processor: systemProcessor };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ---------------------------
 | 
			
		||||
// Screenshot functions (exact from renderer.js)
 | 
			
		||||
// ---------------------------
 | 
			
		||||
async function captureScreenshot(imageQuality = 'medium', isManual = false) {
 | 
			
		||||
    console.log(`Capturing ${isManual ? 'manual' : 'automated'} screenshot...`);
 | 
			
		||||
 | 
			
		||||
    // Check rate limiting for automated screenshots only
 | 
			
		||||
    if (!isManual && tokenTracker.shouldThrottle()) {
 | 
			
		||||
        console.log('Automated screenshot skipped due to rate limiting');
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        // Request screenshot from main process
 | 
			
		||||
        const result = await window.api.listenCapture.captureScreenshot({
 | 
			
		||||
            quality: imageQuality,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (result.success && result.base64) {
 | 
			
		||||
            // Store the latest screenshot
 | 
			
		||||
            lastScreenshotBase64 = result.base64;
 | 
			
		||||
 | 
			
		||||
            // Note: sendResult is not defined in the original, this was likely an error
 | 
			
		||||
            // Commenting out this section as it references undefined variable
 | 
			
		||||
            /*
 | 
			
		||||
            if (sendResult.success) {
 | 
			
		||||
                // Track image tokens after successful send
 | 
			
		||||
                const imageTokens = tokenTracker.calculateImageTokens(result.width || 1920, result.height || 1080);
 | 
			
		||||
                tokenTracker.addTokens(imageTokens, 'image');
 | 
			
		||||
                console.log(`📊 Image sent successfully - ${imageTokens} tokens used (${result.width}x${result.height})`);
 | 
			
		||||
            } else {
 | 
			
		||||
                console.error('Failed to send image:', sendResult.error);
 | 
			
		||||
            }
 | 
			
		||||
            */
 | 
			
		||||
        } else {
 | 
			
		||||
            console.error('Failed to capture screenshot:', result.error);
 | 
			
		||||
        }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        console.error('Error capturing screenshot:', error);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function captureManualScreenshot(imageQuality = null) {
 | 
			
		||||
    console.log('Manual screenshot triggered');
 | 
			
		||||
    const quality = imageQuality || currentImageQuality;
 | 
			
		||||
    await captureScreenshot(quality, true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getCurrentScreenshot() {
 | 
			
		||||
    try {
 | 
			
		||||
        // First try to get a fresh screenshot from main process
 | 
			
		||||
        const result = await window.api.listenCapture.getCurrentScreenshot();
 | 
			
		||||
 | 
			
		||||
        if (result.success && result.base64) {
 | 
			
		||||
            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 window.api.listenCapture.captureScreenshot({
 | 
			
		||||
            quality: currentImageQuality,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (captureResult.success && captureResult.base64) {
 | 
			
		||||
            lastScreenshotBase64 = captureResult.base64;
 | 
			
		||||
            return captureResult.base64;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Fallback to last stored screenshot
 | 
			
		||||
        if (lastScreenshotBase64) {
 | 
			
		||||
            console.log('Using cached screenshot');
 | 
			
		||||
            return lastScreenshotBase64;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        throw new Error('Failed to get screenshot');
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        console.error('Error getting current screenshot:', error);
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ---------------------------
 | 
			
		||||
// Main capture functions (exact from renderer.js)
 | 
			
		||||
// ---------------------------
 | 
			
		||||
async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'medium') {
 | 
			
		||||
    // Store the image quality for manual screenshots
 | 
			
		||||
    currentImageQuality = imageQuality;
 | 
			
		||||
 | 
			
		||||
    // Reset token tracker when starting new capture session
 | 
			
		||||
    tokenTracker.reset();
 | 
			
		||||
@ -534,13 +443,6 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Initialize screen capture in main process
 | 
			
		||||
            const screenResult = await window.api.listenCapture.startScreenCapture();
 | 
			
		||||
            if (!screenResult.success) {
 | 
			
		||||
                throw new Error('Failed to start screen capture: ' + screenResult.error);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                micMediaStream = await navigator.mediaDevices.getUserMedia({
 | 
			
		||||
                    audio: {
 | 
			
		||||
@ -602,12 +504,6 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
 | 
			
		||||
            // Windows - capture mic and system audio separately using native loopback
 | 
			
		||||
            console.log('Starting Windows capture with native loopback audio...');
 | 
			
		||||
 | 
			
		||||
            // Start screen capture in main process for screenshots
 | 
			
		||||
            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 window.api.listenCapture.isSessionActive();
 | 
			
		||||
            if (!sessionActive) {
 | 
			
		||||
@ -656,20 +552,6 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
 | 
			
		||||
                // Continue without system audio
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Start capturing screenshots - check if manual mode
 | 
			
		||||
        if (screenshotIntervalSeconds === 'manual' || screenshotIntervalSeconds === 'Manual') {
 | 
			
		||||
            console.log('Manual mode enabled - screenshots will be captured on demand only');
 | 
			
		||||
            // Don't start automatic capture in manual mode
 | 
			
		||||
        } else {
 | 
			
		||||
            // 스크린샷 기능 활성화 (chatModel에서 사용)
 | 
			
		||||
            const intervalMilliseconds = parseInt(screenshotIntervalSeconds) * 1000;
 | 
			
		||||
            screenshotInterval = setInterval(() => captureScreenshot(imageQuality), intervalMilliseconds);
 | 
			
		||||
 | 
			
		||||
            // Capture first screenshot immediately
 | 
			
		||||
            setTimeout(() => captureScreenshot(imageQuality), 100);
 | 
			
		||||
            console.log(`📸 Screenshot capture enabled with ${screenshotIntervalSeconds}s interval`);
 | 
			
		||||
        }
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
        console.error('Error starting capture:', err);
 | 
			
		||||
        // Note: pickleGlass.e() is not available in this context, commenting out
 | 
			
		||||
@ -678,11 +560,6 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function stopCapture() {
 | 
			
		||||
    if (screenshotInterval) {
 | 
			
		||||
        clearInterval(screenshotInterval);
 | 
			
		||||
        screenshotInterval = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Clean up microphone resources
 | 
			
		||||
    if (audioProcessor) {
 | 
			
		||||
        audioProcessor.disconnect();
 | 
			
		||||
@ -713,11 +590,6 @@ function stopCapture() {
 | 
			
		||||
        micMediaStream = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Stop screen capture in main process
 | 
			
		||||
    window.api.listenCapture.stopScreenCapture().catch(err => {
 | 
			
		||||
        console.error('Error stopping screen capture:', err);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Stop macOS audio capture if running
 | 
			
		||||
    if (isMacOS) {
 | 
			
		||||
        window.api.listenCapture.stopMacosSystemAudio().catch(err => {
 | 
			
		||||
@ -735,19 +607,14 @@ module.exports = {
 | 
			
		||||
    disposeAec,      // 필요시 Rust 객체 파괴
 | 
			
		||||
    startCapture,
 | 
			
		||||
    stopCapture,
 | 
			
		||||
    captureManualScreenshot,
 | 
			
		||||
    getCurrentScreenshot,
 | 
			
		||||
    isLinux,
 | 
			
		||||
    isMacOS,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Expose functions to global scope for external access (exact from renderer.js)
 | 
			
		||||
if (typeof window !== 'undefined') {
 | 
			
		||||
    window.captureManualScreenshot = captureManualScreenshot;
 | 
			
		||||
    window.listenCapture = module.exports;
 | 
			
		||||
    window.pickleGlass = window.pickleGlass || {};
 | 
			
		||||
    window.pickleGlass.startCapture = startCapture;
 | 
			
		||||
    window.pickleGlass.stopCapture = stopCapture;
 | 
			
		||||
    window.pickleGlass.captureManualScreenshot = captureManualScreenshot;
 | 
			
		||||
    window.pickleGlass.getCurrentScreenshot = getCurrentScreenshot;
 | 
			
		||||
} 
 | 
			
		||||
@ -693,6 +693,7 @@ export class SettingsView extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    async handleClearKey(provider) {
 | 
			
		||||
        console.log(`[SettingsView] handleClearKey: ${provider}`);
 | 
			
		||||
        this.saving = true;
 | 
			
		||||
        await window.api.settingsView.removeApiKey(provider);
 | 
			
		||||
        this.apiKeys = { ...this.apiKeys, [provider]: '' };
 | 
			
		||||
@ -1097,13 +1098,6 @@ export class SettingsView extends LitElement {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleClearApiKey() {
 | 
			
		||||
        console.log('Clear API Key clicked');
 | 
			
		||||
        await window.api.settingsView.removeApiKey();
 | 
			
		||||
        this.apiKey = null;
 | 
			
		||||
        this.requestUpdate();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleQuit() {
 | 
			
		||||
        console.log('Quit clicked');
 | 
			
		||||
        window.api.settingsView.quitApplication();
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
const { BrowserWindow, globalShortcut, ipcMain, screen, app, shell, desktopCapturer } = require('electron');
 | 
			
		||||
const { BrowserWindow, globalShortcut, screen, app, shell } = require('electron');
 | 
			
		||||
const WindowLayoutManager = require('./windowLayoutManager');
 | 
			
		||||
const SmoothMovementManager = require('./smoothMovementManager');
 | 
			
		||||
const path = require('node:path');
 | 
			
		||||
@ -570,10 +570,7 @@ function createWindows() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setupIpcHandlers(movementManager) {
 | 
			
		||||
    setupApiKeyIPC();
 | 
			
		||||
 | 
			
		||||
    // quit-application handler moved to windowBridge.js to avoid duplication
 | 
			
		||||
 | 
			
		||||
    screen.on('display-added', (event, newDisplay) => {
 | 
			
		||||
        console.log('[Display] New display added:', newDisplay.id);
 | 
			
		||||
    });
 | 
			
		||||
@ -591,387 +588,146 @@ function setupIpcHandlers(movementManager) {
 | 
			
		||||
        // console.log('[Display] Display metrics changed:', display.id, changedMetrics);
 | 
			
		||||
        updateLayout();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 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}`);
 | 
			
		||||
        currentHeaderState = state;
 | 
			
		||||
 | 
			
		||||
        if (state === 'main') {
 | 
			
		||||
            createFeatureWindows(windowPool.get('header'));
 | 
			
		||||
        } else {         // 'apikey' | 'permission'
 | 
			
		||||
            destroyFeatureWindows();
 | 
			
		||||
        }
 | 
			
		||||
        internalBridge.emit('reregister-shortcuts');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // resize-header-window handler moved to windowBridge.js to avoid duplication
 | 
			
		||||
 | 
			
		||||
    ipcMain.on('header-animation-finished', (event, state) => {
 | 
			
		||||
        const header = windowPool.get('header');
 | 
			
		||||
        if (!header || header.isDestroyed()) return;
 | 
			
		||||
    
 | 
			
		||||
        if (state === 'hidden') {
 | 
			
		||||
            header.hide();
 | 
			
		||||
            console.log('[WindowManager] Header hidden after animation.');
 | 
			
		||||
        } else if (state === 'visible') {
 | 
			
		||||
            console.log('[WindowManager] Header shown after animation.');
 | 
			
		||||
            updateLayout();
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('get-header-position', () => {
 | 
			
		||||
        const header = windowPool.get('header');
 | 
			
		||||
        if (header) {
 | 
			
		||||
            const [x, y] = header.getPosition();
 | 
			
		||||
            return { x, y };
 | 
			
		||||
        }
 | 
			
		||||
        return { x: 0, y: 0 };
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('move-header', (event, newX, newY) => {
 | 
			
		||||
        const header = windowPool.get('header');
 | 
			
		||||
        if (header) {
 | 
			
		||||
            const currentY = newY !== undefined ? newY : header.getBounds().y;
 | 
			
		||||
            header.setPosition(newX, currentY, false);
 | 
			
		||||
 | 
			
		||||
            updateLayout();
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('move-header-to', (event, newX, newY) => {
 | 
			
		||||
        const header = windowPool.get('header');
 | 
			
		||||
        if (header) {
 | 
			
		||||
            const targetDisplay = screen.getDisplayNearestPoint({ x: newX, y: newY });
 | 
			
		||||
            const { x: workAreaX, y: workAreaY, width, height } = targetDisplay.workArea;
 | 
			
		||||
            const headerBounds = header.getBounds();
 | 
			
		||||
 | 
			
		||||
            // Only clamp if the new position would actually go out of bounds
 | 
			
		||||
            // This prevents progressive restriction of movement
 | 
			
		||||
            let clampedX = newX;
 | 
			
		||||
            let clampedY = newY;
 | 
			
		||||
            
 | 
			
		||||
            // Check if we need to clamp X position
 | 
			
		||||
            if (newX < workAreaX) {
 | 
			
		||||
                clampedX = workAreaX;
 | 
			
		||||
            } else if (newX + headerBounds.width > workAreaX + width) {
 | 
			
		||||
                clampedX = workAreaX + width - headerBounds.width;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // Check if we need to clamp Y position  
 | 
			
		||||
            if (newY < workAreaY) {
 | 
			
		||||
                clampedY = workAreaY;
 | 
			
		||||
            } else if (newY + headerBounds.height > workAreaY + height) {
 | 
			
		||||
                clampedY = workAreaY + height - headerBounds.height;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            header.setPosition(clampedX, clampedY, false);
 | 
			
		||||
 | 
			
		||||
            updateLayout();
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // move-window-step handler moved to windowBridge.js to avoid duplication
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('adjust-window-height', (event, targetHeight) => {
 | 
			
		||||
        const senderWindow = BrowserWindow.fromWebContents(event.sender);
 | 
			
		||||
        if (senderWindow) {
 | 
			
		||||
            const wasResizable = senderWindow.isResizable();
 | 
			
		||||
            if (!wasResizable) {
 | 
			
		||||
                senderWindow.setResizable(true);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const currentBounds = senderWindow.getBounds();
 | 
			
		||||
            const minHeight = senderWindow.getMinimumSize()[1];
 | 
			
		||||
            const maxHeight = senderWindow.getMaximumSize()[1];
 | 
			
		||||
            
 | 
			
		||||
            let adjustedHeight;
 | 
			
		||||
            if (maxHeight === 0) {
 | 
			
		||||
                adjustedHeight = Math.max(minHeight, targetHeight);
 | 
			
		||||
            } else {
 | 
			
		||||
                adjustedHeight = Math.max(minHeight, Math.min(maxHeight, targetHeight));
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            senderWindow.setSize(currentBounds.width, adjustedHeight, false);
 | 
			
		||||
 | 
			
		||||
            if (!wasResizable) {
 | 
			
		||||
                senderWindow.setResizable(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            updateLayout();
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('check-system-permissions', async () => {
 | 
			
		||||
        const { systemPreferences } = require('electron');
 | 
			
		||||
        const permissions = {
 | 
			
		||||
            microphone: 'unknown',
 | 
			
		||||
            screen: 'unknown',
 | 
			
		||||
            needsSetup: true
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            if (process.platform === 'darwin') {
 | 
			
		||||
                // Check microphone permission on macOS
 | 
			
		||||
                const micStatus = systemPreferences.getMediaAccessStatus('microphone');
 | 
			
		||||
                console.log('[Permissions] Microphone status:', micStatus);
 | 
			
		||||
                permissions.microphone = micStatus;
 | 
			
		||||
 | 
			
		||||
                // Check screen recording permission using the system API
 | 
			
		||||
                const screenStatus = systemPreferences.getMediaAccessStatus('screen');
 | 
			
		||||
                console.log('[Permissions] Screen status:', screenStatus);
 | 
			
		||||
                permissions.screen = screenStatus;
 | 
			
		||||
 | 
			
		||||
                permissions.needsSetup = micStatus !== 'granted' || screenStatus !== 'granted';
 | 
			
		||||
            } else {
 | 
			
		||||
                permissions.microphone = 'granted';
 | 
			
		||||
                permissions.screen = 'granted';
 | 
			
		||||
                permissions.needsSetup = false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            console.log('[Permissions] System permissions status:', permissions);
 | 
			
		||||
            return permissions;
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[Permissions] Error checking permissions:', error);
 | 
			
		||||
            return {
 | 
			
		||||
                microphone: 'unknown',
 | 
			
		||||
                screen: 'unknown',
 | 
			
		||||
                needsSetup: true,
 | 
			
		||||
                error: error.message
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('request-microphone-permission', async () => {
 | 
			
		||||
        if (process.platform !== 'darwin') {
 | 
			
		||||
            return { success: true };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const { systemPreferences } = require('electron');
 | 
			
		||||
        try {
 | 
			
		||||
            const status = systemPreferences.getMediaAccessStatus('microphone');
 | 
			
		||||
            console.log('[Permissions] Microphone status:', status);
 | 
			
		||||
            if (status === 'granted') {
 | 
			
		||||
                return { success: true, status: 'granted' };
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Req mic permission
 | 
			
		||||
            const granted = await systemPreferences.askForMediaAccess('microphone');
 | 
			
		||||
            return { 
 | 
			
		||||
                success: granted, 
 | 
			
		||||
                status: granted ? 'granted' : 'denied'
 | 
			
		||||
            };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[Permissions] Error requesting microphone permission:', error);
 | 
			
		||||
            return { 
 | 
			
		||||
                success: false, 
 | 
			
		||||
                error: error.message 
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('open-system-preferences', async (event, section) => {
 | 
			
		||||
        if (process.platform !== 'darwin') {
 | 
			
		||||
            return { success: false, error: 'Not supported on this platform' };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            if (section === 'screen-recording') {
 | 
			
		||||
                // First trigger screen capture request to register the app in system preferences
 | 
			
		||||
                try {
 | 
			
		||||
                    console.log('[Permissions] Triggering screen capture request to register app...');
 | 
			
		||||
                    await desktopCapturer.getSources({ 
 | 
			
		||||
                        types: ['screen'], 
 | 
			
		||||
                        thumbnailSize: { width: 1, height: 1 } 
 | 
			
		||||
                    });
 | 
			
		||||
                    console.log('[Permissions] App registered for screen recording');
 | 
			
		||||
                } catch (captureError) {
 | 
			
		||||
                    console.log('[Permissions] Screen capture request triggered (expected to fail):', captureError.message);
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                // Then open system preferences
 | 
			
		||||
                // await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture');
 | 
			
		||||
            }
 | 
			
		||||
            // if (section === 'microphone') {
 | 
			
		||||
            //     await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone');
 | 
			
		||||
            // }
 | 
			
		||||
            return { success: true };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[Permissions] Error opening system preferences:', error);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('mark-permissions-completed', async () => {
 | 
			
		||||
        try {
 | 
			
		||||
            // This is a system-level setting, not user-specific.
 | 
			
		||||
            await permissionRepository.markPermissionsAsCompleted();
 | 
			
		||||
            console.log('[Permissions] Marked permissions as completed');
 | 
			
		||||
            return { success: true };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[Permissions] Error marking permissions as completed:', error);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('check-permissions-completed', async () => {
 | 
			
		||||
        try {
 | 
			
		||||
            const completed = await permissionRepository.checkPermissionsCompleted();
 | 
			
		||||
            console.log('[Permissions] Permissions completed status:', completed);
 | 
			
		||||
            return completed;
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[Permissions] Error checking permissions completed status:', error);
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    ipcMain.handle('toggle-all-windows-visibility', () => toggleAllWindowsVisibility());
 | 
			
		||||
 | 
			
		||||
    // ipcMain.handle('toggle-feature', async (event, featureName) => {
 | 
			
		||||
    //     return toggleFeature(featureName);
 | 
			
		||||
    // });
 | 
			
		||||
 | 
			
		||||
    ipcMain.on('animation-finished', (event) => {
 | 
			
		||||
        const win = BrowserWindow.fromWebContents(event.sender);
 | 
			
		||||
        if (win && !win.isDestroyed()) {
 | 
			
		||||
            console.log(`[WindowManager] Hiding window after animation.`);
 | 
			
		||||
            win.hide();
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('ask:closeAskWindow', async () => {
 | 
			
		||||
        const askWindow = windowPool.get('ask');
 | 
			
		||||
        if (askWindow) {
 | 
			
		||||
            askWindow.webContents.send('window-hide-animation');
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const handleHeaderStateChanged = (state) => {
 | 
			
		||||
    console.log(`[WindowManager] Header state changed to: ${state}`);
 | 
			
		||||
    currentHeaderState = state;
 | 
			
		||||
 | 
			
		||||
// /**
 | 
			
		||||
//  * 
 | 
			
		||||
//  * @param {'listen'|'ask'|'settings'} featureName
 | 
			
		||||
//  * @param {{
 | 
			
		||||
// *   listen?:   { targetVisibility?: 'show'|'hide' },
 | 
			
		||||
// *   ask?:      { targetVisibility?: 'show'|'hide', questionText?: string },
 | 
			
		||||
// *   settings?: { targetVisibility?: 'show'|'hide' }
 | 
			
		||||
// * }} [options={}]
 | 
			
		||||
// */
 | 
			
		||||
// async function toggleFeature(featureName, options = {}) {
 | 
			
		||||
//     if (!windowPool.get(featureName) && currentHeaderState === 'main') {
 | 
			
		||||
//         createFeatureWindows(windowPool.get('header'));
 | 
			
		||||
//     }
 | 
			
		||||
 | 
			
		||||
//     if (featureName === 'ask') {
 | 
			
		||||
//         let askWindow = windowPool.get('ask');
 | 
			
		||||
 | 
			
		||||
//         if (!askWindow || askWindow.isDestroyed()) {
 | 
			
		||||
//             console.log('[WindowManager] Ask window not found, creating new one');
 | 
			
		||||
//             return;
 | 
			
		||||
//         }
 | 
			
		||||
 | 
			
		||||
//         const questionText = options?.ask?.questionText ?? null;
 | 
			
		||||
//         const targetVisibility = options?.ask?.targetVisibility ?? null;
 | 
			
		||||
//         if (askWindow.isVisible()) {
 | 
			
		||||
//             if (questionText) {
 | 
			
		||||
//                 askWindow.webContents.send('ask:sendQuestionToRenderer', questionText);
 | 
			
		||||
//             } else {
 | 
			
		||||
//                 updateLayout();
 | 
			
		||||
//                 if (targetVisibility === 'show') {
 | 
			
		||||
//                     askWindow.webContents.send('ask:showTextInput');
 | 
			
		||||
//                 } else {
 | 
			
		||||
//                     askWindow.webContents.send('window-hide-animation');
 | 
			
		||||
//                 }
 | 
			
		||||
//             }
 | 
			
		||||
//         } else {
 | 
			
		||||
//             console.log('[WindowManager] Showing hidden Ask window');
 | 
			
		||||
//             askWindow.show();
 | 
			
		||||
//             updateLayout();
 | 
			
		||||
//             if (questionText) {
 | 
			
		||||
//                 askWindow.webContents.send('ask:sendQuestionToRenderer', questionText);
 | 
			
		||||
//             }
 | 
			
		||||
//             askWindow.webContents.send('window-show-animation');
 | 
			
		||||
//         }
 | 
			
		||||
//     }
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
async function toggleFeature(featureName, options = {}) {
 | 
			
		||||
    if (!windowPool.get(featureName) && currentHeaderState === 'main') {
 | 
			
		||||
    if (state === 'main') {
 | 
			
		||||
        createFeatureWindows(windowPool.get('header'));
 | 
			
		||||
    } else {         // 'apikey' | 'permission'
 | 
			
		||||
        destroyFeatureWindows();
 | 
			
		||||
    }
 | 
			
		||||
    internalBridge.emit('reregister-shortcuts');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
    if (featureName === 'ask') {
 | 
			
		||||
        let askWindow = windowPool.get('ask');
 | 
			
		||||
const handleHeaderAnimationFinished = (state) => {
 | 
			
		||||
    const header = windowPool.get('header');
 | 
			
		||||
    if (!header || header.isDestroyed()) return;
 | 
			
		||||
 | 
			
		||||
        if (!askWindow || askWindow.isDestroyed()) {
 | 
			
		||||
            console.log('[WindowManager] Ask window not found, creating new one');
 | 
			
		||||
            return;
 | 
			
		||||
    if (state === 'hidden') {
 | 
			
		||||
        header.hide();
 | 
			
		||||
        console.log('[WindowManager] Header hidden after animation.');
 | 
			
		||||
    } else if (state === 'visible') {
 | 
			
		||||
        console.log('[WindowManager] Header shown after animation.');
 | 
			
		||||
        updateLayout();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getHeaderPosition = () => {
 | 
			
		||||
    const header = windowPool.get('header');
 | 
			
		||||
    if (header) {
 | 
			
		||||
        const [x, y] = header.getPosition();
 | 
			
		||||
        return { x, y };
 | 
			
		||||
    }
 | 
			
		||||
    return { x: 0, y: 0 };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const moveHeader = (newX, newY) => {
 | 
			
		||||
    const header = windowPool.get('header');
 | 
			
		||||
    if (header) {
 | 
			
		||||
        const currentY = newY !== undefined ? newY : header.getBounds().y;
 | 
			
		||||
        header.setPosition(newX, currentY, false);
 | 
			
		||||
        updateLayout();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const moveHeaderTo = (newX, newY) => {
 | 
			
		||||
    const header = windowPool.get('header');
 | 
			
		||||
    if (header) {
 | 
			
		||||
        const targetDisplay = screen.getDisplayNearestPoint({ x: newX, y: newY });
 | 
			
		||||
        const { x: workAreaX, y: workAreaY, width, height } = targetDisplay.workArea;
 | 
			
		||||
        const headerBounds = header.getBounds();
 | 
			
		||||
 | 
			
		||||
        let clampedX = newX;
 | 
			
		||||
        let clampedY = newY;
 | 
			
		||||
        
 | 
			
		||||
        if (newX < workAreaX) {
 | 
			
		||||
            clampedX = workAreaX;
 | 
			
		||||
        } else if (newX + headerBounds.width > workAreaX + width) {
 | 
			
		||||
            clampedX = workAreaX + width - headerBounds.width;
 | 
			
		||||
        }
 | 
			
		||||
        if (askWindow.isVisible()) {
 | 
			
		||||
            askWindow.webContents.send('ask:showTextInput');
 | 
			
		||||
        
 | 
			
		||||
        if (newY < workAreaY) {
 | 
			
		||||
            clampedY = workAreaY;
 | 
			
		||||
        } else if (newY + headerBounds.height > workAreaY + height) {
 | 
			
		||||
            clampedY = workAreaY + height - headerBounds.height;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        header.setPosition(clampedX, clampedY, false);
 | 
			
		||||
        updateLayout();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const adjustWindowHeight = (sender, targetHeight) => {
 | 
			
		||||
    const senderWindow = BrowserWindow.fromWebContents(sender);
 | 
			
		||||
    if (senderWindow) {
 | 
			
		||||
        const wasResizable = senderWindow.isResizable();
 | 
			
		||||
        if (!wasResizable) {
 | 
			
		||||
            senderWindow.setResizable(true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const currentBounds = senderWindow.getBounds();
 | 
			
		||||
        const minHeight = senderWindow.getMinimumSize()[1];
 | 
			
		||||
        const maxHeight = senderWindow.getMaximumSize()[1];
 | 
			
		||||
        
 | 
			
		||||
        let adjustedHeight;
 | 
			
		||||
        if (maxHeight === 0) {
 | 
			
		||||
            adjustedHeight = Math.max(minHeight, targetHeight);
 | 
			
		||||
        } else {
 | 
			
		||||
            console.log('[WindowManager] Showing hidden Ask window');
 | 
			
		||||
            askWindow.show();
 | 
			
		||||
            updateLayout();
 | 
			
		||||
            askWindow.webContents.send('window-show-animation');
 | 
			
		||||
            adjustedHeight = Math.max(minHeight, Math.min(maxHeight, targetHeight));
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        senderWindow.setSize(currentBounds.width, adjustedHeight, false);
 | 
			
		||||
 | 
			
		||||
        if (!wasResizable) {
 | 
			
		||||
            senderWindow.setResizable(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        updateLayout();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleAnimationFinished = (sender) => {
 | 
			
		||||
    const win = BrowserWindow.fromWebContents(sender);
 | 
			
		||||
    if (win && !win.isDestroyed()) {
 | 
			
		||||
        console.log(`[WindowManager] Hiding window after animation.`);
 | 
			
		||||
        win.hide();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const closeAskWindow = () => {
 | 
			
		||||
    const askWindow = windowPool.get('ask');
 | 
			
		||||
    if (askWindow) {
 | 
			
		||||
        askWindow.webContents.send('window-hide-animation');
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
async function ensureAskWindowVisible() {
 | 
			
		||||
    if (currentHeaderState !== 'main') {
 | 
			
		||||
        console.log('[WindowManager] Not in main state, skipping ensureAskWindowVisible');
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let askWindow = windowPool.get('ask');
 | 
			
		||||
 | 
			
		||||
    if (!askWindow || askWindow.isDestroyed()) {
 | 
			
		||||
        console.log('[WindowManager] Ask window not found, creating new one');
 | 
			
		||||
        createFeatureWindows(windowPool.get('header'), 'ask');
 | 
			
		||||
        askWindow = windowPool.get('ask');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!askWindow.isVisible()) {
 | 
			
		||||
        console.log('[WindowManager] Showing hidden Ask window');
 | 
			
		||||
        askWindow.show();
 | 
			
		||||
        updateLayout();
 | 
			
		||||
        askWindow.webContents.send('window-show-animation');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
//////// after_modelStateService ////////
 | 
			
		||||
async function getStoredApiKey() {
 | 
			
		||||
    if (global.modelStateService) {
 | 
			
		||||
        const provider = await getStoredProvider();
 | 
			
		||||
        return global.modelStateService.getApiKey(provider);
 | 
			
		||||
    }
 | 
			
		||||
    return null; // Fallback
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getStoredProvider() {
 | 
			
		||||
    if (global.modelStateService) {
 | 
			
		||||
        return global.modelStateService.getCurrentProvider('llm');
 | 
			
		||||
    }
 | 
			
		||||
    return 'openai'; // Fallback
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 
 | 
			
		||||
 * @param {IpcMainInvokeEvent} event 
 | 
			
		||||
 * @param {{type: 'llm' | 'stt'}}
 | 
			
		||||
 */
 | 
			
		||||
async function getCurrentModelInfo(event, { type }) {
 | 
			
		||||
    if (global.modelStateService && (type === 'llm' || type === 'stt')) {
 | 
			
		||||
        return global.modelStateService.getCurrentModelInfo(type);
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setupApiKeyIPC() {
 | 
			
		||||
    const { ipcMain } = require('electron');
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('get-stored-api-key', getStoredApiKey);
 | 
			
		||||
    ipcMain.handle('get-ai-provider', getStoredProvider);
 | 
			
		||||
    ipcMain.handle('get-current-model-info', getCurrentModelInfo);
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('api-key-validated', async (event, data) => {
 | 
			
		||||
        console.warn("[DEPRECATED] 'api-key-validated' IPC was called. This logic is now handled by 'model:validate-key'.");
 | 
			
		||||
        return { success: true };
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('remove-api-key', async () => {
 | 
			
		||||
         console.warn("[DEPRECATED] 'remove-api-key' IPC was called. This is now handled by 'model:remove-api-key'.");
 | 
			
		||||
        return { success: true };
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    console.log('[WindowManager] API key related IPC handlers have been updated for ModelStateService.');
 | 
			
		||||
}
 | 
			
		||||
//////// after_modelStateService ////////
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const closeWindow = (windowName) => {
 | 
			
		||||
    const win = windowPool.get(windowName);
 | 
			
		||||
@ -985,10 +741,6 @@ module.exports = {
 | 
			
		||||
    createWindows,
 | 
			
		||||
    windowPool,
 | 
			
		||||
    fixedYPosition,
 | 
			
		||||
    getStoredApiKey,
 | 
			
		||||
    getStoredProvider,
 | 
			
		||||
    getCurrentModelInfo,
 | 
			
		||||
    toggleFeature, // Export toggleFeature so shortcutsService can use it
 | 
			
		||||
    toggleContentProtection,
 | 
			
		||||
    resizeHeaderWindow,
 | 
			
		||||
    getContentProtectionStatus,
 | 
			
		||||
@ -999,4 +751,14 @@ module.exports = {
 | 
			
		||||
    openLoginPage,
 | 
			
		||||
    moveWindowStep,
 | 
			
		||||
    closeWindow,
 | 
			
		||||
    toggleAllWindowsVisibility,
 | 
			
		||||
    handleHeaderStateChanged,
 | 
			
		||||
    handleHeaderAnimationFinished,
 | 
			
		||||
    getHeaderPosition,
 | 
			
		||||
    moveHeader,
 | 
			
		||||
    moveHeaderTo,
 | 
			
		||||
    adjustWindowHeight,
 | 
			
		||||
    handleAnimationFinished,
 | 
			
		||||
    closeAskWindow,
 | 
			
		||||
    ensureAskWindowVisible,
 | 
			
		||||
};
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user