Merge branch 'main' into pr-107
This commit is contained in:
		
						commit
						6ec45e138f
					
				
							
								
								
									
										2
									
								
								.github/workflows/assign-on-comment.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/assign-on-comment.yml
									
									
									
									
										vendored
									
									
								
							@ -8,7 +8,7 @@ jobs:
 | 
			
		||||
  # Job 1: Any contributor can self-assign
 | 
			
		||||
  self-assign:
 | 
			
		||||
    # Only run if the comment is exactly '/assign'
 | 
			
		||||
    if: github.event.comment.body == '/assign'
 | 
			
		||||
    if: startsWith(github.event.comment.body, '/assign') && !contains(github.event.comment.body, '@')
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    permissions:
 | 
			
		||||
      issues: write
 | 
			
		||||
 | 
			
		||||
@ -46,8 +46,11 @@ win:
 | 
			
		||||
        - target: portable
 | 
			
		||||
          arch: x64
 | 
			
		||||
    requestedExecutionLevel: asInvoker
 | 
			
		||||
    # Disable code signing to avoid symbolic link issues on Windows
 | 
			
		||||
    signAndEditExecutable: false
 | 
			
		||||
    signAndEditExecutable: true
 | 
			
		||||
    cscLink: build\certs\glass-dev.pfx
 | 
			
		||||
    cscKeyPassword: "${env.CSC_KEY_PASSWORD}"
 | 
			
		||||
    signtoolOptions:
 | 
			
		||||
      certificateSubjectName: "Glass Dev Code Signing"
 | 
			
		||||
 | 
			
		||||
# NSIS installer configuration for Windows
 | 
			
		||||
nsis:
 | 
			
		||||
 | 
			
		||||
@ -1,87 +0,0 @@
 | 
			
		||||
const { FusesPlugin } = require('@electron-forge/plugin-fuses');
 | 
			
		||||
const { FuseV1Options, FuseVersion } = require('@electron/fuses');
 | 
			
		||||
const { notarizeApp } = require('./notarize');
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    packagerConfig: {
 | 
			
		||||
        asar: {
 | 
			
		||||
            unpack: '**/*.node,**/*.dylib,' + '**/node_modules/{sharp,@img}/**/*',
 | 
			
		||||
        },
 | 
			
		||||
        extraResource: ['./src/assets/SystemAudioDump', './pickleglass_web/out'],
 | 
			
		||||
        name: 'Glass',
 | 
			
		||||
        icon: 'src/assets/logo',
 | 
			
		||||
        appBundleId: 'com.pickle.glass',
 | 
			
		||||
        arch: 'universal',
 | 
			
		||||
        protocols: [
 | 
			
		||||
            {
 | 
			
		||||
                name: 'PickleGlass Protocol',
 | 
			
		||||
                schemes: ['pickleglass'],
 | 
			
		||||
            },
 | 
			
		||||
        ],
 | 
			
		||||
        asarUnpack: [
 | 
			
		||||
            '**/*.node',
 | 
			
		||||
            '**/*.dylib',
 | 
			
		||||
            'node_modules/@img/sharp-darwin-x64/**',
 | 
			
		||||
            'node_modules/@img/sharp-libvips-darwin-x64/**',
 | 
			
		||||
            'node_modules/@img/sharp-darwin-arm64/**',
 | 
			
		||||
            'node_modules/@img/sharp-libvips-darwin-arm64/**',
 | 
			
		||||
        ],
 | 
			
		||||
        osxSign: {
 | 
			
		||||
            identity: process.env.APPLE_SIGNING_IDENTITY,
 | 
			
		||||
            'hardened-runtime': true,
 | 
			
		||||
            entitlements: 'entitlements.plist',
 | 
			
		||||
            'entitlements-inherit': 'entitlements.plist',
 | 
			
		||||
        },
 | 
			
		||||
        osxNotarize: {
 | 
			
		||||
            tool: 'notarytool',
 | 
			
		||||
            appleId: process.env.APPLE_ID,
 | 
			
		||||
            appleIdPassword: process.env.APPLE_ID_PASSWORD,
 | 
			
		||||
            teamId: process.env.APPLE_TEAM_ID,
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    rebuildConfig: {},
 | 
			
		||||
    makers: [
 | 
			
		||||
        {
 | 
			
		||||
            name: '@electron-forge/maker-squirrel',
 | 
			
		||||
            config: {
 | 
			
		||||
                name: 'pickle-glass',
 | 
			
		||||
                productName: 'Glass',
 | 
			
		||||
                shortcutName: 'Glass',
 | 
			
		||||
                createDesktopShortcut: true,
 | 
			
		||||
                createStartMenuShortcut: true,
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            name: '@electron-forge/maker-dmg',
 | 
			
		||||
            platforms: ['darwin'],
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            name: '@electron-forge/maker-deb',
 | 
			
		||||
            config: {},
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            name: '@electron-forge/maker-rpm',
 | 
			
		||||
            config: {},
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
    hooks: {
 | 
			
		||||
        afterSign: async (context, forgeConfig, platform, arch, appPath) => {
 | 
			
		||||
            await notarizeApp(context, forgeConfig, platform, arch, appPath);
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    plugins: [
 | 
			
		||||
        {
 | 
			
		||||
            name: '@electron-forge/plugin-auto-unpack-natives',
 | 
			
		||||
            config: {},
 | 
			
		||||
        },
 | 
			
		||||
        new FusesPlugin({
 | 
			
		||||
            version: FuseVersion.V1,
 | 
			
		||||
            [FuseV1Options.RunAsNode]: false,
 | 
			
		||||
            [FuseV1Options.EnableCookieEncryption]: true,
 | 
			
		||||
            [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
 | 
			
		||||
            [FuseV1Options.EnableNodeCliInspectArguments]: false,
 | 
			
		||||
            [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
 | 
			
		||||
            [FuseV1Options.OnlyLoadAppFromAsar]: false,
 | 
			
		||||
        }),
 | 
			
		||||
    ],
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										2389
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2389
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										14
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								package.json
									
									
									
									
									
								
							@ -1,15 +1,13 @@
 | 
			
		||||
{
 | 
			
		||||
    "name": "pickle-glass",
 | 
			
		||||
    "productName": "Glass",
 | 
			
		||||
 | 
			
		||||
    "version": "0.2.4",
 | 
			
		||||
 | 
			
		||||
    "description": "Cl*ely for Free",
 | 
			
		||||
    "main": "src/index.js",
 | 
			
		||||
    "scripts": {
 | 
			
		||||
        "setup": "npm install && cd pickleglass_web && npm install && npm run build && cd .. && npm start",
 | 
			
		||||
        "start": "npm run build:renderer && electron-forge start",
 | 
			
		||||
        "package": "npm run build:renderer && electron-forge package",
 | 
			
		||||
        "start": "npm run build:renderer && electron .",
 | 
			
		||||
        "package": "npm run build:all && electron-builder --dir",
 | 
			
		||||
        "make": "npm run build:renderer && electron-forge make",
 | 
			
		||||
        "build": "npm run build:all && electron-builder --config electron-builder.yml --publish never",
 | 
			
		||||
        "build:win": "npm run build:all && electron-builder --win --x64 --publish never",
 | 
			
		||||
@ -58,14 +56,6 @@
 | 
			
		||||
        "ws": "^8.18.0"
 | 
			
		||||
    },
 | 
			
		||||
    "devDependencies": {
 | 
			
		||||
        "@electron-forge/cli": "^7.8.1",
 | 
			
		||||
        "@electron-forge/maker-deb": "^7.8.1",
 | 
			
		||||
        "@electron-forge/maker-dmg": "^7.8.1",
 | 
			
		||||
        "@electron-forge/maker-rpm": "^7.8.1",
 | 
			
		||||
        "@electron-forge/maker-squirrel": "^7.8.1",
 | 
			
		||||
        "@electron-forge/maker-zip": "^7.8.1",
 | 
			
		||||
        "@electron-forge/plugin-auto-unpack-natives": "^7.8.1",
 | 
			
		||||
        "@electron-forge/plugin-fuses": "^7.8.1",
 | 
			
		||||
        "@electron/fuses": "^1.8.0",
 | 
			
		||||
        "@electron/notarize": "^2.5.0",
 | 
			
		||||
        "electron": "^30.5.1",
 | 
			
		||||
 | 
			
		||||
@ -43,9 +43,10 @@ export default function LoginPage() {
 | 
			
		||||
            
 | 
			
		||||
            window.location.href = deepLinkUrl
 | 
			
		||||
            
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
              alert('Login completed. Please return to Pickle Glass app.')
 | 
			
		||||
            }, 1000)
 | 
			
		||||
            // Maybe we don't need this
 | 
			
		||||
            // setTimeout(() => {
 | 
			
		||||
            //   alert('Login completed. Please return to Pickle Glass app.')
 | 
			
		||||
            // }, 1000)
 | 
			
		||||
            
 | 
			
		||||
          } catch (error) {
 | 
			
		||||
            console.error('❌ Deep link processing failed:', error)
 | 
			
		||||
 | 
			
		||||
@ -26,14 +26,13 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
  static styles = css`
 | 
			
		||||
        :host {
 | 
			
		||||
          display: block;
 | 
			
		||||
            transform: translate3d(0, 0, 0);
 | 
			
		||||
            backface-visibility: hidden;
 | 
			
		||||
            transition: opacity 0.25s ease-out;
 | 
			
		||||
          transition: opacity 0.3s ease-in, transform 0.3s ease-in;
 | 
			
		||||
          will-change: opacity, transform;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host(.sliding-out) {
 | 
			
		||||
            animation: slideOutUp 0.3s ease-in forwards;
 | 
			
		||||
            will-change: opacity, transform;
 | 
			
		||||
            opacity: 0;
 | 
			
		||||
            transform: translateY(-20px);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host(.hidden) {
 | 
			
		||||
@ -41,17 +40,6 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
            pointer-events: none;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @keyframes slideOutUp {
 | 
			
		||||
            from {
 | 
			
		||||
                opacity: 1;
 | 
			
		||||
                transform: translateY(0);
 | 
			
		||||
            }
 | 
			
		||||
            to {
 | 
			
		||||
                opacity: 0;
 | 
			
		||||
                transform: translateY(-20px);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        * {
 | 
			
		||||
            font-family: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
 | 
			
		||||
            cursor: default;
 | 
			
		||||
@ -60,6 +48,7 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .container {
 | 
			
		||||
            -webkit-app-region: drag;
 | 
			
		||||
            width: 350px;
 | 
			
		||||
            min-height: 260px;
 | 
			
		||||
            padding: 18px 20px;
 | 
			
		||||
@ -89,6 +78,7 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .close-button {
 | 
			
		||||
            -webkit-app-region: no-drag;
 | 
			
		||||
            position: absolute;
 | 
			
		||||
            top: 10px;
 | 
			
		||||
            right: 10px;
 | 
			
		||||
@ -168,6 +158,7 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .api-input {
 | 
			
		||||
            -webkit-app-region: no-drag;
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            height: 34px;
 | 
			
		||||
            background: rgba(255, 255, 255, 0.1);
 | 
			
		||||
@ -195,6 +186,7 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
        .provider-column { flex: 1; display: flex; flex-direction: column; align-items: center; }
 | 
			
		||||
        .provider-label { color: rgba(255, 255, 255, 0.7); font-size: 11px; font-weight: 500; margin-bottom: 6px; }
 | 
			
		||||
        .api-input, .provider-select {
 | 
			
		||||
            -webkit-app-region: no-drag;
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            height: 34px;
 | 
			
		||||
            text-align: center;
 | 
			
		||||
@ -221,6 +213,7 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        .action-button {
 | 
			
		||||
            -webkit-app-region: no-drag;
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            height: 34px;
 | 
			
		||||
            background: rgba(255, 255, 255, 0.2);
 | 
			
		||||
@ -266,37 +259,10 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
            font-weight: 500; /* Medium */
 | 
			
		||||
            margin: 10px 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        /* ────────────────[ GLASS BYPASS ]─────────────── */
 | 
			
		||||
        :host-context(body.has-glass) .container,
 | 
			
		||||
        :host-context(body.has-glass) .api-input,
 | 
			
		||||
        :host-context(body.has-glass) .provider-select,
 | 
			
		||||
        :host-context(body.has-glass) .action-button,
 | 
			
		||||
        :host-context(body.has-glass) .close-button {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
            border: none !important;
 | 
			
		||||
            box-shadow: none !important;
 | 
			
		||||
            filter: none !important;
 | 
			
		||||
            backdrop-filter: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host-context(body.has-glass) .container::after,
 | 
			
		||||
        :host-context(body.has-glass) .action-button::after {
 | 
			
		||||
            display: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host-context(body.has-glass) .action-button:hover,
 | 
			
		||||
        :host-context(body.has-glass) .provider-select:hover,
 | 
			
		||||
        :host-context(body.has-glass) .close-button:hover {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
        }
 | 
			
		||||
    `
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    super()
 | 
			
		||||
    this.dragState = null
 | 
			
		||||
    this.wasJustDragged = false
 | 
			
		||||
    this.isLoading = false
 | 
			
		||||
    this.errorMessage = ""
 | 
			
		||||
    this.successMessage = ""
 | 
			
		||||
@ -358,8 +324,6 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
    this.loadProviderConfig();
 | 
			
		||||
    //////// after_modelStateService ////////
 | 
			
		||||
 | 
			
		||||
    this.handleMouseMove = this.handleMouseMove.bind(this)
 | 
			
		||||
    this.handleMouseUp = this.handleMouseUp.bind(this)
 | 
			
		||||
    this.handleKeyPress = this.handleKeyPress.bind(this)
 | 
			
		||||
    this.handleSubmit = this.handleSubmit.bind(this)
 | 
			
		||||
    this.handleInput = this.handleInput.bind(this)
 | 
			
		||||
@ -1533,7 +1497,6 @@ export class ApiKeyHeader extends LitElement {
 | 
			
		||||
 | 
			
		||||
  handleUsePicklesKey(e) {
 | 
			
		||||
    e.preventDefault()
 | 
			
		||||
    if (this.wasJustDragged) return
 | 
			
		||||
 | 
			
		||||
    console.log("Requesting Firebase authentication from main process...")
 | 
			
		||||
    if (window.require) {
 | 
			
		||||
 | 
			
		||||
@ -2,17 +2,16 @@ import { html, css, LitElement } from '../assets/lit-core-2.7.4.min.js';
 | 
			
		||||
 | 
			
		||||
export class MainHeader extends LitElement {
 | 
			
		||||
    static properties = {
 | 
			
		||||
        isSessionActive: { type: Boolean, state: true },
 | 
			
		||||
        // isSessionActive: { type: Boolean, state: true },
 | 
			
		||||
        isTogglingSession: { type: Boolean, state: true },
 | 
			
		||||
        actionText: { type: String, state: true },
 | 
			
		||||
        shortcuts: { type: Object, state: true },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    static styles = css`
 | 
			
		||||
        :host {
 | 
			
		||||
            display: flex;
 | 
			
		||||
            transform: translate3d(0, 0, 0);
 | 
			
		||||
            backface-visibility: hidden;
 | 
			
		||||
            transition: transform 0.2s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.2s ease-out;
 | 
			
		||||
            will-change: transform, opacity;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host(.hiding) {
 | 
			
		||||
@ -33,65 +32,6 @@ export class MainHeader extends LitElement {
 | 
			
		||||
            pointer-events: none;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @keyframes slideUp {
 | 
			
		||||
            0% {
 | 
			
		||||
                opacity: 1;
 | 
			
		||||
                transform: translateY(0) scale(1);
 | 
			
		||||
                filter: blur(0px);
 | 
			
		||||
            }
 | 
			
		||||
            30% {
 | 
			
		||||
                opacity: 0.7;
 | 
			
		||||
                transform: translateY(-20%) scale(0.98);
 | 
			
		||||
                filter: blur(0.5px);
 | 
			
		||||
            }
 | 
			
		||||
            70% {
 | 
			
		||||
                opacity: 0.3;
 | 
			
		||||
                transform: translateY(-80%) scale(0.92);
 | 
			
		||||
                filter: blur(1.5px);
 | 
			
		||||
            }
 | 
			
		||||
            100% {
 | 
			
		||||
                opacity: 0;
 | 
			
		||||
                transform: translateY(-150%) scale(0.85);
 | 
			
		||||
                filter: blur(2px);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @keyframes slideDown {
 | 
			
		||||
            0% {
 | 
			
		||||
                opacity: 0;
 | 
			
		||||
                transform: translateY(-150%) scale(0.85);
 | 
			
		||||
                filter: blur(2px);
 | 
			
		||||
            }
 | 
			
		||||
            30% {
 | 
			
		||||
                opacity: 0.5;
 | 
			
		||||
                transform: translateY(-50%) scale(0.92);
 | 
			
		||||
                filter: blur(1px);
 | 
			
		||||
            }
 | 
			
		||||
            65% {
 | 
			
		||||
                opacity: 0.9;
 | 
			
		||||
                transform: translateY(-5%) scale(0.99);
 | 
			
		||||
                filter: blur(0.2px);
 | 
			
		||||
            }
 | 
			
		||||
            85% {
 | 
			
		||||
                opacity: 0.98;
 | 
			
		||||
                transform: translateY(2%) scale(1.005);
 | 
			
		||||
                filter: blur(0px);
 | 
			
		||||
            }
 | 
			
		||||
            100% {
 | 
			
		||||
                opacity: 1;
 | 
			
		||||
                transform: translateY(0) scale(1);
 | 
			
		||||
                filter: blur(0px);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @keyframes fadeIn {
 | 
			
		||||
            0% {
 | 
			
		||||
                opacity: 0;
 | 
			
		||||
            }
 | 
			
		||||
            100% {
 | 
			
		||||
                opacity: 1;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        * {
 | 
			
		||||
            font-family: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
 | 
			
		||||
@ -100,6 +40,7 @@ export class MainHeader extends LitElement {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .header {
 | 
			
		||||
            -webkit-app-region: drag;
 | 
			
		||||
            width: max-content;
 | 
			
		||||
            height: 47px;
 | 
			
		||||
            padding: 2px 10px 2px 13px;
 | 
			
		||||
@ -141,6 +82,7 @@ export class MainHeader extends LitElement {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .listen-button {
 | 
			
		||||
            -webkit-app-region: no-drag;
 | 
			
		||||
            height: 26px;
 | 
			
		||||
            padding: 0 13px;
 | 
			
		||||
            background: transparent;
 | 
			
		||||
@ -155,6 +97,11 @@ export class MainHeader extends LitElement {
 | 
			
		||||
            position: relative;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .listen-button:disabled {
 | 
			
		||||
            cursor: default;
 | 
			
		||||
            opacity: 0.8;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .listen-button.active::before {
 | 
			
		||||
            background: rgba(215, 0, 0, 0.5);
 | 
			
		||||
        }
 | 
			
		||||
@ -163,6 +110,24 @@ export class MainHeader extends LitElement {
 | 
			
		||||
            background: rgba(255, 20, 20, 0.6);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .listen-button.done {
 | 
			
		||||
            background-color: rgba(255, 255, 255, 0.6);
 | 
			
		||||
            transition: background-color 0.15s ease;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .listen-button.done .action-text-content {
 | 
			
		||||
            color: black;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .listen-button.done .listen-icon svg rect,
 | 
			
		||||
        .listen-button.done .listen-icon svg path {
 | 
			
		||||
            fill: black;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .listen-button.done:hover {
 | 
			
		||||
            background-color: #f0f0f0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .listen-button:hover::before {
 | 
			
		||||
            background: rgba(255, 255, 255, 0.18);
 | 
			
		||||
        }
 | 
			
		||||
@ -192,7 +157,40 @@ export class MainHeader extends LitElement {
 | 
			
		||||
            pointer-events: none;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .listen-button.done::after {
 | 
			
		||||
            display: none;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .loading-dots {
 | 
			
		||||
            display: flex;
 | 
			
		||||
            align-items: center;
 | 
			
		||||
            gap: 5px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .loading-dots span {
 | 
			
		||||
            width: 6px;
 | 
			
		||||
            height: 6px;
 | 
			
		||||
            background-color: white;
 | 
			
		||||
            border-radius: 50%;
 | 
			
		||||
            animation: pulse 1.4s infinite ease-in-out both;
 | 
			
		||||
        }
 | 
			
		||||
        .loading-dots span:nth-of-type(1) {
 | 
			
		||||
            animation-delay: -0.32s;
 | 
			
		||||
        }
 | 
			
		||||
        .loading-dots span:nth-of-type(2) {
 | 
			
		||||
            animation-delay: -0.16s;
 | 
			
		||||
        }
 | 
			
		||||
        @keyframes pulse {
 | 
			
		||||
            0%, 80%, 100% {
 | 
			
		||||
                opacity: 0.2;
 | 
			
		||||
            }
 | 
			
		||||
            40% {
 | 
			
		||||
                opacity: 1.0;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .header-actions {
 | 
			
		||||
            -webkit-app-region: no-drag;
 | 
			
		||||
            height: 26px;
 | 
			
		||||
            box-sizing: border-box;
 | 
			
		||||
            justify-content: flex-start;
 | 
			
		||||
@ -264,6 +262,7 @@ export class MainHeader extends LitElement {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .settings-button {
 | 
			
		||||
            -webkit-app-region: no-drag;
 | 
			
		||||
            padding: 5px;
 | 
			
		||||
            border-radius: 50%;
 | 
			
		||||
            background: transparent;
 | 
			
		||||
@ -291,125 +290,22 @@ export class MainHeader extends LitElement {
 | 
			
		||||
            width: 16px;
 | 
			
		||||
            height: 16px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /* ────────────────[ GLASS BYPASS ]─────────────── */
 | 
			
		||||
        :host-context(body.has-glass) .header,
 | 
			
		||||
        :host-context(body.has-glass) .listen-button,
 | 
			
		||||
        :host-context(body.has-glass) .header-actions,
 | 
			
		||||
        :host-context(body.has-glass) .settings-button {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
            filter: none !important;
 | 
			
		||||
            box-shadow: none !important;
 | 
			
		||||
            backdrop-filter: none !important;
 | 
			
		||||
        }
 | 
			
		||||
        :host-context(body.has-glass) .icon-box {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
            border: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host-context(body.has-glass) .header::before,
 | 
			
		||||
        :host-context(body.has-glass) .header::after,
 | 
			
		||||
        :host-context(body.has-glass) .listen-button::before,
 | 
			
		||||
        :host-context(body.has-glass) .listen-button::after {
 | 
			
		||||
            display: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host-context(body.has-glass) .header-actions:hover,
 | 
			
		||||
        :host-context(body.has-glass) .settings-button:hover,
 | 
			
		||||
        :host-context(body.has-glass) .listen-button:hover::before {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
        }
 | 
			
		||||
        :host-context(body.has-glass) * {
 | 
			
		||||
            animation: none !important;
 | 
			
		||||
            transition: none !important;
 | 
			
		||||
            transform: none !important;
 | 
			
		||||
            filter: none !important;
 | 
			
		||||
            backdrop-filter: none !important;
 | 
			
		||||
            box-shadow: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host-context(body.has-glass) .header,
 | 
			
		||||
        :host-context(body.has-glass) .listen-button,
 | 
			
		||||
        :host-context(body.has-glass) .header-actions,
 | 
			
		||||
        :host-context(body.has-glass) .settings-button,
 | 
			
		||||
        :host-context(body.has-glass) .icon-box {
 | 
			
		||||
            border-radius: 0 !important;
 | 
			
		||||
        }
 | 
			
		||||
        :host-context(body.has-glass) {
 | 
			
		||||
            animation: none !important;
 | 
			
		||||
            transition: none !important;
 | 
			
		||||
            transform: none !important;
 | 
			
		||||
            will-change: auto !important;
 | 
			
		||||
        }
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
        this.shortcuts = {};
 | 
			
		||||
        this.dragState = null;
 | 
			
		||||
        this.wasJustDragged = false;
 | 
			
		||||
        this.isVisible = true;
 | 
			
		||||
        this.isAnimating = false;
 | 
			
		||||
        this.hasSlidIn = false;
 | 
			
		||||
        this.settingsHideTimer = null;
 | 
			
		||||
        this.isSessionActive = false;
 | 
			
		||||
        // this.isSessionActive = false;
 | 
			
		||||
        this.isTogglingSession = false;
 | 
			
		||||
        this.actionText = 'Listen';
 | 
			
		||||
        this.animationEndTimer = null;
 | 
			
		||||
        this.handleMouseMove = this.handleMouseMove.bind(this);
 | 
			
		||||
        this.handleMouseUp = this.handleMouseUp.bind(this);
 | 
			
		||||
        this.handleAnimationEnd = this.handleAnimationEnd.bind(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleMouseDown(e) {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        const initialPosition = await ipcRenderer.invoke('get-header-position');
 | 
			
		||||
 | 
			
		||||
        this.dragState = {
 | 
			
		||||
            initialMouseX: e.screenX,
 | 
			
		||||
            initialMouseY: e.screenY,
 | 
			
		||||
            initialWindowX: initialPosition.x,
 | 
			
		||||
            initialWindowY: initialPosition.y,
 | 
			
		||||
            moved: false,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        window.addEventListener('mousemove', this.handleMouseMove, { capture: true });
 | 
			
		||||
        window.addEventListener('mouseup', this.handleMouseUp, { once: true, capture: true });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleMouseMove(e) {
 | 
			
		||||
        if (!this.dragState) return;
 | 
			
		||||
 | 
			
		||||
        const deltaX = Math.abs(e.screenX - this.dragState.initialMouseX);
 | 
			
		||||
        const deltaY = Math.abs(e.screenY - this.dragState.initialMouseY);
 | 
			
		||||
        
 | 
			
		||||
        if (deltaX > 3 || deltaY > 3) {
 | 
			
		||||
            this.dragState.moved = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleMouseUp(e) {
 | 
			
		||||
        if (!this.dragState) return;
 | 
			
		||||
 | 
			
		||||
        const wasDragged = this.dragState.moved;
 | 
			
		||||
 | 
			
		||||
        window.removeEventListener('mousemove', this.handleMouseMove, { capture: true });
 | 
			
		||||
        this.dragState = null;
 | 
			
		||||
 | 
			
		||||
        if (wasDragged) {
 | 
			
		||||
            this.wasJustDragged = true;
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                this.wasJustDragged = false;
 | 
			
		||||
            }, 0);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    toggleVisibility() {
 | 
			
		||||
        if (this.isAnimating) {
 | 
			
		||||
            console.log('[MainHeader] Animation already in progress, ignoring toggle');
 | 
			
		||||
@ -431,58 +327,29 @@ export class MainHeader extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    hide() {
 | 
			
		||||
        this.classList.remove('showing', 'hidden');
 | 
			
		||||
        this.classList.remove('showing');
 | 
			
		||||
        this.classList.add('hiding');
 | 
			
		||||
        this.isVisible = false;
 | 
			
		||||
        
 | 
			
		||||
        this.animationEndTimer = setTimeout(() => {
 | 
			
		||||
            if (this.classList.contains('hiding')) {
 | 
			
		||||
                this.handleAnimationEnd({ target: this });
 | 
			
		||||
            }
 | 
			
		||||
        }, 350);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    show() {
 | 
			
		||||
        this.classList.remove('hiding', 'hidden');
 | 
			
		||||
        this.classList.add('showing');
 | 
			
		||||
        this.isVisible = true;
 | 
			
		||||
        
 | 
			
		||||
        this.animationEndTimer = setTimeout(() => {
 | 
			
		||||
            if (this.classList.contains('showing')) {
 | 
			
		||||
                this.handleAnimationEnd({ target: this });
 | 
			
		||||
            }
 | 
			
		||||
        }, 400);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    handleAnimationEnd(e) {
 | 
			
		||||
        if (e.target !== this) return;
 | 
			
		||||
    
 | 
			
		||||
        if (this.animationEndTimer) {
 | 
			
		||||
            clearTimeout(this.animationEndTimer);
 | 
			
		||||
            this.animationEndTimer = null;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        this.isAnimating = false;
 | 
			
		||||
    
 | 
			
		||||
        if (this.classList.contains('hiding')) {
 | 
			
		||||
            this.classList.remove('hiding');
 | 
			
		||||
            this.classList.add('hidden');
 | 
			
		||||
            
 | 
			
		||||
            if (window.require) {
 | 
			
		||||
                const { ipcRenderer } = window.require('electron');
 | 
			
		||||
                ipcRenderer.send('header-animation-complete', 'hidden');
 | 
			
		||||
                window.require('electron').ipcRenderer.send('header-animation-finished', 'hidden');
 | 
			
		||||
            }
 | 
			
		||||
        } else if (this.classList.contains('showing')) {
 | 
			
		||||
            this.classList.remove('showing');
 | 
			
		||||
            
 | 
			
		||||
            if (window.require) {
 | 
			
		||||
                const { ipcRenderer } = window.require('electron');
 | 
			
		||||
                ipcRenderer.send('header-animation-complete', 'visible');
 | 
			
		||||
                window.require('electron').ipcRenderer.send('header-animation-finished', 'visible');
 | 
			
		||||
            }
 | 
			
		||||
        } else if (this.classList.contains('sliding-in')) {
 | 
			
		||||
            this.classList.remove('sliding-in');
 | 
			
		||||
            this.hasSlidIn = true;
 | 
			
		||||
            console.log('[MainHeader] Slide-in animation completed');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -497,10 +364,19 @@ export class MainHeader extends LitElement {
 | 
			
		||||
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            this._sessionStateListener = (event, { isActive }) => {
 | 
			
		||||
                this.isSessionActive = isActive;
 | 
			
		||||
 | 
			
		||||
            this._sessionStateTextListener = (event, text) => {
 | 
			
		||||
                this.actionText = text;
 | 
			
		||||
                this.isTogglingSession = false;
 | 
			
		||||
            };
 | 
			
		||||
            ipcRenderer.on('session-state-changed', this._sessionStateListener);
 | 
			
		||||
            ipcRenderer.on('session-state-text', this._sessionStateTextListener);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            // this._sessionStateListener = (event, { isActive }) => {
 | 
			
		||||
            //     this.isSessionActive = isActive;
 | 
			
		||||
            //     this.isTogglingSession = false;
 | 
			
		||||
            // };
 | 
			
		||||
            // ipcRenderer.on('session-state-changed', this._sessionStateListener);
 | 
			
		||||
            this._shortcutListener = (event, keybinds) => {
 | 
			
		||||
                console.log('[MainHeader] Received updated shortcuts:', keybinds);
 | 
			
		||||
                this.shortcuts = keybinds;
 | 
			
		||||
@ -520,9 +396,12 @@ export class MainHeader extends LitElement {
 | 
			
		||||
        
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            if (this._sessionStateListener) {
 | 
			
		||||
                ipcRenderer.removeListener('session-state-changed', this._sessionStateListener);
 | 
			
		||||
            if (this._sessionStateTextListener) {
 | 
			
		||||
                ipcRenderer.removeListener('session-state-text', this._sessionStateTextListener);
 | 
			
		||||
            }
 | 
			
		||||
            // if (this._sessionStateListener) {
 | 
			
		||||
            //     ipcRenderer.removeListener('session-state-changed', this._sessionStateListener);
 | 
			
		||||
            // }
 | 
			
		||||
            if (this._shortcutListener) {
 | 
			
		||||
                ipcRenderer.removeListener('shortcuts-updated', this._shortcutListener);
 | 
			
		||||
            }
 | 
			
		||||
@ -530,51 +409,56 @@ export class MainHeader extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    invoke(channel, ...args) {
 | 
			
		||||
        if (this.wasJustDragged) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            window.require('electron').ipcRenderer.invoke(channel, ...args);
 | 
			
		||||
        }
 | 
			
		||||
        // return Promise.resolve();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    showWindow(name, element) {
 | 
			
		||||
        if (this.wasJustDragged) return;
 | 
			
		||||
    showSettingsWindow(element) {
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            console.log(`[MainHeader] showWindow('${name}') called at ${Date.now()}`);
 | 
			
		||||
            console.log(`[MainHeader] showSettingsWindow called at ${Date.now()}`);
 | 
			
		||||
            
 | 
			
		||||
            ipcRenderer.send('cancel-hide-window', name);
 | 
			
		||||
            ipcRenderer.send('cancel-hide-settings-window');
 | 
			
		||||
 | 
			
		||||
            if (name === 'settings' && element) {
 | 
			
		||||
                const rect = element.getBoundingClientRect();
 | 
			
		||||
                ipcRenderer.send('show-window', {
 | 
			
		||||
                    name: 'settings',
 | 
			
		||||
                    bounds: {
 | 
			
		||||
                        x: rect.left,
 | 
			
		||||
                        y: rect.top,
 | 
			
		||||
                        width: rect.width,
 | 
			
		||||
                        height: rect.height
 | 
			
		||||
                    }
 | 
			
		||||
            if (element) {
 | 
			
		||||
                const { left, top, width, height } = element.getBoundingClientRect();
 | 
			
		||||
                ipcRenderer.send('show-settings-window', {
 | 
			
		||||
                    x: left,
 | 
			
		||||
                    y: top,
 | 
			
		||||
                    width,
 | 
			
		||||
                    height,
 | 
			
		||||
                });
 | 
			
		||||
            } else {
 | 
			
		||||
                ipcRenderer.send('show-window', name);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    hideWindow(name) {
 | 
			
		||||
        if (this.wasJustDragged) return;
 | 
			
		||||
    hideSettingsWindow() {
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            console.log(`[MainHeader] hideWindow('${name}') called at ${Date.now()}`);
 | 
			
		||||
            window.require('electron').ipcRenderer.send('hide-window', name);
 | 
			
		||||
            console.log(`[MainHeader] hideSettingsWindow called at ${Date.now()}`);
 | 
			
		||||
            window.require('electron').ipcRenderer.send('hide-settings-window');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    cancelHideWindow(name) {
 | 
			
		||||
 | 
			
		||||
    async _handleListenClick() {
 | 
			
		||||
        if (this.isTogglingSession) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.isTogglingSession = true;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const channel = 'toggle-feature';
 | 
			
		||||
            const args = ['listen'];
 | 
			
		||||
            await this.invoke(channel, ...args);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('IPC invoke for session toggle failed:', error);
 | 
			
		||||
            this.isTogglingSession = false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    renderShortcut(accelerator) {
 | 
			
		||||
        if (!accelerator) return html``;
 | 
			
		||||
 | 
			
		||||
@ -599,22 +483,35 @@ export class MainHeader extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        const buttonClasses = {
 | 
			
		||||
            active: this.actionText === 'Stop',
 | 
			
		||||
            done: this.actionText === 'Done',
 | 
			
		||||
        };
 | 
			
		||||
        const showStopIcon = this.actionText === 'Stop' || this.actionText === 'Done';
 | 
			
		||||
 | 
			
		||||
        return html`
 | 
			
		||||
            <div class="header" @mousedown=${this.handleMouseDown}>
 | 
			
		||||
            <div class="header">
 | 
			
		||||
                <button 
 | 
			
		||||
                    class="listen-button ${this.isSessionActive ? 'active' : ''}"
 | 
			
		||||
                    @click=${() => this.invoke(this.isSessionActive ? 'close-session' : 'toggle-feature', 'listen')}
 | 
			
		||||
                    class="listen-button ${Object.keys(buttonClasses).filter(k => buttonClasses[k]).join(' ')}"
 | 
			
		||||
                    @click=${this._handleListenClick}
 | 
			
		||||
                    ?disabled=${this.isTogglingSession}
 | 
			
		||||
                >
 | 
			
		||||
                    ${this.isTogglingSession
 | 
			
		||||
                        ? html`
 | 
			
		||||
                            <div class="loading-dots">
 | 
			
		||||
                                <span></span><span></span><span></span>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        `
 | 
			
		||||
                        : html`
 | 
			
		||||
                            <div class="action-text">
 | 
			
		||||
                        <div class="action-text-content">${this.isSessionActive ? 'Stop' : 'Listen'}</div>
 | 
			
		||||
                                <div class="action-text-content">${this.actionText}</div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="listen-icon">
 | 
			
		||||
                        ${this.isSessionActive
 | 
			
		||||
                                ${showStopIcon
 | 
			
		||||
                                    ? html`
 | 
			
		||||
                                        <svg width="9" height="9" viewBox="0 0 9 9" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
                                            <rect width="9" height="9" rx="1" fill="white"/>
 | 
			
		||||
                                        </svg>
 | 
			
		||||
 | 
			
		||||
                                    `
 | 
			
		||||
                                    : html`
 | 
			
		||||
                                        <svg width="12" height="11" viewBox="0 0 12 11" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
@ -624,6 +521,7 @@ export class MainHeader extends LitElement {
 | 
			
		||||
                                        </svg>
 | 
			
		||||
                                    `}
 | 
			
		||||
                            </div>
 | 
			
		||||
                        `}
 | 
			
		||||
                </button>
 | 
			
		||||
 | 
			
		||||
                <div class="header-actions ask-action" @click=${() => this.invoke('toggle-feature', 'ask')}>
 | 
			
		||||
@ -646,8 +544,8 @@ export class MainHeader extends LitElement {
 | 
			
		||||
 | 
			
		||||
                <button 
 | 
			
		||||
                    class="settings-button"
 | 
			
		||||
                    @mouseenter=${(e) => this.showWindow('settings', e.currentTarget)}
 | 
			
		||||
                    @mouseleave=${() => this.hideWindow('settings')}
 | 
			
		||||
                    @mouseenter=${(e) => this.showSettingsWindow(e.currentTarget)}
 | 
			
		||||
                    @mouseleave=${() => this.hideSettingsWindow()}
 | 
			
		||||
                >
 | 
			
		||||
                    <div class="settings-icon">
 | 
			
		||||
                        <svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
 | 
			
		||||
@ -4,14 +4,13 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
    static styles = css`
 | 
			
		||||
        :host {
 | 
			
		||||
            display: block;
 | 
			
		||||
            transform: translate3d(0, 0, 0);
 | 
			
		||||
            backface-visibility: hidden;
 | 
			
		||||
            transition: opacity 0.25s ease-out;
 | 
			
		||||
            transition: opacity 0.3s ease-in, transform 0.3s ease-in;
 | 
			
		||||
            will-change: opacity, transform;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host(.sliding-out) {
 | 
			
		||||
            animation: slideOutUp 0.3s ease-in forwards;
 | 
			
		||||
            will-change: opacity, transform;
 | 
			
		||||
            opacity: 0;
 | 
			
		||||
            transform: translateY(-20px);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host(.hidden) {
 | 
			
		||||
@ -19,17 +18,6 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
            pointer-events: none;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @keyframes slideOutUp {
 | 
			
		||||
            from {
 | 
			
		||||
                opacity: 1;
 | 
			
		||||
                transform: translateY(0);
 | 
			
		||||
            }
 | 
			
		||||
            to {
 | 
			
		||||
                opacity: 0;
 | 
			
		||||
                transform: translateY(-20px);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        * {
 | 
			
		||||
            font-family: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
 | 
			
		||||
            cursor: default;
 | 
			
		||||
@ -38,6 +26,7 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .container {
 | 
			
		||||
            -webkit-app-region: drag;
 | 
			
		||||
            width: 285px;
 | 
			
		||||
            height: 220px;
 | 
			
		||||
            padding: 18px 20px;
 | 
			
		||||
@ -67,6 +56,7 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .close-button {
 | 
			
		||||
            -webkit-app-region: no-drag;
 | 
			
		||||
            position: absolute;
 | 
			
		||||
            top: 10px;
 | 
			
		||||
            right: 10px;
 | 
			
		||||
@ -157,6 +147,7 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .action-button {
 | 
			
		||||
            -webkit-app-region: no-drag;
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            height: 34px;
 | 
			
		||||
            background: rgba(255, 255, 255, 0.2);
 | 
			
		||||
@ -198,6 +189,7 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .continue-button {
 | 
			
		||||
            -webkit-app-region: no-drag;
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            height: 34px;
 | 
			
		||||
            background: rgba(34, 197, 94, 0.8);
 | 
			
		||||
@ -237,30 +229,6 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
            background: rgba(255, 255, 255, 0.2);
 | 
			
		||||
            cursor: not-allowed;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /* ────────────────[ GLASS BYPASS ]─────────────── */
 | 
			
		||||
        :host-context(body.has-glass) .container,
 | 
			
		||||
        :host-context(body.has-glass) .action-button,
 | 
			
		||||
        :host-context(body.has-glass) .continue-button,
 | 
			
		||||
        :host-context(body.has-glass) .close-button {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
            border: none !important;
 | 
			
		||||
            box-shadow: none !important;
 | 
			
		||||
            filter: none !important;
 | 
			
		||||
            backdrop-filter: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host-context(body.has-glass) .container::after,
 | 
			
		||||
        :host-context(body.has-glass) .action-button::after,
 | 
			
		||||
        :host-context(body.has-glass) .continue-button::after {
 | 
			
		||||
            display: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host-context(body.has-glass) .action-button:hover,
 | 
			
		||||
        :host-context(body.has-glass) .continue-button:hover,
 | 
			
		||||
        :host-context(body.has-glass) .close-button:hover {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
        }
 | 
			
		||||
    `;
 | 
			
		||||
 | 
			
		||||
    static properties = {
 | 
			
		||||
@ -276,9 +244,6 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
        this.screenGranted = 'unknown';
 | 
			
		||||
        this.isChecking = false;
 | 
			
		||||
        this.continueCallback = null;
 | 
			
		||||
 | 
			
		||||
        this.handleMouseMove = this.handleMouseMove.bind(this);
 | 
			
		||||
        this.handleMouseUp = this.handleMouseUp.bind(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async connectedCallback() {
 | 
			
		||||
@ -298,61 +263,6 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleMouseDown(e) {
 | 
			
		||||
        if (e.target.tagName === 'BUTTON') {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        const initialPosition = await ipcRenderer.invoke('get-header-position');
 | 
			
		||||
 | 
			
		||||
        this.dragState = {
 | 
			
		||||
            initialMouseX: e.screenX,
 | 
			
		||||
            initialMouseY: e.screenY,
 | 
			
		||||
            initialWindowX: initialPosition.x,
 | 
			
		||||
            initialWindowY: initialPosition.y,
 | 
			
		||||
            moved: false,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        window.addEventListener('mousemove', this.handleMouseMove);
 | 
			
		||||
        window.addEventListener('mouseup', this.handleMouseUp, { once: true });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleMouseMove(e) {
 | 
			
		||||
        if (!this.dragState) return;
 | 
			
		||||
 | 
			
		||||
        const deltaX = Math.abs(e.screenX - this.dragState.initialMouseX);
 | 
			
		||||
        const deltaY = Math.abs(e.screenY - this.dragState.initialMouseY);
 | 
			
		||||
 | 
			
		||||
        if (deltaX > 3 || deltaY > 3) {
 | 
			
		||||
            this.dragState.moved = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleMouseUp(e) {
 | 
			
		||||
        if (!this.dragState) return;
 | 
			
		||||
 | 
			
		||||
        const wasDragged = this.dragState.moved;
 | 
			
		||||
 | 
			
		||||
        window.removeEventListener('mousemove', this.handleMouseMove);
 | 
			
		||||
        this.dragState = null;
 | 
			
		||||
 | 
			
		||||
        if (wasDragged) {
 | 
			
		||||
            this.wasJustDragged = true;
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                this.wasJustDragged = false;
 | 
			
		||||
            }, 200);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async checkPermissions() {
 | 
			
		||||
        if (!window.require || this.isChecking) return;
 | 
			
		||||
        
 | 
			
		||||
@ -390,7 +300,7 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleMicrophoneClick() {
 | 
			
		||||
        if (!window.require || this.microphoneGranted === 'granted' || this.wasJustDragged) return;
 | 
			
		||||
        if (!window.require || this.microphoneGranted === 'granted') return;
 | 
			
		||||
        
 | 
			
		||||
        console.log('[PermissionHeader] Requesting microphone permission...');
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
@ -423,7 +333,7 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleScreenClick() {
 | 
			
		||||
        if (!window.require || this.screenGranted === 'granted' || this.wasJustDragged) return;
 | 
			
		||||
        if (!window.require || this.screenGranted === 'granted') return;
 | 
			
		||||
        
 | 
			
		||||
        console.log('[PermissionHeader] Checking screen recording permission...');
 | 
			
		||||
        const { ipcRenderer } = window.require('electron');
 | 
			
		||||
@ -453,8 +363,7 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
    async handleContinue() {
 | 
			
		||||
        if (this.continueCallback && 
 | 
			
		||||
            this.microphoneGranted === 'granted' && 
 | 
			
		||||
            this.screenGranted === 'granted' && 
 | 
			
		||||
            !this.wasJustDragged) {
 | 
			
		||||
            this.screenGranted === 'granted') {
 | 
			
		||||
            // Mark permissions as completed
 | 
			
		||||
            if (window.require) {
 | 
			
		||||
                const { ipcRenderer } = window.require('electron');
 | 
			
		||||
@ -481,7 +390,7 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
        const allGranted = this.microphoneGranted === 'granted' && this.screenGranted === 'granted';
 | 
			
		||||
 | 
			
		||||
        return html`
 | 
			
		||||
            <div class="container" @mousedown=${this.handleMouseDown}>
 | 
			
		||||
            <div class="container">
 | 
			
		||||
                <button class="close-button" @click=${this.handleClose} title="Close application">
 | 
			
		||||
                    <svg width="8" height="8" viewBox="0 0 10 10" fill="currentColor">
 | 
			
		||||
                        <path d="M1 1L9 9M9 1L1 9" stroke="currentColor" stroke-width="1.2" />
 | 
			
		||||
 | 
			
		||||
@ -68,12 +68,7 @@ export class PickleGlassApp extends LitElement {
 | 
			
		||||
        this.selectedScreenshotInterval = localStorage.getItem('selectedScreenshotInterval') || '5';
 | 
			
		||||
        this.selectedImageQuality = localStorage.getItem('selectedImageQuality') || 'medium';
 | 
			
		||||
        this._isClickThrough = false;
 | 
			
		||||
        this.outlines = [];
 | 
			
		||||
        this.analysisRequests = [];
 | 
			
		||||
 | 
			
		||||
        window.pickleGlass.setStructuredData = data => {
 | 
			
		||||
            this.updateStructuredData(data);
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    connectedCallback() {
 | 
			
		||||
@ -82,14 +77,13 @@ export class PickleGlassApp extends LitElement {
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            
 | 
			
		||||
            ipcRenderer.on('update-status', (_, status) => this.setStatus(status));
 | 
			
		||||
            ipcRenderer.on('click-through-toggled', (_, isEnabled) => {
 | 
			
		||||
                this._isClickThrough = isEnabled;
 | 
			
		||||
            });
 | 
			
		||||
            ipcRenderer.on('start-listening-session', () => {
 | 
			
		||||
                console.log('Received start-listening-session command, calling handleListenClick.');
 | 
			
		||||
                this.handleListenClick();
 | 
			
		||||
            });
 | 
			
		||||
            // ipcRenderer.on('start-listening-session', () => {
 | 
			
		||||
            //     console.log('Received start-listening-session command, calling handleListenClick.');
 | 
			
		||||
            //     this.handleListenClick();
 | 
			
		||||
            // });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -97,16 +91,15 @@ export class PickleGlassApp extends LitElement {
 | 
			
		||||
        super.disconnectedCallback();
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            ipcRenderer.removeAllListeners('update-status');
 | 
			
		||||
            ipcRenderer.removeAllListeners('click-through-toggled');
 | 
			
		||||
            ipcRenderer.removeAllListeners('start-listening-session');
 | 
			
		||||
            // ipcRenderer.removeAllListeners('start-listening-session');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updated(changedProperties) {
 | 
			
		||||
        if (changedProperties.has('isMainViewVisible') || changedProperties.has('currentView')) {
 | 
			
		||||
            this.requestWindowResize();
 | 
			
		||||
        }
 | 
			
		||||
        // if (changedProperties.has('isMainViewVisible') || changedProperties.has('currentView')) {
 | 
			
		||||
        //     this.requestWindowResize();
 | 
			
		||||
        // }
 | 
			
		||||
 | 
			
		||||
        if (changedProperties.has('currentView')) {
 | 
			
		||||
            const viewContainer = this.shadowRoot?.querySelector('.view-container');
 | 
			
		||||
@ -136,67 +129,35 @@ export class PickleGlassApp extends LitElement {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    requestWindowResize() {
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            ipcRenderer.invoke('resize-window', {
 | 
			
		||||
                isMainViewVisible: this.isMainViewVisible,
 | 
			
		||||
                view: this.currentView,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setStatus(text) {
 | 
			
		||||
        this.statusText = text;
 | 
			
		||||
    }
 | 
			
		||||
    // async handleListenClick() {
 | 
			
		||||
    //     if (window.require) {
 | 
			
		||||
    //         const { ipcRenderer } = window.require('electron');
 | 
			
		||||
    //         const isActive = await ipcRenderer.invoke('is-session-active');
 | 
			
		||||
    //         // if (isActive) {
 | 
			
		||||
    //         //     console.log('Session is already active. No action needed.');
 | 
			
		||||
    //         //     return;
 | 
			
		||||
    //         // }
 | 
			
		||||
    //     }
 | 
			
		||||
 | 
			
		||||
    async handleListenClick() {
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            const isActive = await ipcRenderer.invoke('is-session-active');
 | 
			
		||||
            if (isActive) {
 | 
			
		||||
                console.log('Session is already active. No action needed.');
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    //     if (window.pickleGlass) {
 | 
			
		||||
    //         // await window.pickleGlass.initializeopenai(this.selectedProfile, this.selectedLanguage);
 | 
			
		||||
    //         window.pickleGlass.startCapture(this.selectedScreenshotInterval, this.selectedImageQuality);
 | 
			
		||||
    //     }
 | 
			
		||||
 | 
			
		||||
        if (window.pickleGlass) {
 | 
			
		||||
            await window.pickleGlass.initializeopenai(this.selectedProfile, this.selectedLanguage);
 | 
			
		||||
            window.pickleGlass.startCapture(this.selectedScreenshotInterval, this.selectedImageQuality);
 | 
			
		||||
        }
 | 
			
		||||
    //     // 🔄 Clear previous summary/analysis when a new listening session begins
 | 
			
		||||
    //     this.structuredData = {
 | 
			
		||||
    //         summary: [],
 | 
			
		||||
    //         topic: { header: '', bullets: [] },
 | 
			
		||||
    //         actions: [],
 | 
			
		||||
    //         followUps: [],
 | 
			
		||||
    //     };
 | 
			
		||||
 | 
			
		||||
        // 🔄 Clear previous summary/analysis when a new listening session begins
 | 
			
		||||
        this.structuredData = {
 | 
			
		||||
            summary: [],
 | 
			
		||||
            topic: { header: '', bullets: [] },
 | 
			
		||||
            actions: [],
 | 
			
		||||
            followUps: [],
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this.currentResponseIndex = -1;
 | 
			
		||||
        this.startTime = Date.now();
 | 
			
		||||
        this.currentView = 'listen';
 | 
			
		||||
        this.isMainViewVisible = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleShowHideClick() {
 | 
			
		||||
        this.isMainViewVisible = !this.isMainViewVisible;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleSettingsClick() {
 | 
			
		||||
        this.currentView = 'settings';
 | 
			
		||||
        this.isMainViewVisible = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleHelpClick() {
 | 
			
		||||
        this.currentView = 'help';
 | 
			
		||||
        this.isMainViewVisible = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleHistoryClick() {
 | 
			
		||||
        this.currentView = 'history';
 | 
			
		||||
        this.isMainViewVisible = true;
 | 
			
		||||
    }
 | 
			
		||||
    //     this.currentResponseIndex = -1;
 | 
			
		||||
    //     this.startTime = Date.now();
 | 
			
		||||
    //     this.currentView = 'listen';
 | 
			
		||||
    //     this.isMainViewVisible = true;
 | 
			
		||||
    // }
 | 
			
		||||
 | 
			
		||||
    async handleClose() {
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
@ -205,50 +166,8 @@ export class PickleGlassApp extends LitElement {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleBackClick() {
 | 
			
		||||
        this.currentView = 'listen';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleSendText(message) {
 | 
			
		||||
        if (window.pickleGlass) {
 | 
			
		||||
            const result = await window.pickleGlass.sendTextMessage(message);
 | 
			
		||||
 | 
			
		||||
            if (!result.success) {
 | 
			
		||||
                console.error('Failed to send message:', result.error);
 | 
			
		||||
                this.setStatus('Error sending message: ' + result.error);
 | 
			
		||||
            } else {
 | 
			
		||||
                this.setStatus('Message sent...');
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // updateOutline(outline) {
 | 
			
		||||
    //     console.log('📝 PickleGlassApp updateOutline:', outline);
 | 
			
		||||
    //     this.outlines = [...outline];
 | 
			
		||||
    //     this.requestUpdate();
 | 
			
		||||
    // }
 | 
			
		||||
 | 
			
		||||
    // updateAnalysisRequests(requests) {
 | 
			
		||||
    //     console.log('📝 PickleGlassApp updateAnalysisRequests:', requests);
 | 
			
		||||
    //     this.analysisRequests = [...requests];
 | 
			
		||||
    //     this.requestUpdate();
 | 
			
		||||
    // }
 | 
			
		||||
 | 
			
		||||
    updateStructuredData(data) {
 | 
			
		||||
        console.log('📝 PickleGlassApp updateStructuredData:', data);
 | 
			
		||||
        this.structuredData = data;
 | 
			
		||||
        this.requestUpdate();
 | 
			
		||||
        
 | 
			
		||||
        const assistantView = this.shadowRoot?.querySelector('assistant-view');
 | 
			
		||||
        if (assistantView) {
 | 
			
		||||
            assistantView.structuredData = data;
 | 
			
		||||
            console.log('✅ Structured data passed to AssistantView');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleResponseIndexChanged(e) {
 | 
			
		||||
        this.currentResponseIndex = e.detail.index;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        switch (this.currentView) {
 | 
			
		||||
@ -257,7 +176,6 @@ export class PickleGlassApp extends LitElement {
 | 
			
		||||
                    .currentResponseIndex=${this.currentResponseIndex}
 | 
			
		||||
                    .selectedProfile=${this.selectedProfile}
 | 
			
		||||
                    .structuredData=${this.structuredData}
 | 
			
		||||
                    .onSendText=${message => this.handleSendText(message)}
 | 
			
		||||
                    @response-index-changed=${e => (this.currentResponseIndex = e.detail.index)}
 | 
			
		||||
                ></assistant-view>`;
 | 
			
		||||
            case 'ask':
 | 
			
		||||
 | 
			
		||||
@ -237,46 +237,45 @@
 | 
			
		||||
        <script>
 | 
			
		||||
            window.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
                const app = document.getElementById('pickle-glass');
 | 
			
		||||
                let animationTimeout = null;
 | 
			
		||||
        
 | 
			
		||||
                if (window.require) {
 | 
			
		||||
                    const { ipcRenderer } = window.require('electron');
 | 
			
		||||
        
 | 
			
		||||
                    // --- 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');
 | 
			
		||||
        
 | 
			
		||||
                            // 완료 후 애니메이션 클래스 정리
 | 
			
		||||
                            app.classList.remove('window-sliding-up', 'settings-window-hide');
 | 
			
		||||
                            app.classList.add('window-hidden');
 | 
			
		||||
                        } else if (event.animationName === 'slideDownFromHeader' || event.animationName === 'settingsPopFromButton') {
 | 
			
		||||
                             // 보이기 애니메이션 완료 후 클래스 정리
 | 
			
		||||
                            app.classList.remove('window-sliding-down', 'settings-window-show');
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
        
 | 
			
		||||
                    ipcRenderer.on('window-show-animation', () => {
 | 
			
		||||
                        console.log('Starting window show animation');
 | 
			
		||||
                        app.classList.remove('window-hidden', 'window-sliding-up', 'settings-window-hide');
 | 
			
		||||
                        app.classList.add('window-sliding-down');
 | 
			
		||||
                        
 | 
			
		||||
                        if (animationTimeout) clearTimeout(animationTimeout);
 | 
			
		||||
                        animationTimeout = setTimeout(() => {
 | 
			
		||||
                            app.classList.remove('window-sliding-down');
 | 
			
		||||
                        }, 120);
 | 
			
		||||
                    });
 | 
			
		||||
        
 | 
			
		||||
                    ipcRenderer.on('window-hide-animation', () => {
 | 
			
		||||
                        console.log('Starting window hide animation');
 | 
			
		||||
                        app.classList.remove('window-sliding-down', 'settings-window-show');
 | 
			
		||||
                        app.classList.add('window-sliding-up');
 | 
			
		||||
                        
 | 
			
		||||
                        if (animationTimeout) clearTimeout(animationTimeout);
 | 
			
		||||
                        animationTimeout = setTimeout(() => {
 | 
			
		||||
                            app.classList.remove('window-sliding-up');
 | 
			
		||||
                            app.classList.add('window-hidden');
 | 
			
		||||
                        }, 100);
 | 
			
		||||
                    });
 | 
			
		||||
        
 | 
			
		||||
                    ipcRenderer.on('settings-window-hide-animation', () => {
 | 
			
		||||
                        console.log('Starting settings window hide animation');
 | 
			
		||||
                        app.classList.remove('window-sliding-down', 'settings-window-show');
 | 
			
		||||
                        app.classList.add('settings-window-hide');
 | 
			
		||||
                        
 | 
			
		||||
                        if (animationTimeout) clearTimeout(animationTimeout);
 | 
			
		||||
                        animationTimeout = setTimeout(() => {
 | 
			
		||||
                            app.classList.remove('settings-window-hide');
 | 
			
		||||
                            app.classList.add('window-hidden');
 | 
			
		||||
                        }, 100);
 | 
			
		||||
                    });
 | 
			
		||||
        
 | 
			
		||||
                    // --- UNCHANGED: Existing logic for listen window movement ---
 | 
			
		||||
                    ipcRenderer.on('listen-window-move-to-center', () => {
 | 
			
		||||
                        console.log('Moving listen window to center');
 | 
			
		||||
                        app.classList.add('listen-window-moving');
 | 
			
		||||
@ -305,6 +304,11 @@
 | 
			
		||||
            const params = new URLSearchParams(window.location.search);
 | 
			
		||||
            if (params.get('glass') === 'true') {
 | 
			
		||||
                document.body.classList.add('has-glass');
 | 
			
		||||
                // --- ADDED: Link to centralized glass-bypass styles ---
 | 
			
		||||
                const link = document.createElement('link');
 | 
			
		||||
                link.rel = 'stylesheet';
 | 
			
		||||
                link.href = '../common/styles/glass-bypass.css';
 | 
			
		||||
                document.head.appendChild(link);
 | 
			
		||||
            }
 | 
			
		||||
        </script>
 | 
			
		||||
    </body>
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,11 @@
 | 
			
		||||
            const params = new URLSearchParams(window.location.search);
 | 
			
		||||
            if (params.get('glass') === 'true') {
 | 
			
		||||
                document.body.classList.add('has-glass');
 | 
			
		||||
                // --- ADDED: Link to centralized glass-bypass styles ---
 | 
			
		||||
                const link = document.createElement('link');
 | 
			
		||||
                link.rel = 'stylesheet';
 | 
			
		||||
                link.href = '../common/styles/glass-bypass.css';
 | 
			
		||||
                document.head.appendChild(link);
 | 
			
		||||
            }
 | 
			
		||||
        </script>
 | 
			
		||||
    </body>
 | 
			
		||||
 | 
			
		||||
@ -1,89 +0,0 @@
 | 
			
		||||
const crypto = require('crypto');
 | 
			
		||||
const { app } = require('electron');
 | 
			
		||||
const os = require('os');
 | 
			
		||||
 | 
			
		||||
class CryptoService {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.algorithm = 'aes-256-gcm';
 | 
			
		||||
        this.saltLength = 32;
 | 
			
		||||
        this.tagLength = 16;
 | 
			
		||||
        this.ivLength = 16;
 | 
			
		||||
        this.iterations = 100000;
 | 
			
		||||
        this.keyLength = 32;
 | 
			
		||||
        this._derivedKey = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _getMachineId() {
 | 
			
		||||
        const machineInfo = `${os.hostname()}-${os.platform()}-${os.arch()}`;
 | 
			
		||||
        const appPath = app.getPath('userData');
 | 
			
		||||
        return crypto.createHash('sha256').update(machineInfo + appPath).digest('hex');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _deriveKey() {
 | 
			
		||||
        if (this._derivedKey) return this._derivedKey;
 | 
			
		||||
        
 | 
			
		||||
        const machineId = this._getMachineId();
 | 
			
		||||
        const salt = crypto.createHash('sha256').update('pickle-glass-salt').digest();
 | 
			
		||||
        this._derivedKey = crypto.pbkdf2Sync(machineId, salt, this.iterations, this.keyLength, 'sha256');
 | 
			
		||||
        return this._derivedKey;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    encrypt(text) {
 | 
			
		||||
        if (!text) return null;
 | 
			
		||||
        
 | 
			
		||||
        try {
 | 
			
		||||
            const iv = crypto.randomBytes(this.ivLength);
 | 
			
		||||
            const salt = crypto.randomBytes(this.saltLength);
 | 
			
		||||
            const key = this._deriveKey();
 | 
			
		||||
            
 | 
			
		||||
            const cipher = crypto.createCipheriv(this.algorithm, key, iv);
 | 
			
		||||
            
 | 
			
		||||
            const encrypted = Buffer.concat([
 | 
			
		||||
                cipher.update(text, 'utf8'),
 | 
			
		||||
                cipher.final()
 | 
			
		||||
            ]);
 | 
			
		||||
            
 | 
			
		||||
            const tag = cipher.getAuthTag();
 | 
			
		||||
            
 | 
			
		||||
            const combined = Buffer.concat([salt, iv, tag, encrypted]);
 | 
			
		||||
            return combined.toString('base64');
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[CryptoService] Encryption failed:', error.message);
 | 
			
		||||
            throw new Error('Encryption failed');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    decrypt(encryptedData) {
 | 
			
		||||
        if (!encryptedData) return null;
 | 
			
		||||
        
 | 
			
		||||
        try {
 | 
			
		||||
            const combined = Buffer.from(encryptedData, 'base64');
 | 
			
		||||
            
 | 
			
		||||
            const salt = combined.slice(0, this.saltLength);
 | 
			
		||||
            const iv = combined.slice(this.saltLength, this.saltLength + this.ivLength);
 | 
			
		||||
            const tag = combined.slice(this.saltLength + this.ivLength, this.saltLength + this.ivLength + this.tagLength);
 | 
			
		||||
            const encrypted = combined.slice(this.saltLength + this.ivLength + this.tagLength);
 | 
			
		||||
            
 | 
			
		||||
            const key = this._deriveKey();
 | 
			
		||||
            
 | 
			
		||||
            const decipher = crypto.createDecipheriv(this.algorithm, key, iv);
 | 
			
		||||
            decipher.setAuthTag(tag);
 | 
			
		||||
            
 | 
			
		||||
            const decrypted = Buffer.concat([
 | 
			
		||||
                decipher.update(encrypted),
 | 
			
		||||
                decipher.final()
 | 
			
		||||
            ]);
 | 
			
		||||
            
 | 
			
		||||
            return decrypted.toString('utf8');
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[CryptoService] Decryption failed:', error.message);
 | 
			
		||||
            throw new Error('Decryption failed');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    clearCache() {
 | 
			
		||||
        this._derivedKey = null;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = new CryptoService();
 | 
			
		||||
@ -2,7 +2,7 @@ const Store = require('electron-store');
 | 
			
		||||
const fetch = require('node-fetch');
 | 
			
		||||
const { ipcMain, webContents } = require('electron');
 | 
			
		||||
const { PROVIDERS } = require('../ai/factory');
 | 
			
		||||
const cryptoService = require('./cryptoService');
 | 
			
		||||
const encryptionService = require('./encryptionService');
 | 
			
		||||
 | 
			
		||||
class ModelStateService {
 | 
			
		||||
    constructor(authService) {
 | 
			
		||||
@ -11,8 +11,8 @@ class ModelStateService {
 | 
			
		||||
        this.state = {};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    initialize() {
 | 
			
		||||
        this._loadStateForCurrentUser();
 | 
			
		||||
    async initialize() {
 | 
			
		||||
        await this._loadStateForCurrentUser();
 | 
			
		||||
 | 
			
		||||
        this.setupIpcHandlers();
 | 
			
		||||
        console.log('[ModelStateService] Initialized.');
 | 
			
		||||
@ -64,8 +64,12 @@ class ModelStateService {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _loadStateForCurrentUser() {
 | 
			
		||||
    async _loadStateForCurrentUser() {
 | 
			
		||||
        const userId = this.authService.getCurrentUserId();
 | 
			
		||||
        
 | 
			
		||||
        // Initialize encryption service for current user
 | 
			
		||||
        await encryptionService.initializeKey(userId);
 | 
			
		||||
        
 | 
			
		||||
        const initialApiKeys = Object.keys(PROVIDERS).reduce((acc, key) => {
 | 
			
		||||
            acc[key] = null;
 | 
			
		||||
            return acc;
 | 
			
		||||
@ -83,7 +87,7 @@ class ModelStateService {
 | 
			
		||||
                this.state.apiKeys[p] = null;
 | 
			
		||||
            } else if (this.state.apiKeys[p] && p !== 'ollama' && p !== 'whisper') {
 | 
			
		||||
                try {
 | 
			
		||||
                    this.state.apiKeys[p] = cryptoService.decrypt(this.state.apiKeys[p]);
 | 
			
		||||
                    this.state.apiKeys[p] = encryptionService.decrypt(this.state.apiKeys[p]);
 | 
			
		||||
                } catch (error) {
 | 
			
		||||
                    console.error(`[ModelStateService] Failed to decrypt API key for ${p}, resetting`);
 | 
			
		||||
                    this.state.apiKeys[p] = null;
 | 
			
		||||
@ -107,7 +111,7 @@ class ModelStateService {
 | 
			
		||||
        for (const [provider, key] of Object.entries(stateToSave.apiKeys)) {
 | 
			
		||||
            if (key && provider !== 'ollama' && provider !== 'whisper') {
 | 
			
		||||
                try {
 | 
			
		||||
                    stateToSave.apiKeys[provider] = cryptoService.encrypt(key);
 | 
			
		||||
                    stateToSave.apiKeys[provider] = encryptionService.encrypt(key);
 | 
			
		||||
                } catch (error) {
 | 
			
		||||
                    console.error(`[ModelStateService] Failed to encrypt API key for ${provider}`);
 | 
			
		||||
                    stateToSave.apiKeys[provider] = null;
 | 
			
		||||
@ -213,13 +217,21 @@ class ModelStateService {
 | 
			
		||||
        const llmModels = PROVIDERS['openai-glass']?.llmModels;
 | 
			
		||||
        const sttModels = PROVIDERS['openai-glass']?.sttModels;
 | 
			
		||||
 | 
			
		||||
        if (!this.state.selectedModels.llm && llmModels?.length > 0) {
 | 
			
		||||
        // When logging in with Pickle, prioritize Pickle's models over existing selections
 | 
			
		||||
        if (virtualKey && llmModels?.length > 0) {
 | 
			
		||||
            this.state.selectedModels.llm = llmModels[0].id;
 | 
			
		||||
            console.log(`[ModelStateService] Prioritized Pickle LLM model: ${llmModels[0].id}`);
 | 
			
		||||
        }
 | 
			
		||||
        if (!this.state.selectedModels.stt && sttModels?.length > 0) {
 | 
			
		||||
        if (virtualKey && sttModels?.length > 0) {
 | 
			
		||||
            this.state.selectedModels.stt = sttModels[0].id;
 | 
			
		||||
            console.log(`[ModelStateService] Prioritized Pickle STT model: ${sttModels[0].id}`);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // If logging out (virtualKey is null), run auto-selection to find alternatives
 | 
			
		||||
        if (!virtualKey) {
 | 
			
		||||
            this._autoSelectAvailableModels();
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        this._saveState();
 | 
			
		||||
        this._logCurrentSelection();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -48,7 +48,11 @@ class WhisperService extends LocalAIServiceBase {
 | 
			
		||||
            
 | 
			
		||||
            this.modelsDir = path.join(whisperDir, 'models');
 | 
			
		||||
            this.tempDir = path.join(whisperDir, 'temp');
 | 
			
		||||
            this.whisperPath = path.join(whisperDir, 'bin', 'whisper');
 | 
			
		||||
            
 | 
			
		||||
            // Windows에서는 .exe 확장자 필요
 | 
			
		||||
            const platform = this.getPlatform();
 | 
			
		||||
            const whisperExecutable = platform === 'win32' ? 'whisper.exe' : 'whisper';
 | 
			
		||||
            this.whisperPath = path.join(whisperDir, 'bin', whisperExecutable);
 | 
			
		||||
 | 
			
		||||
            await this.ensureDirectories();
 | 
			
		||||
            await this.ensureWhisperBinary();
 | 
			
		||||
@ -304,18 +308,113 @@ class WhisperService extends LocalAIServiceBase {
 | 
			
		||||
        const tempFile = path.join(this.tempDir, 'whisper-binary.zip');
 | 
			
		||||
        
 | 
			
		||||
        try {
 | 
			
		||||
            console.log('[WhisperService] Step 1: Downloading Whisper binary...');
 | 
			
		||||
            await this.downloadWithRetry(binaryUrl, tempFile);
 | 
			
		||||
            const extractDir = path.dirname(this.whisperPath);
 | 
			
		||||
            await spawnAsync('powershell', ['-command', `Expand-Archive -Path '${tempFile}' -DestinationPath '${extractDir}' -Force`]);
 | 
			
		||||
            await fsPromises.unlink(tempFile);
 | 
			
		||||
            
 | 
			
		||||
            console.log('[WhisperService] Step 2: Extracting archive...');
 | 
			
		||||
            const extractDir = path.join(this.tempDir, 'extracted');
 | 
			
		||||
            
 | 
			
		||||
            // 임시 압축 해제 디렉토리 생성
 | 
			
		||||
            await fsPromises.mkdir(extractDir, { recursive: true });
 | 
			
		||||
            
 | 
			
		||||
            // PowerShell 명령에서 경로를 올바르게 인용
 | 
			
		||||
            const expandCommand = `Expand-Archive -Path "${tempFile}" -DestinationPath "${extractDir}" -Force`;
 | 
			
		||||
            await spawnAsync('powershell', ['-command', expandCommand]);
 | 
			
		||||
            
 | 
			
		||||
            console.log('[WhisperService] Step 3: Finding and moving whisper executable...');
 | 
			
		||||
            
 | 
			
		||||
            // 압축 해제된 디렉토리에서 whisper.exe 파일 찾기
 | 
			
		||||
            const whisperExecutables = await this.findWhisperExecutables(extractDir);
 | 
			
		||||
            
 | 
			
		||||
            if (whisperExecutables.length === 0) {
 | 
			
		||||
                throw new Error('whisper.exe not found in extracted files');
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            // 첫 번째로 찾은 whisper.exe를 목표 위치로 복사
 | 
			
		||||
            const sourceExecutable = whisperExecutables[0];
 | 
			
		||||
            const targetDir = path.dirname(this.whisperPath);
 | 
			
		||||
            await fsPromises.mkdir(targetDir, { recursive: true });
 | 
			
		||||
            await fsPromises.copyFile(sourceExecutable, this.whisperPath);
 | 
			
		||||
            
 | 
			
		||||
            console.log('[WhisperService] Step 4: Verifying installation...');
 | 
			
		||||
            
 | 
			
		||||
            // 설치 검증
 | 
			
		||||
            await fsPromises.access(this.whisperPath, fs.constants.F_OK);
 | 
			
		||||
            
 | 
			
		||||
            // whisper.exe 실행 테스트
 | 
			
		||||
            try {
 | 
			
		||||
                await spawnAsync(this.whisperPath, ['--help']);
 | 
			
		||||
                console.log('[WhisperService] Whisper executable verified successfully');
 | 
			
		||||
            } catch (testError) {
 | 
			
		||||
                console.warn('[WhisperService] Whisper executable test failed, but file exists:', testError.message);
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            console.log('[WhisperService] Step 5: Cleanup...');
 | 
			
		||||
            
 | 
			
		||||
            // 임시 파일 정리
 | 
			
		||||
            await fsPromises.unlink(tempFile).catch(() => {});
 | 
			
		||||
            await this.removeDirectory(extractDir).catch(() => {});
 | 
			
		||||
            
 | 
			
		||||
            console.log('[WhisperService] Whisper installed successfully on Windows');
 | 
			
		||||
            return true;
 | 
			
		||||
            
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[WhisperService] Windows installation failed:', error);
 | 
			
		||||
            
 | 
			
		||||
            // 실패 시 임시 파일 정리
 | 
			
		||||
            await fsPromises.unlink(tempFile).catch(() => {});
 | 
			
		||||
            await this.removeDirectory(path.join(this.tempDir, 'extracted')).catch(() => {});
 | 
			
		||||
            
 | 
			
		||||
            throw new Error(`Failed to install Whisper on Windows: ${error.message}`);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // 압축 해제된 디렉토리에서 whisper.exe 파일들을 재귀적으로 찾기
 | 
			
		||||
    async findWhisperExecutables(dir) {
 | 
			
		||||
        const executables = [];
 | 
			
		||||
        
 | 
			
		||||
        try {
 | 
			
		||||
            const items = await fsPromises.readdir(dir, { withFileTypes: true });
 | 
			
		||||
            
 | 
			
		||||
            for (const item of items) {
 | 
			
		||||
                const fullPath = path.join(dir, item.name);
 | 
			
		||||
                
 | 
			
		||||
                if (item.isDirectory()) {
 | 
			
		||||
                    const subExecutables = await this.findWhisperExecutables(fullPath);
 | 
			
		||||
                    executables.push(...subExecutables);
 | 
			
		||||
                } else if (item.isFile() && (item.name === 'whisper.exe' || item.name === 'main.exe')) {
 | 
			
		||||
                    // main.exe도 포함 (일부 빌드에서 whisper 실행파일이 main.exe로 명명됨)
 | 
			
		||||
                    executables.push(fullPath);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.warn('[WhisperService] Error reading directory:', dir, error.message);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        return executables;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // 디렉토리 재귀적 삭제
 | 
			
		||||
    async removeDirectory(dir) {
 | 
			
		||||
        try {
 | 
			
		||||
            const items = await fsPromises.readdir(dir, { withFileTypes: true });
 | 
			
		||||
            
 | 
			
		||||
            for (const item of items) {
 | 
			
		||||
                const fullPath = path.join(dir, item.name);
 | 
			
		||||
                
 | 
			
		||||
                if (item.isDirectory()) {
 | 
			
		||||
                    await this.removeDirectory(fullPath);
 | 
			
		||||
                } else {
 | 
			
		||||
                    await fsPromises.unlink(fullPath);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            await fsPromises.rmdir(dir);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.warn('[WhisperService] Error removing directory:', dir, error.message);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async installLinux() {
 | 
			
		||||
        console.log('[WhisperService] Installing Whisper on Linux...');
 | 
			
		||||
        const version = 'v1.7.6';
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										12
									
								
								src/common/styles/glass-bypass.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/common/styles/glass-bypass.css
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
			
		||||
/*
 | 
			
		||||
  이 파일은 body.has-glass 클래스가 적용되었을 때 모든 애니메이션, 트랜지션,
 | 
			
		||||
  배경, 테두리 등을 비활성화하여 깨끗한 투명 효과(Glass)를 보장합니다.
 | 
			
		||||
*/
 | 
			
		||||
body.has-glass * {
 | 
			
		||||
    animation: none !important;
 | 
			
		||||
    transition: none !important;
 | 
			
		||||
    background: transparent !important;
 | 
			
		||||
    border: none !important;
 | 
			
		||||
    box-shadow: none !important;
 | 
			
		||||
    backdrop-filter: none !important;
 | 
			
		||||
}
 | 
			
		||||
@ -78,13 +78,6 @@ function updateLayout() {
 | 
			
		||||
let movementManager = null;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const featureWindows = ['listen','ask','settings'];
 | 
			
		||||
// const featureWindows = ['listen','ask','settings','shortcut-settings'];
 | 
			
		||||
function isAllowed(name) {
 | 
			
		||||
    if (name === 'header') return true;
 | 
			
		||||
    return featureWindows.includes(name) && currentHeaderState === 'main';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function createFeatureWindows(header, namesToCreate) {
 | 
			
		||||
    // if (windowPool.has('listen')) return;
 | 
			
		||||
 | 
			
		||||
@ -97,7 +90,7 @@ function createFeatureWindows(header, namesToCreate) {
 | 
			
		||||
        hasShadow: false,
 | 
			
		||||
        skipTaskbar: true,
 | 
			
		||||
        hiddenInMissionControl: true,
 | 
			
		||||
        resizable: false,
 | 
			
		||||
        resizable: true,
 | 
			
		||||
        webPreferences: { nodeIntegration: true, contextIsolation: false },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
@ -107,8 +100,8 @@ function createFeatureWindows(header, namesToCreate) {
 | 
			
		||||
        switch (name) {
 | 
			
		||||
            case 'listen': {
 | 
			
		||||
                const listen = new BrowserWindow({
 | 
			
		||||
                    ...commonChildOptions, width:400,minWidth:400,maxWidth:400,
 | 
			
		||||
                    maxHeight:700,
 | 
			
		||||
                    ...commonChildOptions, width:400,minWidth:400,maxWidth:900,
 | 
			
		||||
                    maxHeight:900,
 | 
			
		||||
                });
 | 
			
		||||
                listen.setContentProtection(isContentProtectionOn);
 | 
			
		||||
                listen.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true});
 | 
			
		||||
@ -305,6 +298,7 @@ function createFeatureWindows(header, namesToCreate) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function destroyFeatureWindows() {
 | 
			
		||||
    const featureWindows = ['listen','ask','settings','shortcut-settings'];
 | 
			
		||||
    if (settingsHideTimer) {
 | 
			
		||||
        clearTimeout(settingsHideTimer);
 | 
			
		||||
        settingsHideTimer = null;
 | 
			
		||||
@ -337,77 +331,34 @@ function getDisplayById(displayId) {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function toggleAllWindowsVisibility(movementManager) {
 | 
			
		||||
function toggleAllWindowsVisibility() {
 | 
			
		||||
    const header = windowPool.get('header');
 | 
			
		||||
    if (!header) return;
 | 
			
		||||
  
 | 
			
		||||
    if (header.isVisible()) {
 | 
			
		||||
        console.log('[Visibility] Smart hiding - calculating nearest edge');
 | 
			
		||||
 | 
			
		||||
        const headerBounds = header.getBounds();
 | 
			
		||||
        const display = screen.getPrimaryDisplay();
 | 
			
		||||
        const { width: screenWidth, height: screenHeight } = display.workAreaSize;
 | 
			
		||||
 | 
			
		||||
        const centerX = headerBounds.x + headerBounds.width / 2;
 | 
			
		||||
        const centerY = headerBounds.y + headerBounds.height / 2;
 | 
			
		||||
 | 
			
		||||
        const distances = {
 | 
			
		||||
            top: centerY,
 | 
			
		||||
            bottom: screenHeight - centerY,
 | 
			
		||||
            left: centerX,
 | 
			
		||||
            right: screenWidth - centerX,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const nearestEdge = Object.keys(distances).reduce((nearest, edge) => (distances[edge] < distances[nearest] ? edge : nearest));
 | 
			
		||||
 | 
			
		||||
        console.log(`[Visibility] Nearest edge: ${nearestEdge} (distance: ${distances[nearestEdge].toFixed(1)}px)`);
 | 
			
		||||
 | 
			
		||||
      lastVisibleWindows.clear();
 | 
			
		||||
        lastVisibleWindows.add('header');
 | 
			
		||||
  
 | 
			
		||||
      windowPool.forEach((win, name) => {
 | 
			
		||||
            if (win.isVisible()) {
 | 
			
		||||
        if (win && !win.isDestroyed() && win.isVisible()) {
 | 
			
		||||
          lastVisibleWindows.add(name);
 | 
			
		||||
                if (name !== 'header') {
 | 
			
		||||
                    // win.webContents.send('window-hide-animation');
 | 
			
		||||
                    // setTimeout(() => {
 | 
			
		||||
                    //     if (!win.isDestroyed()) {
 | 
			
		||||
                    //         win.hide();
 | 
			
		||||
                    //     }
 | 
			
		||||
                    // }, 200);
 | 
			
		||||
                    win.hide();
 | 
			
		||||
                }
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
  
 | 
			
		||||
        console.log('[Visibility] Visible windows before hide:', Array.from(lastVisibleWindows));
 | 
			
		||||
 | 
			
		||||
        movementManager.hideToEdge(nearestEdge, () => {
 | 
			
		||||
            header.hide();
 | 
			
		||||
            console.log('[Visibility] Smart hide completed');
 | 
			
		||||
        }, { instant: true });
 | 
			
		||||
    } else {
 | 
			
		||||
        console.log('[Visibility] Smart showing from hidden position');
 | 
			
		||||
        console.log('[Visibility] Restoring windows:', Array.from(lastVisibleWindows));
 | 
			
		||||
 | 
			
		||||
        header.show();
 | 
			
		||||
 | 
			
		||||
        movementManager.showFromEdge(() => {
 | 
			
		||||
      lastVisibleWindows.forEach(name => {
 | 
			
		||||
        if (name === 'header') return;
 | 
			
		||||
        const win = windowPool.get(name);
 | 
			
		||||
                if (win && !win.isDestroyed()) {
 | 
			
		||||
        if (win && !win.isDestroyed()) win.hide();
 | 
			
		||||
      });
 | 
			
		||||
      header.hide();
 | 
			
		||||
  
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  
 | 
			
		||||
    lastVisibleWindows.forEach(name => {
 | 
			
		||||
      const win = windowPool.get(name);
 | 
			
		||||
      if (win && !win.isDestroyed())
 | 
			
		||||
        win.show();
 | 
			
		||||
                    win.webContents.send('window-show-animation');
 | 
			
		||||
                }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
            setImmediate(updateLayout);
 | 
			
		||||
            setTimeout(updateLayout, 120);
 | 
			
		||||
 | 
			
		||||
            console.log('[Visibility] Smart show completed');
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -464,6 +415,7 @@ function createWindows() {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    windowPool.set('header', header);
 | 
			
		||||
    header.on('moved', updateLayout);
 | 
			
		||||
    layoutManager = new WindowLayoutManager(windowPool);
 | 
			
		||||
 | 
			
		||||
    header.webContents.once('dom-ready', () => {
 | 
			
		||||
@ -507,25 +459,38 @@ function createWindows() {
 | 
			
		||||
        updateLayout();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('toggle-all-windows-visibility', () => toggleAllWindowsVisibility(movementManager));
 | 
			
		||||
    ipcMain.handle('toggle-all-windows-visibility', () => toggleAllWindowsVisibility());
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('toggle-feature', async (event, featureName) => {
 | 
			
		||||
        if (!windowPool.get(featureName) && currentHeaderState === 'main') {
 | 
			
		||||
            createFeatureWindows(windowPool.get('header'));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const windowToToggle = windowPool.get(featureName);
 | 
			
		||||
 | 
			
		||||
        if (windowToToggle) {
 | 
			
		||||
        const header = windowPool.get('header');
 | 
			
		||||
        if (featureName === 'listen') {
 | 
			
		||||
            console.log(`[WindowManager] Toggling feature: ${featureName}`);
 | 
			
		||||
            const listenWindow = windowPool.get(featureName);
 | 
			
		||||
            const listenService = global.listenService;
 | 
			
		||||
            if (listenService && listenService.isSessionActive()) {
 | 
			
		||||
                console.log('[WindowManager] Listen session is active, closing it via toggle.');
 | 
			
		||||
                await listenService.closeSession();
 | 
			
		||||
                    return;
 | 
			
		||||
                listenWindow.webContents.send('session-state-changed', { isActive: false });
 | 
			
		||||
                header.webContents.send('session-state-text', 'Done');
 | 
			
		||||
                // return;
 | 
			
		||||
            } else {
 | 
			
		||||
                if (listenWindow.isVisible()) {
 | 
			
		||||
                    listenWindow.webContents.send('window-hide-animation');
 | 
			
		||||
                    listenWindow.webContents.send('session-state-changed', { isActive: false });
 | 
			
		||||
                    header.webContents.send('session-state-text', 'Listen');
 | 
			
		||||
                } else {
 | 
			
		||||
                    listenWindow.show();
 | 
			
		||||
                    updateLayout();
 | 
			
		||||
                    listenWindow.webContents.send('window-show-animation');
 | 
			
		||||
                    await listenService.initializeSession();
 | 
			
		||||
                    listenWindow.webContents.send('session-state-changed', { isActive: true });
 | 
			
		||||
                    header.webContents.send('session-state-text', 'Stop');
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            console.log(`[WindowManager] Toggling feature: ${featureName}`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (featureName === 'ask') {
 | 
			
		||||
@ -590,13 +555,6 @@ function createWindows() {
 | 
			
		||||
                    } else {
 | 
			
		||||
                        console.log('[WindowManager] No response found, closing window');
 | 
			
		||||
                        askWindow.webContents.send('window-hide-animation');
 | 
			
		||||
 | 
			
		||||
                        setTimeout(() => {
 | 
			
		||||
                            if (!askWindow.isDestroyed()) {
 | 
			
		||||
                                askWindow.hide();
 | 
			
		||||
                                updateLayout();
 | 
			
		||||
                            }
 | 
			
		||||
                        }, 250);
 | 
			
		||||
                    }
 | 
			
		||||
                } catch (error) {
 | 
			
		||||
                    console.error('[WindowManager] Error checking Ask window state:', error);
 | 
			
		||||
@ -610,38 +568,29 @@ function createWindows() {
 | 
			
		||||
                askWindow.webContents.send('window-show-animation');
 | 
			
		||||
                askWindow.webContents.send('window-did-show');
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            const windowToToggle = windowPool.get(featureName);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
            if (windowToToggle) {
 | 
			
		||||
                if (windowToToggle.isDestroyed()) {
 | 
			
		||||
        if (featureName === 'settings') {
 | 
			
		||||
            const settingsWindow = windowPool.get(featureName);
 | 
			
		||||
 | 
			
		||||
            if (settingsWindow) {
 | 
			
		||||
                if (settingsWindow.isDestroyed()) {
 | 
			
		||||
                    console.error(`Window ${featureName} is destroyed, cannot toggle`);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (windowToToggle.isVisible()) {
 | 
			
		||||
                if (settingsWindow.isVisible()) {
 | 
			
		||||
                    if (featureName === 'settings') {
 | 
			
		||||
                        windowToToggle.webContents.send('settings-window-hide-animation');
 | 
			
		||||
                        settingsWindow.webContents.send('settings-window-hide-animation');
 | 
			
		||||
                    } else {
 | 
			
		||||
                        windowToToggle.webContents.send('window-hide-animation');
 | 
			
		||||
                        settingsWindow.webContents.send('window-hide-animation');
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    setTimeout(() => {
 | 
			
		||||
                        if (!windowToToggle.isDestroyed()) {
 | 
			
		||||
                            windowToToggle.hide();
 | 
			
		||||
                            updateLayout();
 | 
			
		||||
                        }
 | 
			
		||||
                    }, 250);
 | 
			
		||||
                } else {
 | 
			
		||||
                    try {
 | 
			
		||||
                        windowToToggle.show();
 | 
			
		||||
                        settingsWindow.show();
 | 
			
		||||
                        updateLayout();
 | 
			
		||||
 | 
			
		||||
                        if (featureName === 'listen') {
 | 
			
		||||
                            windowToToggle.webContents.send('start-listening-session');
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        windowToToggle.webContents.send('window-show-animation');
 | 
			
		||||
                        settingsWindow.webContents.send('window-show-animation');
 | 
			
		||||
                    } catch (e) {
 | 
			
		||||
                        console.error('Error showing window:', e);
 | 
			
		||||
                    }
 | 
			
		||||
@ -767,9 +716,17 @@ function setupIpcHandlers(movementManager) {
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.on('show-window', (event, args) => {
 | 
			
		||||
        const { name, bounds } = typeof args === 'object' && args !== null ? args : { name: args, bounds: null };
 | 
			
		||||
        const win = windowPool.get(name);
 | 
			
		||||
    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.on('show-settings-window', (event, bounds) => {
 | 
			
		||||
        if (!bounds) return;  
 | 
			
		||||
        const win = windowPool.get('settings');
 | 
			
		||||
 | 
			
		||||
        if (win && !win.isDestroyed()) {
 | 
			
		||||
            if (settingsHideTimer) {
 | 
			
		||||
@ -777,7 +734,6 @@ function setupIpcHandlers(movementManager) {
 | 
			
		||||
                settingsHideTimer = null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (name === 'settings') {
 | 
			
		||||
            // Adjust position based on button bounds
 | 
			
		||||
            const header = windowPool.get('header');
 | 
			
		||||
            const headerBounds = header?.getBounds() ?? { x: 0, y: 0 };
 | 
			
		||||
@ -795,60 +751,43 @@ function setupIpcHandlers(movementManager) {
 | 
			
		||||
            win.setBounds({ x, y });
 | 
			
		||||
            win.__lockedByButton = true;
 | 
			
		||||
            console.log(`[WindowManager] Positioning settings window at (${x}, ${y}) based on button bounds.`);
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            win.show();
 | 
			
		||||
            win.moveTop();
 | 
			
		||||
 | 
			
		||||
            if (name === 'settings') {
 | 
			
		||||
            win.setAlwaysOnTop(true);
 | 
			
		||||
        }
 | 
			
		||||
            // updateLayout();
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.on('hide-window', (event, name) => {
 | 
			
		||||
        const window = windowPool.get(name);
 | 
			
		||||
    ipcMain.on('hide-settings-window', (event) => {
 | 
			
		||||
        const window = windowPool.get("settings");
 | 
			
		||||
        if (window && !window.isDestroyed()) {
 | 
			
		||||
            if (name === 'settings') {
 | 
			
		||||
            if (settingsHideTimer) {
 | 
			
		||||
                clearTimeout(settingsHideTimer);
 | 
			
		||||
            }
 | 
			
		||||
            settingsHideTimer = setTimeout(() => {
 | 
			
		||||
                    // window.setAlwaysOnTop(false);
 | 
			
		||||
                    // window.hide();
 | 
			
		||||
                if (window && !window.isDestroyed()) {
 | 
			
		||||
                    window.setAlwaysOnTop(false);
 | 
			
		||||
                    window.hide();
 | 
			
		||||
                }
 | 
			
		||||
                settingsHideTimer = null;
 | 
			
		||||
            }, 200);
 | 
			
		||||
            } else {
 | 
			
		||||
                window.hide();
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            window.__lockedByButton = false;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.on('cancel-hide-window', (event, name) => {
 | 
			
		||||
        if (name === 'settings' && settingsHideTimer) {
 | 
			
		||||
    ipcMain.on('cancel-hide-settings-window', (event) => {
 | 
			
		||||
        if (settingsHideTimer) {
 | 
			
		||||
            clearTimeout(settingsHideTimer);
 | 
			
		||||
            settingsHideTimer = null;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('hide-all', () => {
 | 
			
		||||
        windowPool.forEach(win => {
 | 
			
		||||
            if (win.isFocused()) return;
 | 
			
		||||
            win.hide();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('quit-application', () => {
 | 
			
		||||
        app.quit();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('is-window-visible', (event, windowName) => {
 | 
			
		||||
    ipcMain.handle('is-ask-window-visible', (event, windowName) => {
 | 
			
		||||
        const window = windowPool.get(windowName);
 | 
			
		||||
        if (window && !window.isDestroyed()) {
 | 
			
		||||
            return window.isVisible();
 | 
			
		||||
@ -882,15 +821,6 @@ function setupIpcHandlers(movementManager) {
 | 
			
		||||
            destroyFeatureWindows();
 | 
			
		||||
        }
 | 
			
		||||
        loadAndRegisterShortcuts(movementManager);
 | 
			
		||||
 | 
			
		||||
        for (const [name, win] of windowPool) {
 | 
			
		||||
            if (!isAllowed(name) && !win.isDestroyed()) {
 | 
			
		||||
                win.hide();
 | 
			
		||||
            }
 | 
			
		||||
            if (isAllowed(name) && win.isVisible()) {
 | 
			
		||||
                win.show();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.on('update-keybinds', (event, newKeybinds) => {
 | 
			
		||||
@ -963,9 +893,6 @@ function setupIpcHandlers(movementManager) {
 | 
			
		||||
 | 
			
		||||
    setupApiKeyIPC();
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('resize-window', () => {});
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('resize-for-view', () => {});
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('resize-header-window', (event, { width, height }) => {
 | 
			
		||||
        const header = windowPool.get('header');
 | 
			
		||||
@ -1018,74 +945,19 @@ function setupIpcHandlers(movementManager) {
 | 
			
		||||
        return { success: false, error: 'Header window not found' };
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.on('header-animation-complete', (event, state) => {
 | 
			
		||||
    ipcMain.on('header-animation-finished', (event, state) => {
 | 
			
		||||
        const header = windowPool.get('header');
 | 
			
		||||
        if (!header) return;
 | 
			
		||||
        if (!header || header.isDestroyed()) return;
 | 
			
		||||
    
 | 
			
		||||
        if (state === 'hidden') {
 | 
			
		||||
            header.hide();
 | 
			
		||||
            console.log('[WindowManager] Header hidden after animation.');
 | 
			
		||||
        } else if (state === 'visible') {
 | 
			
		||||
            lastVisibleWindows.forEach(name => {
 | 
			
		||||
                if (name === 'header') return;
 | 
			
		||||
                const win = windowPool.get(name);
 | 
			
		||||
                if (win) win.show();
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            setImmediate(updateLayout);
 | 
			
		||||
            setTimeout(updateLayout, 120);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
 | 
			
		||||
            console.log('[WindowManager] Header shown after animation.');
 | 
			
		||||
            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();
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('move-window-step', (event, direction) => {
 | 
			
		||||
        if (movementManager) {
 | 
			
		||||
@ -1099,13 +971,6 @@ function setupIpcHandlers(movementManager) {
 | 
			
		||||
            console.log(`[WindowManager] Force closing window: ${windowName}`);
 | 
			
		||||
 | 
			
		||||
            window.webContents.send('window-hide-animation');
 | 
			
		||||
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                if (!window.isDestroyed()) {
 | 
			
		||||
                    window.hide();
 | 
			
		||||
                    updateLayout();
 | 
			
		||||
                }
 | 
			
		||||
            }, 250);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@ -1399,7 +1264,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementMan
 | 
			
		||||
    if (state === 'apikey') {
 | 
			
		||||
        if (keybinds.toggleVisibility) {
 | 
			
		||||
            try {
 | 
			
		||||
                globalShortcut.register(keybinds.toggleVisibility, () => toggleAllWindowsVisibility(movementManager));
 | 
			
		||||
                globalShortcut.register(keybinds.toggleVisibility, () => toggleAllWindowsVisibility());
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                console.error(`Failed to register toggleVisibility (${keybinds.toggleVisibility}):`, error);
 | 
			
		||||
            }
 | 
			
		||||
@ -1435,7 +1300,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementMan
 | 
			
		||||
            let callback;
 | 
			
		||||
            switch(action) {
 | 
			
		||||
                case 'toggleVisibility':
 | 
			
		||||
                    callback = () => toggleAllWindowsVisibility(movementManager);
 | 
			
		||||
                    callback = () => toggleAllWindowsVisibility();
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'nextStep':
 | 
			
		||||
                    callback = () => {
 | 
			
		||||
@ -1605,9 +1470,6 @@ module.exports = {
 | 
			
		||||
    createWindows,
 | 
			
		||||
    windowPool,
 | 
			
		||||
    fixedYPosition,
 | 
			
		||||
    //////// before_modelStateService ////////
 | 
			
		||||
    // setApiKey,
 | 
			
		||||
    //////// before_modelStateService ////////
 | 
			
		||||
    getStoredApiKey,
 | 
			
		||||
    getStoredProvider,
 | 
			
		||||
    getCurrentModelInfo,
 | 
			
		||||
 | 
			
		||||
@ -41,56 +41,6 @@ export class AskView extends LitElement {
 | 
			
		||||
            pointer-events: none;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @keyframes slideUp {
 | 
			
		||||
            0% {
 | 
			
		||||
                opacity: 1;
 | 
			
		||||
                transform: translateY(0) scale(1);
 | 
			
		||||
                filter: blur(0px);
 | 
			
		||||
            }
 | 
			
		||||
            30% {
 | 
			
		||||
                opacity: 0.7;
 | 
			
		||||
                transform: translateY(-20%) scale(0.98);
 | 
			
		||||
                filter: blur(0.5px);
 | 
			
		||||
            }
 | 
			
		||||
            70% {
 | 
			
		||||
                opacity: 0.3;
 | 
			
		||||
                transform: translateY(-80%) scale(0.92);
 | 
			
		||||
                filter: blur(1.5px);
 | 
			
		||||
            }
 | 
			
		||||
            100% {
 | 
			
		||||
                opacity: 0;
 | 
			
		||||
                transform: translateY(-150%) scale(0.85);
 | 
			
		||||
                filter: blur(2px);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @keyframes slideDown {
 | 
			
		||||
            0% {
 | 
			
		||||
                opacity: 0;
 | 
			
		||||
                transform: translateY(-150%) scale(0.85);
 | 
			
		||||
                filter: blur(2px);
 | 
			
		||||
            }
 | 
			
		||||
            30% {
 | 
			
		||||
                opacity: 0.5;
 | 
			
		||||
                transform: translateY(-50%) scale(0.92);
 | 
			
		||||
                filter: blur(1px);
 | 
			
		||||
            }
 | 
			
		||||
            65% {
 | 
			
		||||
                opacity: 0.9;
 | 
			
		||||
                transform: translateY(-5%) scale(0.99);
 | 
			
		||||
                filter: blur(0.2px);
 | 
			
		||||
            }
 | 
			
		||||
            85% {
 | 
			
		||||
                opacity: 0.98;
 | 
			
		||||
                transform: translateY(2%) scale(1.005);
 | 
			
		||||
                filter: blur(0px);
 | 
			
		||||
            }
 | 
			
		||||
            100% {
 | 
			
		||||
                opacity: 1;
 | 
			
		||||
                transform: translateY(0) scale(1);
 | 
			
		||||
                filter: blur(0px);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        * {
 | 
			
		||||
            font-family: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
 | 
			
		||||
@ -252,20 +202,6 @@ export class AskView extends LitElement {
 | 
			
		||||
            animation: fadeInOut 0.3s ease-in-out;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @keyframes fadeInOut {
 | 
			
		||||
            0% {
 | 
			
		||||
                opacity: 1;
 | 
			
		||||
                transform: translateY(0);
 | 
			
		||||
            }
 | 
			
		||||
            50% {
 | 
			
		||||
                opacity: 0;
 | 
			
		||||
                transform: translateY(-10px);
 | 
			
		||||
            }
 | 
			
		||||
            100% {
 | 
			
		||||
                opacity: 1;
 | 
			
		||||
                transform: translateY(0);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .header-right {
 | 
			
		||||
            display: flex;
 | 
			
		||||
@ -422,19 +358,6 @@ export class AskView extends LitElement {
 | 
			
		||||
            animation-delay: 0.4s;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @keyframes pulse {
 | 
			
		||||
            0%,
 | 
			
		||||
            80%,
 | 
			
		||||
            100% {
 | 
			
		||||
                opacity: 0.3;
 | 
			
		||||
                transform: scale(0.8);
 | 
			
		||||
            }
 | 
			
		||||
            40% {
 | 
			
		||||
                opacity: 1;
 | 
			
		||||
                transform: scale(1.2);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .response-line {
 | 
			
		||||
            position: relative;
 | 
			
		||||
            padding: 2px 0;
 | 
			
		||||
@ -492,7 +415,7 @@ export class AskView extends LitElement {
 | 
			
		||||
            background: rgba(0, 0, 0, 0.1);
 | 
			
		||||
            border-top: 1px solid rgba(255, 255, 255, 0.1);
 | 
			
		||||
            flex-shrink: 0;
 | 
			
		||||
            transition: all 0.3s ease-in-out;
 | 
			
		||||
            transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out;
 | 
			
		||||
            transform-origin: bottom;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -596,42 +519,6 @@ export class AskView extends LitElement {
 | 
			
		||||
            color: rgba(255, 255, 255, 0.5);
 | 
			
		||||
            font-size: 14px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /* ────────────────[ GLASS BYPASS ]─────────────── */
 | 
			
		||||
        :host-context(body.has-glass) .ask-container,
 | 
			
		||||
        :host-context(body.has-glass) .response-header,
 | 
			
		||||
        :host-context(body.has-glass) .response-icon,
 | 
			
		||||
        :host-context(body.has-glass) .copy-button,
 | 
			
		||||
        :host-context(body.has-glass) .close-button,
 | 
			
		||||
        :host-context(body.has-glass) .line-copy-button,
 | 
			
		||||
        :host-context(body.has-glass) .text-input-container,
 | 
			
		||||
        :host-context(body.has-glass) .response-container pre,
 | 
			
		||||
        :host-context(body.has-glass) .response-container p code,
 | 
			
		||||
        :host-context(body.has-glass) .response-container pre code {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
            border: none !important;
 | 
			
		||||
            outline: none !important;
 | 
			
		||||
            box-shadow: none !important;
 | 
			
		||||
            filter: none !important;
 | 
			
		||||
            backdrop-filter: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host-context(body.has-glass) .ask-container::before {
 | 
			
		||||
            display: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host-context(body.has-glass) .copy-button:hover,
 | 
			
		||||
        :host-context(body.has-glass) .close-button:hover,
 | 
			
		||||
        :host-context(body.has-glass) .line-copy-button,
 | 
			
		||||
        :host-context(body.has-glass) .line-copy-button:hover,
 | 
			
		||||
        :host-context(body.has-glass) .response-line:hover {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host-context(body.has-glass) .response-container::-webkit-scrollbar-track,
 | 
			
		||||
        :host-context(body.has-glass) .response-container::-webkit-scrollbar-thumb {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
        }
 | 
			
		||||
    `;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
@ -654,6 +541,7 @@ export class AskView extends LitElement {
 | 
			
		||||
        this.handleStreamChunk = this.handleStreamChunk.bind(this);
 | 
			
		||||
        this.handleStreamEnd = this.handleStreamEnd.bind(this);
 | 
			
		||||
        this.handleSendText = this.handleSendText.bind(this);
 | 
			
		||||
        this.handleGlobalSendRequest = this.handleGlobalSendRequest.bind(this);
 | 
			
		||||
        this.handleTextKeydown = this.handleTextKeydown.bind(this);
 | 
			
		||||
        this.closeResponsePanel = this.closeResponsePanel.bind(this);
 | 
			
		||||
        this.handleCopy = this.handleCopy.bind(this);
 | 
			
		||||
@ -669,7 +557,6 @@ export class AskView extends LitElement {
 | 
			
		||||
        this.loadLibraries();
 | 
			
		||||
 | 
			
		||||
        // --- Resize helpers ---
 | 
			
		||||
        this.adjustHeightThrottle = null;
 | 
			
		||||
        this.isThrottled = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1333,6 +1220,14 @@ export class AskView extends LitElement {
 | 
			
		||||
 | 
			
		||||
    handleGlobalSendRequest() {
 | 
			
		||||
        const textInput = this.shadowRoot?.getElementById('textInput');
 | 
			
		||||
 | 
			
		||||
        if (!this.showTextInput) {
 | 
			
		||||
            this.showTextInput = true;
 | 
			
		||||
            this.requestUpdate();
 | 
			
		||||
            this.focusTextInput();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!textInput) return;
 | 
			
		||||
 | 
			
		||||
        textInput.focus();
 | 
			
		||||
@ -1462,12 +1357,11 @@ export class AskView extends LitElement {
 | 
			
		||||
    adjustWindowHeightThrottled() {
 | 
			
		||||
        if (this.isThrottled) return;
 | 
			
		||||
 | 
			
		||||
        this.adjustWindowHeight();
 | 
			
		||||
        this.isThrottled = true;
 | 
			
		||||
 | 
			
		||||
        this.adjustHeightThrottle = setTimeout(() => {
 | 
			
		||||
        requestAnimationFrame(() => {
 | 
			
		||||
            this.adjustWindowHeight();
 | 
			
		||||
            this.isThrottled = false;
 | 
			
		||||
        }, 16);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -27,56 +27,6 @@ export class AssistantView extends LitElement {
 | 
			
		||||
            pointer-events: none;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @keyframes slideUp {
 | 
			
		||||
            0% {
 | 
			
		||||
                opacity: 1;
 | 
			
		||||
                transform: translateY(0) scale(1);
 | 
			
		||||
                filter: blur(0px);
 | 
			
		||||
            }
 | 
			
		||||
            30% {
 | 
			
		||||
                opacity: 0.7;
 | 
			
		||||
                transform: translateY(-20%) scale(0.98);
 | 
			
		||||
                filter: blur(0.5px);
 | 
			
		||||
            }
 | 
			
		||||
            70% {
 | 
			
		||||
                opacity: 0.3;
 | 
			
		||||
                transform: translateY(-80%) scale(0.92);
 | 
			
		||||
                filter: blur(1.5px);
 | 
			
		||||
            }
 | 
			
		||||
            100% {
 | 
			
		||||
                opacity: 0;
 | 
			
		||||
                transform: translateY(-150%) scale(0.85);
 | 
			
		||||
                filter: blur(2px);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @keyframes slideDown {
 | 
			
		||||
            0% {
 | 
			
		||||
                opacity: 0;
 | 
			
		||||
                transform: translateY(-150%) scale(0.85);
 | 
			
		||||
                filter: blur(2px);
 | 
			
		||||
            }
 | 
			
		||||
            30% {
 | 
			
		||||
                opacity: 0.5;
 | 
			
		||||
                transform: translateY(-50%) scale(0.92);
 | 
			
		||||
                filter: blur(1px);
 | 
			
		||||
            }
 | 
			
		||||
            65% {
 | 
			
		||||
                opacity: 0.9;
 | 
			
		||||
                transform: translateY(-5%) scale(0.99);
 | 
			
		||||
                filter: blur(0.2px);
 | 
			
		||||
            }
 | 
			
		||||
            85% {
 | 
			
		||||
                opacity: 0.98;
 | 
			
		||||
                transform: translateY(2%) scale(1.005);
 | 
			
		||||
                filter: blur(0px);
 | 
			
		||||
            }
 | 
			
		||||
            100% {
 | 
			
		||||
                opacity: 1;
 | 
			
		||||
                transform: translateY(0) scale(1);
 | 
			
		||||
                filter: blur(0px);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        * {
 | 
			
		||||
            font-family: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
 | 
			
		||||
@ -245,17 +195,6 @@ export class AssistantView extends LitElement {
 | 
			
		||||
            animation: slideIn 0.3s ease forwards;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @keyframes slideIn {
 | 
			
		||||
            from {
 | 
			
		||||
                transform: translateX(10%);
 | 
			
		||||
                opacity: 0;
 | 
			
		||||
            }
 | 
			
		||||
            to {
 | 
			
		||||
                transform: translateX(0);
 | 
			
		||||
                opacity: 1;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .bar-controls {
 | 
			
		||||
            display: flex;
 | 
			
		||||
            gap: 4px;
 | 
			
		||||
@ -349,134 +288,6 @@ export class AssistantView extends LitElement {
 | 
			
		||||
            font-size: 10px;
 | 
			
		||||
            color: rgba(255, 255, 255, 0.7);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        /* ────────────────[ GLASS BYPASS ]─────────────── */
 | 
			
		||||
        :host-context(body.has-glass) .assistant-container,
 | 
			
		||||
        :host-context(body.has-glass) .top-bar,
 | 
			
		||||
        :host-context(body.has-glass) .toggle-button,
 | 
			
		||||
        :host-context(body.has-glass) .copy-button,
 | 
			
		||||
        :host-context(body.has-glass) .transcription-container,
 | 
			
		||||
        :host-context(body.has-glass) .insights-container,
 | 
			
		||||
        :host-context(body.has-glass) .stt-message,
 | 
			
		||||
        :host-context(body.has-glass) .outline-item,
 | 
			
		||||
        :host-context(body.has-glass) .request-item,
 | 
			
		||||
        :host-context(body.has-glass) .markdown-content,
 | 
			
		||||
        :host-context(body.has-glass) .insights-container pre,
 | 
			
		||||
        :host-context(body.has-glass) .insights-container p code,
 | 
			
		||||
        :host-context(body.has-glass) .insights-container pre code {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
            border: none !important;
 | 
			
		||||
            outline: none !important;
 | 
			
		||||
            box-shadow: none !important;
 | 
			
		||||
            filter: none !important;
 | 
			
		||||
            backdrop-filter: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host-context(body.has-glass) .assistant-container::before,
 | 
			
		||||
        :host-context(body.has-glass) .assistant-container::after {
 | 
			
		||||
            display: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host-context(body.has-glass) .toggle-button:hover,
 | 
			
		||||
        :host-context(body.has-glass) .copy-button:hover,
 | 
			
		||||
        :host-context(body.has-glass) .outline-item:hover,
 | 
			
		||||
        :host-context(body.has-glass) .request-item.clickable:hover,
 | 
			
		||||
        :host-context(body.has-glass) .markdown-content:hover {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
            transform: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host-context(body.has-glass) .transcription-container::-webkit-scrollbar-track,
 | 
			
		||||
        :host-context(body.has-glass) .transcription-container::-webkit-scrollbar-thumb,
 | 
			
		||||
        :host-context(body.has-glass) .insights-container::-webkit-scrollbar-track,
 | 
			
		||||
        :host-context(body.has-glass) .insights-container::-webkit-scrollbar-thumb {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
        }
 | 
			
		||||
        :host-context(body.has-glass) * {
 | 
			
		||||
            animation: none !important;
 | 
			
		||||
            transition: none !important;
 | 
			
		||||
            transform: none !important;
 | 
			
		||||
            filter: none !important;
 | 
			
		||||
            backdrop-filter: none !important;
 | 
			
		||||
            box-shadow: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host-context(body.has-glass) .assistant-container,
 | 
			
		||||
        :host-context(body.has-glass) .stt-message,
 | 
			
		||||
        :host-context(body.has-glass) .toggle-button,
 | 
			
		||||
        :host-context(body.has-glass) .copy-button {
 | 
			
		||||
            border-radius: 0 !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host-context(body.has-glass) ::-webkit-scrollbar,
 | 
			
		||||
        :host-context(body.has-glass) ::-webkit-scrollbar-track,
 | 
			
		||||
        :host-context(body.has-glass) ::-webkit-scrollbar-thumb {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
            width: 0 !important;      /* 스크롤바 자체 숨기기 */
 | 
			
		||||
        }
 | 
			
		||||
        :host-context(body.has-glass) .assistant-container,
 | 
			
		||||
        :host-context(body.has-glass) .top-bar,
 | 
			
		||||
        :host-context(body.has-glass) .toggle-button,
 | 
			
		||||
        :host-context(body.has-glass) .copy-button,
 | 
			
		||||
        :host-context(body.has-glass) .transcription-container,
 | 
			
		||||
        :host-context(body.has-glass) .insights-container,
 | 
			
		||||
        :host-context(body.has-glass) .stt-message,
 | 
			
		||||
        :host-context(body.has-glass) .outline-item,
 | 
			
		||||
        :host-context(body.has-glass) .request-item,
 | 
			
		||||
        :host-context(body.has-glass) .markdown-content,
 | 
			
		||||
        :host-context(body.has-glass) .insights-container pre,
 | 
			
		||||
        :host-context(body.has-glass) .insights-container p code,
 | 
			
		||||
        :host-context(body.has-glass) .insights-container pre code {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
            border: none !important;
 | 
			
		||||
            outline: none !important;
 | 
			
		||||
            box-shadow: none !important;
 | 
			
		||||
            filter: none !important;
 | 
			
		||||
            backdrop-filter: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host-context(body.has-glass) .assistant-container::before,
 | 
			
		||||
        :host-context(body.has-glass) .assistant-container::after {
 | 
			
		||||
            display: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host-context(body.has-glass) .toggle-button:hover,
 | 
			
		||||
        :host-context(body.has-glass) .copy-button:hover,
 | 
			
		||||
        :host-context(body.has-glass) .outline-item:hover,
 | 
			
		||||
        :host-context(body.has-glass) .request-item.clickable:hover,
 | 
			
		||||
        :host-context(body.has-glass) .markdown-content:hover {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
            transform: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host-context(body.has-glass) .transcription-container::-webkit-scrollbar-track,
 | 
			
		||||
        :host-context(body.has-glass) .transcription-container::-webkit-scrollbar-thumb,
 | 
			
		||||
        :host-context(body.has-glass) .insights-container::-webkit-scrollbar-track,
 | 
			
		||||
        :host-context(body.has-glass) .insights-container::-webkit-scrollbar-thumb {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
        }
 | 
			
		||||
        :host-context(body.has-glass) * {
 | 
			
		||||
            animation: none !important;
 | 
			
		||||
            transition: none !important;
 | 
			
		||||
            transform: none !important;
 | 
			
		||||
            filter: none !important;
 | 
			
		||||
            backdrop-filter: none !important;
 | 
			
		||||
            box-shadow: none !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host-context(body.has-glass) .assistant-container,
 | 
			
		||||
        :host-context(body.has-glass) .stt-message,
 | 
			
		||||
        :host-context(body.has-glass) .toggle-button,
 | 
			
		||||
        :host-context(body.has-glass) .copy-button {
 | 
			
		||||
            border-radius: 0 !important;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        :host-context(body.has-glass) ::-webkit-scrollbar,
 | 
			
		||||
        :host-context(body.has-glass) ::-webkit-scrollbar-track,
 | 
			
		||||
        :host-context(body.has-glass) ::-webkit-scrollbar-thumb {
 | 
			
		||||
            background: transparent !important;
 | 
			
		||||
            width: 0 !important;
 | 
			
		||||
        }
 | 
			
		||||
    `;
 | 
			
		||||
 | 
			
		||||
    static properties = {
 | 
			
		||||
 | 
			
		||||
@ -144,7 +144,6 @@ class ListenService {
 | 
			
		||||
 | 
			
		||||
            console.log('✅ Listen service initialized successfully.');
 | 
			
		||||
            
 | 
			
		||||
            this.sendToRenderer('session-state-changed', { isActive: true });
 | 
			
		||||
            this.sendToRenderer('update-status', 'Connected. Ready to listen.');
 | 
			
		||||
            
 | 
			
		||||
            return true;
 | 
			
		||||
@ -155,6 +154,7 @@ class ListenService {
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.isInitializingSession = false;
 | 
			
		||||
            this.sendToRenderer('session-initializing', false);
 | 
			
		||||
            this.sendToRenderer('change-listen-capture-state', { status: "start" });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -179,6 +179,7 @@ class ListenService {
 | 
			
		||||
 | 
			
		||||
    async closeSession() {
 | 
			
		||||
        try {
 | 
			
		||||
            this.sendToRenderer('change-listen-capture-state', { status: "stop" });
 | 
			
		||||
            // Close STT sessions
 | 
			
		||||
            await this.sttService.closeSessions();
 | 
			
		||||
 | 
			
		||||
@ -192,7 +193,6 @@ class ListenService {
 | 
			
		||||
            this.currentSessionId = null;
 | 
			
		||||
            this.summaryService.resetConversationHistory();
 | 
			
		||||
 | 
			
		||||
            this.sendToRenderer('session-state-changed', { isActive: false });
 | 
			
		||||
            this.sendToRenderer('session-did-close');
 | 
			
		||||
 | 
			
		||||
            console.log('Listen service session closed.');
 | 
			
		||||
@ -282,9 +282,9 @@ class ListenService {
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        ipcMain.handle('close-session', async () => {
 | 
			
		||||
            return await this.closeSession();
 | 
			
		||||
        });
 | 
			
		||||
        // ipcMain.handle('close-session', async () => {
 | 
			
		||||
        //     return await this.closeSession();
 | 
			
		||||
        // });
 | 
			
		||||
 | 
			
		||||
        ipcMain.handle('update-google-search-setting', async (event, enabled) => {
 | 
			
		||||
            try {
 | 
			
		||||
 | 
			
		||||
@ -1,138 +1,30 @@
 | 
			
		||||
// renderer.js
 | 
			
		||||
const { ipcRenderer } = require('electron');
 | 
			
		||||
const listenCapture = require('./listenCapture.js');
 | 
			
		||||
const params        = new URLSearchParams(window.location.search);
 | 
			
		||||
const isListenView  = params.get('view') === 'listen';
 | 
			
		||||
 | 
			
		||||
let realtimeConversationHistory = [];
 | 
			
		||||
 | 
			
		||||
async function queryLoginState() {
 | 
			
		||||
    const userState = await ipcRenderer.invoke('get-current-user');
 | 
			
		||||
    return userState;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function pickleGlassElement() {
 | 
			
		||||
    return document.getElementById('pickle-glass');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function initializeopenai(profile = 'interview', language = 'en') {
 | 
			
		||||
    // The API key is now handled in the main process from .env file.
 | 
			
		||||
    // We just need to trigger the initialization.
 | 
			
		||||
    try {
 | 
			
		||||
        console.log(`Requesting OpenAI initialization with profile: ${profile}, language: ${language}`);
 | 
			
		||||
        const success = await ipcRenderer.invoke('initialize-openai', profile, language);
 | 
			
		||||
        if (success) {
 | 
			
		||||
            // The status will be updated via 'update-status' event from the main process.
 | 
			
		||||
            console.log('OpenAI initialization successful.');
 | 
			
		||||
        } else {
 | 
			
		||||
            console.error('OpenAI initialization failed.');
 | 
			
		||||
            const appElement = pickleGlassElement();
 | 
			
		||||
            if (appElement && typeof appElement.setStatus === 'function') {
 | 
			
		||||
                appElement.setStatus('Initialization Failed');
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        console.error('Error during OpenAI initialization IPC call:', error);
 | 
			
		||||
        const appElement = pickleGlassElement();
 | 
			
		||||
        if (appElement && typeof appElement.setStatus === 'function') {
 | 
			
		||||
            appElement.setStatus('Error');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Listen for status updates
 | 
			
		||||
ipcRenderer.on('update-status', (event, status) => {
 | 
			
		||||
    console.log('Status update:', status);
 | 
			
		||||
    pickleGlass.e().setStatus(status);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Listen for real-time STT updates
 | 
			
		||||
ipcRenderer.on('stt-update', (event, data) => {
 | 
			
		||||
    console.log('Renderer.js stt-update', data);
 | 
			
		||||
    const { speaker, text, isFinal, isPartial, timestamp } = data;
 | 
			
		||||
 | 
			
		||||
    if (isPartial) {
 | 
			
		||||
        console.log(`🔄 [${speaker} - partial]: ${text}`);
 | 
			
		||||
    } else if (isFinal) {
 | 
			
		||||
        console.log(`✅ [${speaker} - final]: ${text}`);
 | 
			
		||||
 | 
			
		||||
        const speakerText = speaker.toLowerCase();
 | 
			
		||||
        const conversationText = `${speakerText}: ${text.trim()}`;
 | 
			
		||||
 | 
			
		||||
        realtimeConversationHistory.push(conversationText);
 | 
			
		||||
 | 
			
		||||
        if (realtimeConversationHistory.length > 30) {
 | 
			
		||||
            realtimeConversationHistory = realtimeConversationHistory.slice(-30);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        console.log(`📝 Updated realtime conversation history: ${realtimeConversationHistory.length} texts`);
 | 
			
		||||
        console.log(`📋 Latest text: ${conversationText}`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (pickleGlass.e() && typeof pickleGlass.e().updateRealtimeTranscription === 'function') {
 | 
			
		||||
        pickleGlass.e().updateRealtimeTranscription({
 | 
			
		||||
            speaker,
 | 
			
		||||
            text,
 | 
			
		||||
            isFinal,
 | 
			
		||||
            isPartial,
 | 
			
		||||
            timestamp,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
ipcRenderer.on('update-structured-data', (_, structuredData) => {
 | 
			
		||||
    console.log('📥 Received structured data update:', structuredData);
 | 
			
		||||
    window.pickleGlass.structuredData = structuredData;
 | 
			
		||||
    window.pickleGlass.setStructuredData(structuredData);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
window.pickleGlass.structuredData = {
 | 
			
		||||
    summary: [],
 | 
			
		||||
    topic: { header: '', bullets: [] },
 | 
			
		||||
    actions: [],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
window.pickleGlass.setStructuredData = data => {
 | 
			
		||||
    window.pickleGlass.structuredData = data;
 | 
			
		||||
    pickleGlass.e()?.updateStructuredData?.(data);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function formatRealtimeConversationHistory() {
 | 
			
		||||
    if (realtimeConversationHistory.length === 0) return 'No conversation history available.';
 | 
			
		||||
 | 
			
		||||
    return realtimeConversationHistory.slice(-30).join('\n');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
window.pickleGlass = {
 | 
			
		||||
    initializeopenai,
 | 
			
		||||
    startCapture: listenCapture.startCapture,
 | 
			
		||||
    stopCapture: listenCapture.stopCapture,
 | 
			
		||||
    isLinux: listenCapture.isLinux,
 | 
			
		||||
    isMacOS: listenCapture.isMacOS,
 | 
			
		||||
    captureManualScreenshot: listenCapture.captureManualScreenshot,
 | 
			
		||||
    getCurrentScreenshot: listenCapture.getCurrentScreenshot,
 | 
			
		||||
    e: pickleGlassElement,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// -------------------------------------------------------
 | 
			
		||||
// 🔔 React to session state changes from the main process
 | 
			
		||||
// When the session ends (isActive === false), ensure we stop
 | 
			
		||||
// all local capture pipelines (mic, screen, etc.).
 | 
			
		||||
// -------------------------------------------------------
 | 
			
		||||
ipcRenderer.on('session-state-changed', (_event, { isActive }) => {
 | 
			
		||||
    if (!isActive) {
 | 
			
		||||
 | 
			
		||||
ipcRenderer.on('change-listen-capture-state', (_event, { status }) => {
 | 
			
		||||
    if (!isListenView) {
 | 
			
		||||
        console.log('[Renderer] Non-listen view: ignoring capture-state change');
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    if (status === "stop") {
 | 
			
		||||
        console.log('[Renderer] Session ended – stopping local capture');
 | 
			
		||||
        listenCapture.stopCapture();
 | 
			
		||||
    } else {
 | 
			
		||||
        console.log('[Renderer] New session started – clearing in-memory history and summaries');
 | 
			
		||||
 | 
			
		||||
        // Reset live conversation & analysis caches
 | 
			
		||||
        realtimeConversationHistory = [];
 | 
			
		||||
 | 
			
		||||
        const blankData = {
 | 
			
		||||
            summary: [],
 | 
			
		||||
            topic: { header: '', bullets: [] },
 | 
			
		||||
            actions: [],
 | 
			
		||||
            followUps: [],
 | 
			
		||||
        };
 | 
			
		||||
        window.pickleGlass.setStructuredData(blankData);
 | 
			
		||||
        console.log('[Renderer] Session initialized – starting local capture');
 | 
			
		||||
        listenCapture.startCapture();
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -264,7 +264,7 @@ export class SummaryView extends LitElement {
 | 
			
		||||
        super.connectedCallback();
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            ipcRenderer.on('update-structured-data', (event, data) => {
 | 
			
		||||
            ipcRenderer.on('summary-update', (event, data) => {
 | 
			
		||||
                this.structuredData = data;
 | 
			
		||||
                this.requestUpdate();
 | 
			
		||||
            });
 | 
			
		||||
@ -275,7 +275,7 @@ export class SummaryView extends LitElement {
 | 
			
		||||
        super.disconnectedCallback();
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            ipcRenderer.removeAllListeners('update-structured-data');
 | 
			
		||||
            ipcRenderer.removeAllListeners('summary-update');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -412,7 +412,7 @@ export class SummaryView extends LitElement {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                const isAskViewVisible = await ipcRenderer.invoke('is-window-visible', 'ask');
 | 
			
		||||
                const isAskViewVisible = await ipcRenderer.invoke('is-ask-window-visible', 'ask');
 | 
			
		||||
 | 
			
		||||
                if (!isAskViewVisible) {
 | 
			
		||||
                    await ipcRenderer.invoke('toggle-feature', 'ask');
 | 
			
		||||
 | 
			
		||||
@ -27,23 +27,6 @@ class SummaryService {
 | 
			
		||||
        this.currentSessionId = sessionId;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // async getApiKey() {
 | 
			
		||||
    //     const storedKey = await getStoredApiKey();
 | 
			
		||||
    //     if (storedKey) {
 | 
			
		||||
    //         console.log('[SummaryService] Using stored API key');
 | 
			
		||||
    //         return storedKey;
 | 
			
		||||
    //     }
 | 
			
		||||
 | 
			
		||||
    //     const envKey = process.env.OPENAI_API_KEY;
 | 
			
		||||
    //     if (envKey) {
 | 
			
		||||
    //         console.log('[SummaryService] Using environment API key');
 | 
			
		||||
    //         return envKey;
 | 
			
		||||
    //     }
 | 
			
		||||
 | 
			
		||||
    //     console.error('[SummaryService] No API key found in storage or environment');
 | 
			
		||||
    //     return null;
 | 
			
		||||
    // }
 | 
			
		||||
 | 
			
		||||
    sendToRenderer(channel, data) {
 | 
			
		||||
        BrowserWindow.getAllWindows().forEach(win => {
 | 
			
		||||
            if (!win.isDestroyed()) {
 | 
			
		||||
@ -327,7 +310,7 @@ Keep all points concise and build upon previous analysis if provided.`,
 | 
			
		||||
                .then(data => {
 | 
			
		||||
                    if (data) {
 | 
			
		||||
                        console.log('📤 Sending structured data to renderer');
 | 
			
		||||
                        this.sendToRenderer('update-structured-data', data);
 | 
			
		||||
                        this.sendToRenderer('summary-update', data);
 | 
			
		||||
                        
 | 
			
		||||
                        // Notify callback
 | 
			
		||||
                        if (this.onAnalysisComplete) {
 | 
			
		||||
 | 
			
		||||
@ -367,11 +367,6 @@ export class SettingsView extends LitElement {
 | 
			
		||||
            margin-right: 6px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @keyframes spin {
 | 
			
		||||
            0% { transform: rotate(0deg); }
 | 
			
		||||
            100% { transform: rotate(360deg); }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .hidden {
 | 
			
		||||
            display: none;
 | 
			
		||||
        }
 | 
			
		||||
@ -1030,14 +1025,14 @@ export class SettingsView extends LitElement {
 | 
			
		||||
    handleMouseEnter = () => {
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            ipcRenderer.send('cancel-hide-window', 'settings');
 | 
			
		||||
            ipcRenderer.send('cancel-hide-settings-window');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleMouseLeave = () => {
 | 
			
		||||
        if (window.require) {
 | 
			
		||||
            const { ipcRenderer } = window.require('electron');
 | 
			
		||||
            ipcRenderer.send('hide-window', 'settings');
 | 
			
		||||
            ipcRenderer.send('hide-settings-window');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										17
									
								
								src/index.js
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								src/index.js
									
									
									
									
									
								
							@ -199,7 +199,7 @@ app.whenReady().then(async () => {
 | 
			
		||||
        await authService.initialize();
 | 
			
		||||
 | 
			
		||||
        //////// after_modelStateService ////////
 | 
			
		||||
        modelStateService.initialize();
 | 
			
		||||
        await modelStateService.initialize();
 | 
			
		||||
        //////// after_modelStateService ////////
 | 
			
		||||
 | 
			
		||||
        listenService.setupIpcHandlers();
 | 
			
		||||
@ -396,21 +396,6 @@ function setupGeneralIpcHandlers() {
 | 
			
		||||
    const userRepository = require('./common/repositories/user');
 | 
			
		||||
    const presetRepository = require('./common/repositories/preset');
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('save-api-key', (event, apiKey) => {
 | 
			
		||||
        try {
 | 
			
		||||
            // The adapter injects the UID and handles local/firebase logic.
 | 
			
		||||
            // Assuming a default provider if not specified.
 | 
			
		||||
            userRepository.saveApiKey(apiKey, 'openai');
 | 
			
		||||
            BrowserWindow.getAllWindows().forEach(win => {
 | 
			
		||||
                win.webContents.send('api-key-updated');
 | 
			
		||||
            });
 | 
			
		||||
            return { success: true };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('IPC: Failed to save API key:', error);
 | 
			
		||||
            return { success: false, error: error.message };
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ipcMain.handle('get-user-presets', () => {
 | 
			
		||||
        // The adapter injects the UID.
 | 
			
		||||
        return presetRepository.getPresets();
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user