[
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build/release\n\non:\n  push:\n    branches:\n      - main\n\n#on: workflow_dispatch\n\njobs:\n\n  create-release:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    steps:\n      - name: Check out Git repository\n        uses: actions/checkout@v1\n\n      - name: Get package.json version\n        id: get_version\n        shell: bash\n        run: |\n          PACKAGE_VERSION=$(node -p \"require('./package.json').version\")\n          echo \"PACKAGE_VERSION=$PACKAGE_VERSION\" >> $GITHUB_ENV\n      - name: Create an empty release\n        shell: bash\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          echo \"Releasing version $PACKAGE_VERSION\"\n          gh release create \"v$PACKAGE_VERSION\" --draft \\\n            --title \"v$PACKAGE_VERSION\" \\\n            --notes-file RELEASE.md\n#            --notes \"Pinokio version $PACKAGE_VERSION.\"\n\n  windows-unsigned:\n    if: false\n    needs: \"create-release\"\n    runs-on: windows-latest\n    permissions:\n      contents: write\n    steps:\n      - name: Check out Git repository\n        uses: actions/checkout@v1\n\n      - name: Install Node.js, NPM and Yarn\n        uses: actions/setup-node@v1\n        with:\n          node-version: 22\n\n      - name: Build/release Electron app\n        id: electron-builder\n        uses: samuelmeuli/action-electron-builder@v1.6.0\n        with:\n          github_token: ${{ secrets.github_token }}\n\n          # If the commit is tagged with a version (e.g. \"v1.0.0\"),\n          # release the app after building\n          #release: ${{ startsWith(github.ref, 'refs/tags/v') }}\n          release: true\n          #args: --win --dir  # Build win-unpacked only\n          args: --win\n\n  windows:\n#    if: false\n    needs: \"create-release\"\n    runs-on: windows-latest\n    permissions:\n      contents: write\n    steps:\n      - name: Check out Git repository\n        uses: actions/checkout@v1\n\n      - name: Install Node.js, NPM and Yarn\n        uses: actions/setup-node@v1\n        with:\n          node-version: 22\n\n      - name: Build/release Electron app\n        id: electron-builder\n        uses: samuelmeuli/action-electron-builder@v1.6.0\n        with:\n          github_token: ${{ secrets.github_token }}\n\n          # If the commit is tagged with a version (e.g. \"v1.0.0\"),\n          # release the app after building\n          #release: ${{ startsWith(github.ref, 'refs/tags/v') }}\n          #release: true\n          release: false\n          #args: --win --dir  # Build win-unpacked only\n          args: --win\n\n#      - name: Check contents\n#        run: |\n#          dir dist-win32 \\\n#          dir dist-win32\\\\win-unpacked\n#        shell: cmd\n\n### sign start\n\n      - name: upload-unsigned-artifact\n        id: upload-unsigned-artifact\n        uses: actions/upload-artifact@v4\n        with:\n          #path: dist-win32\n          #path: dist-win32/win-unpacked/Pinokio.exe\n          path: dist-win32/Pinokio.exe\n          retention-days: 1\n\n      - id: Sign\n        if: ${{ runner.os == 'Windows' }}\n        uses: signpath/github-action-submit-signing-request@v1.1\n        with:\n          api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'\n          organization-id: 'd2da0df2-dc12-4516-8222-87178d5ebf3d'\n          project-slug: 'pinokio'\n          #signing-policy-slug: 'test-signing'\n          signing-policy-slug: 'release-signing'\n          github-artifact-id: '${{ steps.upload-unsigned-artifact.outputs.artifact-id }}'\n          wait-for-completion: true\n          output-artifact-directory: './signed-windows'\n          parameters: |\n            version: ${{ toJSON(github.ref_name) }}\n\n      - name: Rebuild blockmap and latest.yml from signed installer\n        shell: bash\n        run: |\n          set -euo pipefail\n          shopt -s nullglob\n\n          files=(signed-windows/*.exe)\n          if [[ ${#files[@]} -ne 1 ]]; then\n            echo \"Expected exactly one signed exe, found ${#files[@]}\" >&2\n            exit 1\n          fi\n\n          SIGNED_EXE=\"${files[0]}\"\n          EXE_BASENAME=$(basename \"$SIGNED_EXE\")\n          VERSION=$(node -p \"require('./package.json').version\")\n          OUT_DIR=dist-win32\n          mkdir -p \"$OUT_DIR\"\n\n          # Use app-builder bundled with electron-builder to regenerate blockmap for the signed binary\n          APP_BUILDER=$(node -p \"require('app-builder-bin').appBuilderPath\")\n          \"$APP_BUILDER\" blockmap --input \"$SIGNED_EXE\" --output \"$OUT_DIR/${EXE_BASENAME}.blockmap\"\n\n          EXE=\"$SIGNED_EXE\" BLOCKMAP=\"$OUT_DIR/${EXE_BASENAME}.blockmap\" VERSION=\"$VERSION\" OUT_DIR=\"$OUT_DIR\" node - <<'EOF'\n          const fs = require('fs');\n          const crypto = require('crypto');\n          const path = require('path');\n\n          const exe = process.env.EXE;\n          const blockmap = process.env.BLOCKMAP;\n          const version = process.env.VERSION;\n          const outDir = process.env.OUT_DIR;\n\n          const exeStats = fs.statSync(exe);\n          const blockmapStats = fs.statSync(blockmap);\n          const sha512 = crypto.createHash('sha512').update(fs.readFileSync(exe)).digest('base64');\n\n          const lines = [\n            `version: ${version}`,\n            `files:`,\n            `  - url: ${path.basename(exe)}`,\n            `    sha512: ${sha512}`,\n            `    size: ${exeStats.size}`,\n            `    blockMapSize: ${blockmapStats.size}`,\n            `path: ${path.basename(exe)}`,\n            `sha512: ${sha512}`,\n            `releaseDate: \"${new Date().toISOString()}\"`\n          ];\n\n          fs.writeFileSync(path.join(outDir, 'latest.yml'), lines.join('\\n'));\n          EOF\n\n#      # Replace the unsigned exe with the signed exe\n#      - name: Replace with signed exe\n#        run: |\n#          copy /Y \".\\signed-windows\\Pinokio.exe\" \".\\dist-win32\\win-unpacked\\Pinokio.exe\"\n#        shell: cmd\n\n### sign end\n\n#      # Build the final installer from the signed exe\n#      - name: Build final installer\n#        env:\n#          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n##          CSC_IDENTITY_AUTO_DISCOVERY: \"false\"   # disable any auto code-sign discovery\n##          DISABLE_CODE_SIGNING: \"true\"           # electron-builder respects this to skip signing\n#        run: |\n#          #yarn run electron-builder --win --prepackaged dist-win32/win-unpacked --publish never\n#          yarn run electron-builder --win --prepackaged dist-win32/win-unpacked --publish always\n\n\n      - name: Get package.json version\n        id: get_version\n        shell: bash\n        run: |\n          PACKAGE_VERSION=$(node -p \"require('./package.json').version\")\n          echo \"PACKAGE_VERSION=$PACKAGE_VERSION\" >> $GITHUB_ENV\n\n      - name: Publish GitHub Release with gh\n        shell: bash\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          echo \"Releasing version $PACKAGE_VERSION\"\n          #gh release create \"v$PACKAGE_VERSION\" ./signed-windows/*.exe \\\n\n          #gh release upload \"v$PACKAGE_VERSION\" ./dist-win32/*.exe .dist-win32/latest.yml ./dist-win32/*.exe.blockmap\n          gh release upload \"v$PACKAGE_VERSION\" ./signed-windows/*.exe ./dist-win32/latest.yml ./dist-win32/*.exe.blockmap --clobber\n          #gh release create \"v$PACKAGE_VERSION\" ./dist-win32/*.exe \\\n          #  --title \"Release v$PACKAGE_VERSION\" \\\n          #  --notes \"Pinokio version $PACKAGE_VERSION.\"\n\n  mac:\n#    if: false\n    needs: \"create-release\"\n    runs-on: macos-latest\n    permissions:\n      contents: write\n    steps:\n      - name: Check out Git repository\n        uses: actions/checkout@v1\n\n      - name: Install Node.js, NPM and Yarn\n        uses: actions/setup-node@v1\n        with:\n          node-version: 22\n\n#      - name: Prepare for app notarization\n#        if: startsWith(matrix.os, 'macos')\n#        # Import Apple API key for app notarization on macOS\n#        run: |\n#          mkdir -p ~/private_keys/\n#          echo '${{ secrets.api_key }}' > ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8\n\n\n      - name: Build/release Electron app\n        id: electron-builder\n        uses: samuelmeuli/action-electron-builder@v1.6.0\n        with:\n          # GitHub token, automatically provided to the action\n          # (No need to define this secret in the repo settings)\n          github_token: ${{ secrets.github_token }}\n\n          # If the commit is tagged with a version (e.g. \"v1.0.0\"),\n          # release the app after building\n          #release: ${{ startsWith(github.ref, 'refs/tags/v') }}\n          release: true\n          mac_certs: ${{ secrets.mac_certs }}\n          mac_certs_password: ${{ secrets.mac_certs_password }}\n        env:\n          # macOS notarization API key\n          #API_KEY_ID: ${{ secrets.api_key_id }}\n          #API_KEY_ISSUER_ID: ${{ secrets.api_key_issuer_id }}\n          APPLE_ID: ${{ secrets.APPLE_ID }}\n          APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}\n          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}\n\n      - name: Show notarization-error.log\n        if: failure()\n        run: cat dist-darwin/**/notarization-error.log || echo \"No notarization-error.log found\"\n\n  linux:\n#    if: false\n    needs: \"create-release\"\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        arch: [x64, arm64]\n    permissions:\n      contents: write\n    steps:\n      - name: Check out Git repository\n        uses: actions/checkout@v1\n\n      - name: Install Node.js, NPM and Yarn\n        uses: actions/setup-node@v1\n        with:\n          node-version: 22\n\n      - name: Install dependencies\n        shell: bash\n        run: |\n          if [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then\n            npm ci\n          else\n            npm install\n          fi\n\n      - name: Constrain Linux targets to matrix arch\n        shell: bash\n        env:\n          TARGET_ARCH: ${{ matrix.arch }}\n        run: |\n          node - <<'EOF'\n          const fs = require('fs');\n          const packagePath = './package.json';\n          const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));\n\n          const targetArch = process.env.TARGET_ARCH;\n          if (!targetArch) {\n            throw new Error('TARGET_ARCH is not set');\n          }\n\n          const linux = packageJson.build && packageJson.build.linux;\n          if (!linux || !Array.isArray(linux.target)) {\n            throw new Error('build.linux.target is not configured as an array');\n          }\n\n          linux.target = linux.target.map((entry) => {\n            if (typeof entry === 'string') {\n              return { target: entry, arch: [targetArch] };\n            }\n            if (entry && typeof entry === 'object') {\n              return { ...entry, arch: [targetArch] };\n            }\n            return entry;\n          });\n\n          fs.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\\n`);\n          EOF\n\n      - name: Install ARM64 parcel watcher native\n        if: matrix.arch == 'arm64'\n        shell: bash\n        run: |\n          npm_config_os=linux \\\n          npm_config_cpu=arm64 \\\n          npm_config_libc=glibc \\\n          npm_config_force=true \\\n          npm install --no-save @parcel/watcher-linux-arm64-glibc@2.5.1\n\n      - name: Build/release Linux app\n        shell: bash\n        env:\n          GITHUB_TOKEN: ${{ secrets.github_token }}\n        run: |\n          ./node_modules/.bin/electron-builder install-app-deps --platform linux --arch ${{ matrix.arch }}\n          ./node_modules/.bin/electron-builder --linux --${{ matrix.arch }} --publish never\n\n      - name: Validate ARM64 Linux packaged natives\n        if: matrix.arch == 'arm64'\n        shell: bash\n        run: |\n          set -euo pipefail\n          shopt -s nullglob\n\n          pick_file() {\n            for candidate in \"$@\"; do\n              if [ -f \"$candidate\" ]; then\n                printf '%s\\n' \"$candidate\"\n                return 0\n              fi\n            done\n            return 1\n          }\n\n          deb_files=(dist-linux/*arm64*.deb dist-linux/*aarch64*.deb)\n          if [ ${#deb_files[@]} -eq 0 ]; then\n            echo \"No ARM64 .deb artifacts found in dist-linux\" >&2\n            ls -la dist-linux || true\n            exit 1\n          fi\n\n          for deb in \"${deb_files[@]}\"; do\n            echo \"Validating $deb\"\n            workdir=\"$(mktemp -d)\"\n            dpkg-deb -x \"$deb\" \"$workdir/root\"\n\n            if ! grep -R --binary-files=without-match -q '/opt/Pinokio/pinokio-bin' \"$workdir/root\"; then\n              echo \"Could not find /opt/Pinokio/pinokio-bin fallback in extracted package\" >&2\n              exit 1\n            fi\n\n            pty_node=\"$(pick_file \\\n              \"$workdir/root/opt/Pinokio/resources/app.asar.unpacked/node_modules/@homebridge/node-pty-prebuilt-multiarch/build/Release/pty.node\" \\\n              \"$workdir/root/opt/Pinokio/resources/app.asar.unpacked/node_modules/pinokiod/node_modules/@homebridge/node-pty-prebuilt-multiarch/build/Release/pty.node\" \\\n            )\" || {\n              echo \"Missing patched pty.node in ARM64 package\" >&2\n              exit 1\n            }\n\n            watcher_node=\"$(pick_file \\\n              \"$workdir/root/opt/Pinokio/resources/app.asar.unpacked/node_modules/@parcel/watcher/build/Release/watcher.node\" \\\n              \"$workdir/root/opt/Pinokio/resources/app.asar.unpacked/node_modules/pinokiod/node_modules/@parcel/watcher/build/Release/watcher.node\" \\\n            )\" || {\n              echo \"Missing patched watcher.node in ARM64 package\" >&2\n              exit 1\n            }\n\n            watcher_platform_node=\"$(pick_file \\\n              \"$workdir/root/opt/Pinokio/resources/app.asar.unpacked/node_modules/@parcel/watcher-linux-arm64-glibc/watcher.node\" \\\n              \"$workdir/root/opt/Pinokio/resources/app.asar.unpacked/node_modules/pinokiod/node_modules/@parcel/watcher-linux-arm64-glibc/watcher.node\" \\\n            )\" || {\n              echo \"Missing @parcel/watcher-linux-arm64-glibc payload in ARM64 package\" >&2\n              exit 1\n            }\n\n            file \"$pty_node\"\n            file \"$watcher_node\"\n            file \"$watcher_platform_node\"\n\n            file \"$pty_node\" | grep -qi 'aarch64' || {\n              echo \"pty.node is not ARM64 in $deb\" >&2\n              exit 1\n            }\n            file \"$watcher_node\" | grep -qi 'aarch64' || {\n              echo \"watcher.node is not ARM64 in $deb\" >&2\n              exit 1\n            }\n            file \"$watcher_platform_node\" | grep -qi 'aarch64' || {\n              echo \"watcher-linux-arm64-glibc payload is not ARM64 in $deb\" >&2\n              exit 1\n            }\n\n            rm -rf \"$workdir\"\n          done\n\n      - name: Upload Linux release artifacts\n        shell: bash\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          set -euo pipefail\n\n          version=\"$(node -p \"require('./package.json').version\")\"\n          tag=\"v${version}\"\n\n          files=()\n          while IFS= read -r -d '' file; do\n            files+=(\"$file\")\n          done < <(find dist-linux -maxdepth 1 -type f \\\n            \\( -name '*.AppImage' -o -name '*.deb' -o -name '*.rpm' -o -name '*linux*.yml' -o -name 'latest*.yml' \\) \\\n            -print0)\n\n          if [ \"${#files[@]}\" -eq 0 ]; then\n            echo \"No Linux artifacts found in dist-linux\" >&2\n            ls -la dist-linux || true\n            exit 1\n          fi\n\n          gh release upload \"$tag\" \"${files[@]}\" --clobber\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Test\n\n#on: push\non: workflow_dispatch\n\njobs:\n  print:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Get package.json version\n        id: get_version\n        shell: bash\n#        run: echo \"PACKAGE_VERSION=$(node -p 'require(\"./package.json\").version')\" >> $GITHUB_ENV\n        #run: echo 'PACKAGE_VERSION=$(node -p \"require(\\\"./package.json\\\").version\")' >> $GITHUB_ENV\n#        run: echo \"PACKAGE_VERSION=$(node -p \\\"require('./package.json').version\\\")\" >> $GITHUB_ENV\n        run: |\n          PACKAGE_VERSION=$(node -p \"require('./package.json').version\")\n          echo \"PACKAGE_VERSION=$PACKAGE_VERSION\" >> $GITHUB_ENV\n\n      - name: Print env\n        shell: bash\n        run: echo $PACKAGE_VERSION\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\ndist\ncache\npackage-lock.json\n.claude\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright 2023 Pinokio\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Pinokio\n\nLaunch Anything.\n\n# Script Policy\n\nPinokio is a 1-click launcher for any open-source project. Think of it as a terminal application with a user-friendly interface that can programmatically interact with scripts.\n\nThis means:\n\n1. **Scripts can run anything:** Just like terminal apps can run shell scripts, Pinokio scripts can run any command, download files, and execute them. Essentially, Pinokio is a user-friendly terminal with a UI.\n2. **How scripts can be run:** There are two ways to run scripts on Pinokio:\n    1. **Write your own:** Just like writing and executing shell scripts in the terminal, you can create your own scripts and run them locally.\n    2. **Install from the \"Discover\" page:** Vetted scripts are manually listed in the directory, tracked via Git, and frozen under the official GitHub organization. These are guaranteed to be secure and safe to install.\n3. **Verified Scripts:** To be featured on the \"Discover\" page, scripts must go through the following strict process:\n    1. **Publisher Verification:** You must be personally verified to submit scripts for consideration. Contact the Pinokio admin (https://x.com/cocktailpeanut) to request verification.\n    2. **Github Organization Invitation:** Once verified, you'll be invited to the official Pinokio Factory GitHub organization as a contributor. Only members of this organization can publish scripts eligible for the \"Discover\" page. Abusing publishing privileges may result in removal from the organization.\n    3. **Repository Transfer and Freeze** To apply for a feature, you must transfer your script repository to the Pinokio Factory GitHub organization. Follow this guide: https://docs.github.com/en/repositories/creating-and-managing-repositories/transferring-a-repository\n    4. **Feature Application:** Once your repository is fully transferred and controlled by the organization, it is considered \"frozen\". You can then request to feature it on the \"Discover\" page by contacting the admin.\n    5. **Review:** The script will be thoroughly reviewed and tested by the Pinokio admin. If verified as safe, it will be featured on the \"Discover\" page.\n    6. **Troubleshooting:** If any issues arise after a script is featured, the Pinokio admin may:\n        - Delist the script from the \"Discover\" page\n        - Modify the script to resolve the issue. Since the script is under the Pinokio Factory organization, the admin has the rights to make necessary fixes.\n\n# Security\n\n## Scripts are isolated by design\n\nBy default all Pinokio scripts are stored run under an isolated location (at `~/pinokio/api`). Additionally, all binaries installed through the built-in package managers in Pinokio are installed within `~/pinokio/bin`. Basically, everything you do is stored inside `~/pinokio`. The risk factor is when a script intentionally tries to deviatte away from this.\n\nThe script verification process checks to make sure this doesn't happen.\n\nTh Pinokio script syntax was designed to make this process simpler, both by human and machines.\n\n## Scripts are open source\n\nAll scripts must be downloaded from public git repositories. The scripts are both human readable and machine readable (written in JSON syntax), so you can always check the source code before running it.\n\nHere's an example install screen, with an alert letting you know the downloaded 3rd party script is about to be run, as well as the URL to the original script repository where it was downloaded from.\n\n![install.png](install.png)\n\n## Script Verification\n\nVerified scripts are scripts that are explicitly reviewed and approved by the Pinokio admin. Because the scripts are designed to run isolated by default, and the syntax makes it easy to detect when a command intentionally tries to run things outside of the isolated environment, it is easy to detect any script that does things out of the ordinary. Here are some of the checks done by the Pinokio admin to make sure each script file is secure:\n\n1. **Path check:** When we verify the scripts, we look at the scripts to see if all commands are run inside each app's path. The script syntax was designed to make this process easy (with the `path` attribute, which declares the folder path from which to run a command, and by default the execution path is each app's path)\n2. **Venv check:** We also check to make sure every dependency installation is done within the context of each app using `venv`. This process is again made easy with the script syntax (with the `venv` attribute, which automatically activates a virtual environment and installs all dependencies there, inside each app's folder)\n3. **3rd Party Package check:** We also check that any 3rd party packages installed through Pinokio to make sure that they are installed inside the pinokio isolated environment. The built-in package mangagers (Conda, Homebrew, Pip, and NPM) install everything inside the isolated pinokio home path (`~/pinokio`) by default. Since everything runs isolated by default, verifying this is simple (by checking that there are no explicit declaration of additional code that tries to go outside of the isolated environment)\n\nHere's an example execution script that installs python dependencies:\n\n```json\n{\n  \"method\": \"shell.run\",\n  \"params\": {\n    \"message\": \"uv pip install -r requirements.txt\",\n    \"path\": \"server\",\n    \"venv\": \"venv\"\n  }\n}\n```\n\n1. First of all, by default the entire thing is run isolated in the pinokio activated conda environment, and the execution path is the downloaded app's path (for example `~/pinokio/api/myapp`)\n2. second, since the `path` is declared as `server`, the code will be run inside the `server` folder ofr the app (in this case `~/pinokio/api/myapp/server`)\n3. Third, the `venv` attribute  is included, so the python dependencies are also installed in an app-isolated manner. If the app is located at `~/pinokio/api/myapp`, the The depenencies will be stored at `~/pinokio/api/myapp/venv`\n\nThe script verification check makes sure that all these components are run locally within the constraints of each app.\n\nOf course, there are also additional checks such as:\n\n1. Checking the reputation of the repository and the developer of the original project\n2. Trhing out the app personally\n3. Making sure that the install and launch instructions actually follow the recommended instructions suggested in the original project's README.\n\nNo scripts are approved until rigorously tested.\n"
  },
  {
    "path": "RELEASE.md",
    "content": "# Pinokio Release\n\n## Code Signing Policy\n\nFree code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/).\n\n## Privacy Policy\n\nThis program will not transfer any information to other networked systems unless specifically requested by the user or the person installing or operating it.\n"
  },
  {
    "path": "after-pack.js",
    "content": "module.exports = async (context) => {\n  const chmodHandler = require('./chmod')\n  const wrapLinuxLauncher = require('./wrap-linux-launcher')\n  const patchLinuxArm64Natives = require('./patch-linux-arm64-natives')\n\n  await chmodHandler(context)\n  await wrapLinuxLauncher(context)\n  await patchLinuxArm64Natives(context)\n}\n"
  },
  {
    "path": "build/entitlements.mac.inherit.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n    <key>com.apple.security.cs.allow-jit</key>\n    <true/>\n    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>\n    <true/>\n    <key>com.apple.security.cs.disable-library-validation</key>\n    <true/>\n  </dict>\n</plist>\n"
  },
  {
    "path": "build/entitlements.mac.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n    <key>com.apple.security.cs.allow-jit</key>\n    <true/>\n    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>\n    <true/>\n    <key>com.apple.security.cs.disable-library-validation</key>\n    <true/>\n    <key>com.apple.security.device.audio-input</key>\n    <true/>\n    <key>com.apple.security.device.camera</key>\n    <true/>\n  </dict>\n</plist>\n"
  },
  {
    "path": "build/installer.nsh",
    "content": "# https://github.com/electron-userland/electron-builder/issues/6865#issuecomment-1871121350\n!macro customInit\n  Delete \"$INSTDIR\\Uninstall*.exe\"\n!macroend\n"
  },
  {
    "path": "build/sign.js",
    "content": "module.exports = async function () {\n  // no-op: prevents electron-builder from calling signtool.exe\n};\n"
  },
  {
    "path": "chmod.js",
    "content": "const exec = require('child_process').exec;\nmodule.exports = async (context) => {\n  const paths = [\n    `${context.appOutDir}/resources/app.asar.unpacked/node_modules/go-get-folder-size/dist/go-get-folder-size_linux_386/go-get-folder-size`,\n    `${context.appOutDir}/resources/app.asar.unpacked/node_modules/go-get-folder-size/dist/go-get-folder-size_linux_amd64_v1/go-get-folder-size`,\n    `${context.appOutDir}/resources/app.asar.unpacked/node_modules/go-get-folder-size/dist/go-get-folder-size_linux_arm64/go-get-folder-size`,\n  ]\n  for(let p of paths) {\n    await exec(`chmod +x \"${p}\"`);\n  }\n}\n"
  },
  {
    "path": "config.js",
    "content": "const Store = require('electron-store');\nconst packagejson = require(\"./package.json\")\nconst store = new Store();\nmodule.exports = {\n  newsfeed: (gitRemote) => {\n    return `https://pinokiocomputer.github.io/home/item?uri=${gitRemote}&display=feed`\n  },\n  profile: (gitRemote) => {\n    return `https://pinokiocomputer.github.io/home/item?uri=${gitRemote}&display=profile`\n  },\n  site: \"https://pinokio.co\",\n  discover_dark: \"https://beta.pinokio.co\",\n  discover_light: \"https://beta.pinokio.co\",\n  portal: \"https://beta.pinokio.co\",\n  docs: \"https://pinokio.co/docs\",\n  install: \"https://pinokiocomputer.github.io/program.pinokio.computer/#/?id=install\",\n  agent: \"electron\",\n  version: packagejson.version,\n  store\n}\n"
  },
  {
    "path": "full.js",
    "content": "const {app, screen, shell, BrowserWindow, BrowserView, ipcMain, dialog, clipboard, session, desktopCapturer, systemPreferences, Menu } = require('electron')\nconst windowStateKeeper = require('electron-window-state');\nconst fs = require('fs')\nconst path = require(\"path\")\nconst Pinokiod = require(\"pinokiod\")\nconst os = require('os')\nconst Updater = require('./updater')\nconst createPopupShellManager = require('./popup-shell')\nconst is_mac = process.platform.startsWith(\"darwin\")\nconst platform = os.platform()\nvar mainWindow;\nvar root_url;\nvar wins = {}\nvar pinned = {}\nvar launched\nvar theme\nvar colors\nvar splashWindow\nvar splashIcon\nvar updateBannerPayload\nvar updateBannerDismissed = false\nvar updateInfo = null\nvar updateDownloadInFlight = false\nconst updateTestMode = (() => {\n  const value = process.env.PINOKIO_TEST_UPDATE_BANNER\n  if (!value) {\n    return false\n  }\n  return ['1', 'true', 'yes', 'on'].includes(String(value).toLowerCase())\n})()\nlet updateTestInterval = null\nlet updateTestTimeout = null\nconst UPDATE_RELEASES_URL = 'https://github.com/peanutcocktail/pinokio/releases'\nconst setWindowTitleBarOverlay = (win, overlay) => {\n  if (!win || !win.setTitleBarOverlay) {\n    return\n  }\n  try {\n    win.setTitleBarOverlay(overlay)\n  } catch (e) {\n//    console.log(\"ERROR\", e)\n  }\n}\nconst applyTitleBarOverlayToAllWindows = () => {\n  if (!colors) {\n    return\n  }\n  const overlay = titleBarOverlay(colors)\n  const browserWindows = BrowserWindow.getAllWindows()\n  for (const win of browserWindows) {\n    setWindowTitleBarOverlay(win, overlay)\n  }\n}\nconst updateThemeColors = (payload = {}) => {\n  console.log(\"updateThemeColors\", payload)\n  const nextTheme = payload.theme\n  const nextColors = payload.colors\n  if (nextTheme) {\n    theme = nextTheme\n  }\n  if (nextColors) {\n    colors = nextColors\n  }\n  applyTitleBarOverlayToAllWindows()\n}\nconst stripHtmlTags = (value) => {\n  if (!value) {\n    return ''\n  }\n  return String(value).replace(/<[^>]*>/g, '')\n}\nconst buildReleaseNotesPreview = (notes) => {\n  if (!notes) {\n    return ''\n  }\n  let text = ''\n  if (Array.isArray(notes)) {\n    text = notes.map((note) => note && (note.note || note.releaseNotes || note.title || '')).join('\\n')\n  } else if (typeof notes === 'string') {\n    text = notes\n  } else {\n    text = String(notes)\n  }\n  const cleaned = stripHtmlTags(text).replace(/\\r/g, '')\n  const lines = cleaned.split('\\n').map((line) => line.trim()).filter(Boolean)\n  if (!lines.length) {\n    return ''\n  }\n  const firstLine = lines[0]\n  if (firstLine.length > 140) {\n    return `${firstLine.slice(0, 137)}...`\n  }\n  return firstLine\n}\nconst buildProgressLabel = (progress) => {\n  if (!progress || typeof progress.percent !== 'number') {\n    return ''\n  }\n  const percent = Math.round(progress.percent)\n  if (typeof progress.transferred === 'number' && typeof progress.total === 'number' && progress.total > 0) {\n    const transferred = (progress.transferred / 1024 / 1024).toFixed(1)\n    const total = (progress.total / 1024 / 1024).toFixed(1)\n    return `${percent}% (${transferred} MB of ${total} MB)`\n  }\n  return `${percent}%`\n}\nconst buildUpdateBannerPayload = (state, info, extra = {}) => {\n  const resolved = info || {}\n  return {\n    state,\n    version: resolved.version || '',\n    notesPreview: buildReleaseNotesPreview(resolved.releaseNotes),\n    releaseUrl: UPDATE_RELEASES_URL,\n    ...extra\n  }\n}\nconst clearUpdateTestTimers = () => {\n  if (updateTestInterval) {\n    clearInterval(updateTestInterval)\n    updateTestInterval = null\n  }\n  if (updateTestTimeout) {\n    clearTimeout(updateTestTimeout)\n    updateTestTimeout = null\n  }\n}\nconst showUpdateBannerTestAvailable = () => {\n  updateInfo = {\n    version: '99.9.9-test',\n    releaseNotes: 'Simulated update for banner testing.'\n  }\n  updateDownloadInFlight = false\n  updateBannerDismissed = false\n  showUpdateBanner(buildUpdateBannerPayload('available', updateInfo))\n}\nconst startUpdateBannerTestDownload = () => {\n  if (!updateInfo) {\n    showUpdateBannerTestAvailable()\n  }\n  clearUpdateTestTimers()\n  updateDownloadInFlight = true\n  let progress = 0\n  const tick = () => {\n    progress = Math.min(100, progress + 6 + Math.random() * 12)\n    showUpdateBanner(buildUpdateBannerPayload('downloading', updateInfo, {\n      progressPercent: progress,\n      notesPreview: `${Math.round(progress)}%`\n    }))\n    if (progress >= 100) {\n      clearUpdateTestTimers()\n      updateDownloadInFlight = false\n      showUpdateBanner(buildUpdateBannerPayload('ready', updateInfo))\n    }\n  }\n  tick()\n  updateTestInterval = setInterval(tick, 320)\n}\nconst simulateUpdateBannerRestart = () => {\n  clearUpdateTestTimers()\n  hideUpdateBanner()\n  updateTestTimeout = setTimeout(() => {\n    showUpdateBannerTestAvailable()\n  }, 800)\n}\nconst dispatchUpdateBanner = (payload) => {\n  updateBannerPayload = payload\n  if (!mainWindow || mainWindow.isDestroyed()) {\n    return\n  }\n  if (payload && payload.state === 'available' && updateBannerDismissed) {\n    return\n  }\n  if (mainWindow.webContents && !mainWindow.webContents.isDestroyed()) {\n    if (!mainWindow.webContents.isLoading()) {\n      mainWindow.webContents.send('pinokio:update-banner', payload)\n    }\n  }\n}\nconst showUpdateBanner = (payload) => {\n  if (payload && payload.state === 'available' && updateBannerDismissed) {\n    updateBannerPayload = payload\n    return\n  }\n  dispatchUpdateBanner(payload || updateBannerPayload)\n}\nconst hideUpdateBanner = () => {\n  dispatchUpdateBanner({ state: 'hidden' })\n}\nlet PORT\n//let PORT = 42000\n//let PORT = (platform === 'linux' ? 42000 : 80)\n\nlet config = require('./config')\n\nconst filter = function (item) {\n  return item.browserName === 'Chrome';\n};\n\nconst updater = new Updater()\nconst pinokiod = new Pinokiod(config)\nconst ENABLE_BROWSER_CONSOLE_LOG = process.env.PINOKIO_BROWSER_LOG === '1'\nconst browserConsoleState = new WeakMap()\nconst attachedConsoleListeners = new WeakSet()\nconst consoleLevelLabels = ['log', 'info', 'warn', 'error', 'debug']\nlet browserLogFilePath\nlet browserLogFileReady = false\nlet browserLogBuffer = []\nlet browserLogWritePromise = Promise.resolve()\nlet permissionHandlersInstalled = false\nlet injectorHandlersInstalled = false\nconst frameInjectorSyncState = new Map()\nconst frameInjectTargetRegistry = new Map()\nconst PINOKIO_INJECT_ISOLATED_WORLD_ID = 42000\nconst permissionPrompted = new Set()\nconst permissionPromptInFlight = new Set()\nconst safeParseUrl = (value, base) => {\n  if (!value) {\n    return null\n  }\n  try {\n    if (base) {\n      return new URL(value, base)\n    }\n    return new URL(value)\n  } catch (err) {\n    return null\n  }\n}\nconst popupNavigationGuards = new Map()\nconst isRootShellUrl = (value) => {\n  const root = safeParseUrl(root_url)\n  const target = safeParseUrl(value, root ? root.href : undefined)\n  return Boolean(root && target && target.origin === root.origin && (target.pathname || '/') === '/')\n}\nconst getHttpNavigationTarget = (value, base) => {\n  const target = safeParseUrl(value, base)\n  if (!target || (target.protocol !== 'http:' && target.protocol !== 'https:')) {\n    return null\n  }\n  return target\n}\nconst openNonPinokioNavigationInPopup = ({ event, owner, url, frame } = {}) => {\n  const target = getHttpNavigationTarget(url, root_url || undefined)\n  if (!target || !owner || owner.isDestroyed?.() || owner.__pinokioPopupShell) {\n    return false\n  }\n  if (popupShellManager.isPinokioWindowUrl(target.href, root_url)) {\n    return false\n  }\n  if (event && typeof event.preventDefault === 'function') {\n    event.preventDefault()\n  }\n  const frameId = frame && (frame.frameToken || frame.frameTreeNodeId || frame.routingId)\n  if (frameId) {\n    const guardKey = `${owner.id}:${frameId}:${target.href}`\n    const now = Date.now()\n    const last = popupNavigationGuards.get(guardKey) || 0\n    popupNavigationGuards.set(guardKey, now)\n    setTimeout(() => {\n      if (popupNavigationGuards.get(guardKey) === now) {\n        popupNavigationGuards.delete(guardKey)\n      }\n    }, 1500)\n    if (now - last < 1500) {\n      return true\n    }\n  }\n  popupShellManager.openExternalWindow({ url: target.href })\n  return true\n}\nconst installForceDestroyOnClose = (win) => {\n  if (!win || win.__pinokioCloseHandlerInstalled) {\n    return\n  }\n  win.__pinokioCloseHandlerInstalled = true\n  win.once('close', (event) => {\n    if (win.isDestroyed()) {\n      return\n    }\n    event.preventDefault()\n    win.destroy()\n  })\n}\nconst popupShellManager = createPopupShellManager({\n  installForceDestroyOnClose\n})\nconst installClosePopupOnDownload = (targetSession) => {\n  if (!targetSession || targetSession.__pinokioClosePopupOnDownloadInstalled) {\n    return\n  }\n  targetSession.__pinokioClosePopupOnDownloadInstalled = true\n  targetSession.on('will-download', (_event, _item, webContents) => {\n    if (!webContents || typeof webContents.getOwnerBrowserWindow !== 'function') {\n      return\n    }\n    let owner = null\n    try {\n      owner = webContents.getOwnerBrowserWindow()\n    } catch (_) {\n      owner = null\n    }\n    if (!owner || owner.isDestroyed?.() || !owner.__pinokioCloseOnFirstDownload) {\n      return\n    }\n    owner.__pinokioCloseOnFirstDownload = false\n    setTimeout(() => {\n      if (!owner.isDestroyed()) {\n        owner.close()\n      }\n    }, 0)\n  })\n}\nconst resolveConsoleSourceUrl = (sourceId, pageUrl) => {\n  const page = safeParseUrl(pageUrl)\n  const source = safeParseUrl(sourceId, page ? page.href : undefined)\n  if (source && (source.protocol === 'http:' || source.protocol === 'https:' || source.protocol === 'file:')) {\n    return source.href\n  }\n  if (page) {\n    return page.href\n  }\n  return null\n}\nconst shouldLogUrl = (url) => {\n  if (!ENABLE_BROWSER_CONSOLE_LOG) {\n    return false\n  }\n  if (!url) {\n    return false\n  }\n  const rootParsed = safeParseUrl(root_url)\n  const target = safeParseUrl(url, rootParsed ? rootParsed.origin : undefined)\n  if (!target) {\n    return false\n  }\n  if (rootParsed) {\n    if (target.origin !== rootParsed.origin) {\n      return false\n    }\n    const normalizedTargetPath = (target.pathname || '').replace(/\\/+$/, '')\n    const normalizedRootPath = (rootParsed.pathname || '').replace(/\\/+$/, '')\n    if (normalizedTargetPath === normalizedRootPath) {\n      return false\n    }\n  } else {\n    const normalizedTargetPath = (target.pathname || '').replace(/\\/+$/, '')\n    if (!normalizedTargetPath) {\n      return false\n    }\n  }\n  return true\n}\nconst getBrowserLogFile = () => {\n  if (!ENABLE_BROWSER_CONSOLE_LOG) {\n    return null\n  }\n  if (!browserLogFilePath) {\n    if (!pinokiod || !pinokiod.kernel || !pinokiod.kernel.homedir) {\n      return null\n    }\n    try {\n      browserLogFilePath = pinokiod.kernel.path('logs/browser.log')\n    } catch (err) {\n      console.error('[BROWSER LOG] Failed to resolve browser log file path', err)\n      return null\n    }\n  }\n  return browserLogFilePath\n}\nconst ensureBrowserLogFile = () => {\n  if (!ENABLE_BROWSER_CONSOLE_LOG) {\n    return null\n  }\n  const filePath = getBrowserLogFile()\n  if (!filePath) {\n    return null\n  }\n  if (browserLogFileReady) {\n    return filePath\n  }\n  try {\n    fs.mkdirSync(path.dirname(filePath), { recursive: true })\n    if (fs.existsSync(filePath)) {\n      try {\n        const existingContent = fs.readFileSync(filePath, 'utf8')\n        const existingLines = existingContent.split(/\\r?\\n/).filter((line) => line.length > 0)\n        const filteredLines = []\n        for (const line of existingLines) {\n          const parts = line.split('\\t')\n          if (parts.length >= 2) {\n            const urlPart = parts[1]\n            if (!shouldLogUrl(urlPart)) {\n              continue\n            }\n          }\n          filteredLines.push(`${line}\\n`)\n          if (filteredLines.length > 100) {\n            filteredLines.shift()\n          }\n        }\n        browserLogBuffer = filteredLines\n        fs.writeFileSync(filePath, browserLogBuffer.join(''))\n      } catch (err) {\n        console.error('[BROWSER LOG] Failed to prime existing browser log', err)\n        browserLogBuffer = []\n      }\n    }\n    browserLogFileReady = true\n    return filePath\n  } catch (err) {\n    console.error('[BROWSER LOG] Failed to prepare browser log file', err)\n    return null\n  }\n}\nconst titleBarOverlay = (colors) => {\n  if (is_mac) {\n    return false\n  } else {\n    return colors\n  }\n}\nconst getLogFileHint = () => {\n  try {\n    if (pinokiod && pinokiod.kernel && pinokiod.kernel.homedir) {\n      return path.resolve(pinokiod.kernel.homedir, \"logs\", \"stdout.txt\")\n    }\n  } catch (err) {\n  }\n  return path.resolve(os.homedir(), \".pinokio\", \"logs\", \"stdout.txt\")\n}\nconst getSplashIcon = () => {\n  if (splashIcon) {\n    return splashIcon\n  }\n  const candidates = [\n    path.join('assets', 'icon.png'),\n    path.join('assets', 'icon_small@2x.png'),\n    path.join('assets', 'icon_small.png'),\n    'icon2.png'\n  ]\n  for (const relative of candidates) {\n    const absolute = path.join(__dirname, relative)\n    if (fs.existsSync(absolute)) {\n      splashIcon = relative.split(path.sep).join('/')\n      return splashIcon\n    }\n  }\n  splashIcon = path.join('assets', 'icon_small.png').split(path.sep).join('/')\n  return splashIcon\n}\nconst ensureSplashWindow = () => {\n  if (splashWindow && !splashWindow.isDestroyed()) {\n    return splashWindow\n  }\n  splashWindow = new BrowserWindow({\n    width: 420,\n    height: 320,\n    frame: false,\n    resizable: false,\n    transparent: true,\n    show: false,\n    alwaysOnTop: true,\n    skipTaskbar: true,\n    fullscreenable: false,\n    webPreferences: {\n      spellcheck: false,\n      backgroundThrottling: false\n    }\n  })\n  splashWindow.on('closed', () => {\n    splashWindow = null\n  })\n  return splashWindow\n}\nconst updateSplashWindow = ({ state = 'loading', message, detail, logPath, icon } = {}) => {\n  const win = ensureSplashWindow()\n  const query = { state }\n  if (message) {\n    query.message = message\n  }\n  if (detail) {\n    const trimmed = detail.length > 800 ? `${detail.slice(0, 800)}…` : detail\n    query.detail = trimmed\n  }\n  if (logPath) {\n    query.log = logPath\n  }\n  if (icon) {\n    query.icon = icon\n  }\n  win.loadFile(path.join(__dirname, 'splash.html'), { query }).finally(() => {\n    if (!win.isDestroyed()) {\n      win.show()\n    }\n  })\n}\nconst closeSplashWindow = () => {\n  if (splashWindow && !splashWindow.isDestroyed()) {\n    splashWindow.close()\n  }\n}\nconst showStartupError = ({ message, detail, error } = {}) => {\n  const formatted = detail || formatStartupError(error)\n  updateSplashWindow({\n    state: 'error',\n    message: message || 'Pinokio could not start',\n    detail: formatted,\n    logPath: getLogFileHint(),\n    icon: getSplashIcon()\n  })\n}\nconst formatStartupError = (error) => {\n  if (!error) {\n    return ''\n  }\n  if (error.stack) {\n    return `${error.message || 'Unknown error'}\\n\\n${error.stack}`\n  }\n  if (error.message) {\n    return error.message\n  }\n  if (typeof error === 'string') {\n    return error\n  }\n  try {\n    return JSON.stringify(error, null, 2)\n  } catch (err) {\n    return String(error)\n  }\n}\nconst SESSION_COOKIE_TTL_DAYS = 90\nconst SESSION_COOKIE_TTL_SEC = SESSION_COOKIE_TTL_DAYS * 24 * 60 * 60\nconst SESSION_COOKIE_JAR_FILENAME = 'session-cookies.json'\nlet sessionCookieSavePromise = null\nlet isQuitting = false\nconst getSessionCookieJarPath = () => path.join(app.getPath('userData'), SESSION_COOKIE_JAR_FILENAME)\nconst buildCookieUrl = (cookie) => {\n  if (!cookie || !cookie.domain) {\n    return null\n  }\n  const host = cookie.domain.startsWith('.') ? cookie.domain.slice(1) : cookie.domain\n  if (!host) {\n    return null\n  }\n  const scheme = cookie.secure ? 'https://' : 'http://'\n  const cookiePath = cookie.path && cookie.path.startsWith('/') ? cookie.path : '/'\n  return `${scheme}${host}${cookiePath}`\n}\nconst serializeSessionCookie = (cookie) => {\n  const url = buildCookieUrl(cookie)\n  if (!url || typeof cookie.name !== 'string') {\n    return null\n  }\n  const entry = {\n    url,\n    name: cookie.name,\n    value: typeof cookie.value === 'string' ? cookie.value : '',\n    path: cookie.path && cookie.path.startsWith('/') ? cookie.path : '/',\n    secure: !!cookie.secure,\n    httpOnly: !!cookie.httpOnly\n  }\n  if (cookie.hostOnly !== true && cookie.domain) {\n    entry.domain = cookie.domain\n  }\n  if (cookie.sameSite) {\n    entry.sameSite = cookie.sameSite\n  }\n  if (cookie.priority) {\n    entry.priority = cookie.priority\n  }\n  if (cookie.sameParty != null) {\n    entry.sameParty = cookie.sameParty\n  }\n  if (cookie.sourceScheme) {\n    entry.sourceScheme = cookie.sourceScheme\n  }\n  if (Number.isInteger(cookie.sourcePort)) {\n    entry.sourcePort = cookie.sourcePort\n  }\n  return entry\n}\nconst persistSessionCookies = () => {\n  if (sessionCookieSavePromise) {\n    return sessionCookieSavePromise\n  }\n  sessionCookieSavePromise = (async () => {\n    try {\n      const cookies = await session.defaultSession.cookies.get({})\n      const sessionCookies = cookies.filter((cookie) => cookie && cookie.session)\n      const entries = sessionCookies.map(serializeSessionCookie).filter(Boolean)\n      const jarPath = getSessionCookieJarPath()\n      if (!entries.length) {\n        await fs.promises.unlink(jarPath).catch((err) => {\n          if (err && err.code !== 'ENOENT') {\n            console.warn('[Session Cookies] Failed to remove jar', err)\n          }\n        })\n        return\n      }\n      const payload = {\n        version: 1,\n        savedAt: Date.now(),\n        cookies: entries\n      }\n      await fs.promises.mkdir(path.dirname(jarPath), { recursive: true }).catch(() => {})\n      await fs.promises.writeFile(jarPath, JSON.stringify(payload), 'utf8')\n    } catch (err) {\n      console.warn('[Session Cookies] Failed to persist', err)\n    } finally {\n      sessionCookieSavePromise = null\n    }\n  })()\n  return sessionCookieSavePromise\n}\nconst restoreSessionCookies = async () => {\n  const jarPath = getSessionCookieJarPath()\n  let raw\n  try {\n    raw = await fs.promises.readFile(jarPath, 'utf8')\n  } catch (err) {\n    if (err && err.code !== 'ENOENT') {\n      console.warn('[Session Cookies] Failed to read jar', err)\n    }\n    return\n  }\n  let data\n  try {\n    data = JSON.parse(raw)\n  } catch (err) {\n    console.warn('[Session Cookies] Failed to parse jar', err)\n    return\n  }\n  const entries = Array.isArray(data.cookies) ? data.cookies : []\n  if (!entries.length) {\n    return\n  }\n  const expirationDate = Math.floor(Date.now() / 1000) + SESSION_COOKIE_TTL_SEC\n  for (const entry of entries) {\n    if (!entry || !entry.url || !entry.name) {\n      continue\n    }\n    const details = {\n      url: entry.url,\n      name: entry.name,\n      value: typeof entry.value === 'string' ? entry.value : '',\n      path: entry.path || '/',\n      secure: !!entry.secure,\n      httpOnly: !!entry.httpOnly,\n      expirationDate\n    }\n    if (entry.domain) {\n      details.domain = entry.domain\n    }\n    if (entry.sameSite) {\n      details.sameSite = entry.sameSite\n    }\n    if (entry.priority) {\n      details.priority = entry.priority\n    }\n    if (entry.sameParty != null) {\n      details.sameParty = entry.sameParty\n    }\n    if (entry.sourceScheme) {\n      details.sourceScheme = entry.sourceScheme\n    }\n    if (Number.isInteger(entry.sourcePort)) {\n      details.sourcePort = entry.sourcePort\n    }\n    try {\n      await session.defaultSession.cookies.set(details)\n    } catch (err) {\n      console.warn('[Session Cookies] Failed to restore cookie', entry.name, err)\n    }\n  }\n}\nconst clearPersistedSessionCookies = async () => {\n  const jarPath = getSessionCookieJarPath()\n  try {\n    await fs.promises.unlink(jarPath)\n  } catch (err) {\n    if (err && err.code !== 'ENOENT') {\n      console.warn('[Session Cookies] Failed to remove jar', err)\n    }\n  }\n}\nconst clearSessionCaches = async () => {\n  try {\n    await session.defaultSession.clearCache()\n  } catch (err) {\n    console.warn('[Session Cache] Failed to clear http cache', err)\n  }\n  try {\n    await session.defaultSession.clearStorageData({\n      storages: ['serviceworkers', 'cachestorage']\n    })\n  } catch (err) {\n    console.warn('[Session Cache] Failed to clear service worker/cache storage', err)\n  }\n}\nfunction UpsertKeyValue(obj, keyToChange, value) {\n  const keyToChangeLower = keyToChange.toLowerCase();\n  for (const key of Object.keys(obj)) {\n    if (key.toLowerCase() === keyToChangeLower) {\n      // Reassign old key\n      obj[key] = value;\n      // Done\n      return;\n    }\n  }\n  // Insert at end instead\n  obj[keyToChange] = value;\n}\n\nconst clearBrowserConsoleState = (webContents) => {\n  if (browserConsoleState.has(webContents)) {\n    browserConsoleState.delete(webContents)\n  }\n}\n\nconst updateBrowserConsoleTarget = (webContents, url) => {\n  if (!ENABLE_BROWSER_CONSOLE_LOG) {\n    return\n  }\n  if (!root_url) {\n    clearBrowserConsoleState(webContents)\n    return\n  }\n  let parsed\n  try {\n    parsed = new URL(url)\n  } catch (e) {\n    clearBrowserConsoleState(webContents)\n    return\n  }\n  if (parsed.origin !== root_url) {\n    clearBrowserConsoleState(webContents)\n    return\n  }\n  const existing = browserConsoleState.get(webContents)\n  if (existing && existing.url === parsed.href) {\n    return\n  }\n  browserConsoleState.set(webContents, { url: parsed.href })\n}\n\nconst inspectorSessions = new Map()\nlet inspectorHandlersInstalled = false\n\nconst inspectorLogFile = path.join(os.tmpdir(), 'pinokio-inspector.log')\n\nconst inspectorMainLog = (label, payload) => {\n  try {\n    const serialized = payload === undefined ? '' : ' ' + JSON.stringify(payload)\n    const line = `[InspectorMain] ${label}${serialized}\\n`\n    try {\n      fs.appendFileSync(inspectorLogFile, line)\n    } catch (_) {}\n    process.stdout.write(line)\n  } catch (_) {\n    try {\n      fs.appendFileSync(inspectorLogFile, `[InspectorMain] ${label}\\n`)\n    } catch (_) {}\n    process.stdout.write(`[InspectorMain] ${label}\\n`)\n  }\n}\n\nconst normalizeInspectorUrl = (value) => {\n  if (!value) {\n    return null\n  }\n  try {\n    return new URL(value).href\n  } catch (_) {\n    return value\n  }\n}\n\nconst urlsRoughlyMatch = (expected, candidate) => {\n  if (!expected) {\n    return true\n  }\n  if (!candidate) {\n    return false\n  }\n  if (candidate === expected) {\n    return true\n  }\n  return candidate.startsWith(expected) || expected.startsWith(candidate)\n}\n\nconst flattenFrameTree = (frame, acc = [], depth = 0) => {\n  if (!frame) {\n    return acc\n  }\n  let frameName = null\n  try {\n    frameName = typeof frame.name === 'string' && frame.name.length ? frame.name : null\n  } catch (_) {\n    frameName = null\n  }\n  acc.push({ frame, depth, url: normalizeInspectorUrl(frame.url || ''), name: frameName })\n  const children = Array.isArray(frame.frames) ? frame.frames : []\n  for (const child of children) {\n    flattenFrameTree(child, acc, depth + 1)\n  }\n  return acc\n}\n\nconst findDescendantByUrl = (frame, targetUrl) => {\n  if (!frame || !targetUrl) {\n    return null\n  }\n  const normalizedTarget = normalizeInspectorUrl(targetUrl)\n  if (!normalizedTarget) {\n    return null\n  }\n  const stack = [frame]\n  while (stack.length) {\n    const current = stack.pop()\n    try {\n      const currentUrl = normalizeInspectorUrl(current.url || '')\n      if (currentUrl && urlsRoughlyMatch(normalizedTarget, currentUrl)) {\n        return current\n      }\n    } catch (_) {}\n    const children = Array.isArray(current.frames) ? current.frames : []\n    for (const child of children) {\n      if (child) {\n        stack.push(child)\n      }\n    }\n  }\n  return null\n}\n\nconst selectTargetFrame = (webContents, payload = {}) => {\n  if (!webContents || !webContents.mainFrame) {\n    inspectorMainLog('no-webcontents', {})\n    return null\n  }\n  const frames = flattenFrameTree(webContents.mainFrame, [])\n  if (!frames.length) {\n    inspectorMainLog('no-frames', { webContentsId: webContents.id })\n    return null\n  }\n  inspectorMainLog('incoming', {\n    frameUrl: payload.frameUrl || null,\n    frameName: payload.frameName || null,\n    frameNodeId: payload.frameNodeId || null,\n    frameCount: frames.length,\n  })\n\n  const canonicalUrl = normalizeInspectorUrl(payload.frameUrl)\n  const relativeOrdinal = typeof payload.candidateRelativeOrdinal === 'number' ? payload.candidateRelativeOrdinal : null\n  const globalOrdinal = typeof payload.frameIndex === 'number' ? payload.frameIndex : null\n  const canonicalFrameName = typeof payload.frameName === 'string' && payload.frameName.trim() ? payload.frameName.trim() : null\n  const canonicalFrameNodeId = typeof payload.frameNodeId === 'string' && payload.frameNodeId.trim() ? payload.frameNodeId.trim() : null\n\n  if (canonicalFrameName || canonicalFrameNodeId) {\n    inspectorMainLog('identifier-search', {\n      frameName: canonicalFrameName || null,\n      frameNodeId: canonicalFrameNodeId || null,\n      names: frames.map((entry) => entry.name || null).slice(0, 12),\n    })\n\n    let identifierMatch = null\n    if (canonicalFrameNodeId) {\n      identifierMatch = frames.find((entry) => entry && entry.name === canonicalFrameNodeId) || null\n      if (identifierMatch) {\n        const normalizedUrl = normalizeInspectorUrl(identifierMatch.url || '')\n        if (canonicalUrl && (!normalizedUrl || !urlsRoughlyMatch(canonicalUrl, normalizedUrl))) {\n          const descendant = findDescendantByUrl(identifierMatch.frame, canonicalUrl)\n          if (descendant) {\n            inspectorMainLog('identifier-match-node-descendant', {\n              index: frames.indexOf(identifierMatch),\n              name: identifierMatch.name || null,\n              url: identifierMatch.url || null,\n              descendantUrl: normalizeInspectorUrl(descendant.url || ''),\n            })\n            return descendant\n          }\n        }\n        inspectorMainLog('identifier-match-node', {\n          index: frames.indexOf(identifierMatch),\n          name: identifierMatch.name || null,\n          url: identifierMatch.url || null,\n        })\n        return identifierMatch.frame\n      }\n    }\n\n    if (canonicalFrameName) {\n      identifierMatch = frames.find((entry) => entry && entry.name === canonicalFrameName) || null\n      if (identifierMatch) {\n        const normalizedUrl = normalizeInspectorUrl(identifierMatch.url || '')\n        if (canonicalUrl && (!normalizedUrl || !urlsRoughlyMatch(canonicalUrl, normalizedUrl))) {\n          const descendant = findDescendantByUrl(identifierMatch.frame, canonicalUrl)\n          if (descendant) {\n            inspectorMainLog('identifier-match-name-descendant', {\n              index: frames.indexOf(identifierMatch),\n              name: identifierMatch.name || null,\n              url: identifierMatch.url || null,\n              descendantUrl: normalizeInspectorUrl(descendant.url || ''),\n            })\n            return descendant\n          }\n        }\n        inspectorMainLog('identifier-match-name', {\n          index: frames.indexOf(identifierMatch),\n          name: identifierMatch.name || null,\n          url: identifierMatch.url || null,\n        })\n        return identifierMatch.frame\n      }\n    }\n\n    inspectorMainLog('identifier-miss', {})\n  }\n\n  let matches = frames\n  if (canonicalUrl) {\n    matches = frames.filter(({ url }) => urlsRoughlyMatch(canonicalUrl, url))\n  }\n\n  if (matches.length) {\n    if (relativeOrdinal !== null) {\n      const filtered = matches.slice().sort((a, b) => a.depth - b.depth || frames.indexOf(a) - frames.indexOf(b))\n      const targetEntry = filtered[Math.min(Math.max(relativeOrdinal, 0), filtered.length - 1)]\n      if (targetEntry) {\n        inspectorMainLog('relative-ordinal-match', {\n          index: frames.indexOf(targetEntry),\n          name: targetEntry.name || null,\n          url: targetEntry.url || null,\n        })\n        return targetEntry.frame\n      }\n    }\n    const fallbackEntry = matches[0]\n    if (fallbackEntry) {\n      inspectorMainLog('fallback-match', {\n        index: frames.indexOf(fallbackEntry),\n        name: fallbackEntry.name || null,\n        url: fallbackEntry.url || null,\n      })\n      return fallbackEntry.frame\n    }\n  }\n\n  if (globalOrdinal !== null && frames[globalOrdinal]) {\n    inspectorMainLog('global-ordinal-match', {\n      index: globalOrdinal,\n      name: frames[globalOrdinal].name || null,\n      url: frames[globalOrdinal].url || null,\n    })\n    return frames[globalOrdinal].frame\n  }\n\n  inspectorMainLog('default-match', {\n    name: frames[0]?.name || null,\n    url: frames[0]?.url || null,\n  })\n\n  return frames[0]?.frame || null\n}\n\nconst buildInspectorInjection = () => {\n  const source = function () {\n    try {\n      if (window.__PINOKIO_INSPECTOR__ && typeof window.__PINOKIO_INSPECTOR__.stop === 'function') {\n        window.__PINOKIO_INSPECTOR__.stop()\n      }\n\n      const overlay = document.createElement('div')\n      overlay.style.position = 'fixed'\n      overlay.style.pointerEvents = 'none'\n      overlay.style.border = '2px solid rgba(77,163,255,0.9)'\n      overlay.style.background = 'rgba(77,163,255,0.2)'\n      overlay.style.boxShadow = '0 0 0 1px rgba(23,52,92,0.45)'\n      overlay.style.zIndex = '2147483647'\n      overlay.style.display = 'none'\n      document.documentElement.appendChild(overlay)\n\n      let active = true\n\n      const post = (type, payload) => {\n        try {\n          window.parent.postMessage({ pinokioInspector: { type, frameUrl: window.location.href, ...payload } }, '*')\n        } catch (err) {\n          // ignore\n        }\n      }\n\n      const updateBox = (target) => {\n        if (!active || !target) {\n          overlay.style.display = 'none'\n          return\n        }\n        const rect = target.getBoundingClientRect()\n        if (!rect || rect.width <= 0 || rect.height <= 0) {\n          overlay.style.display = 'none'\n          return\n        }\n        overlay.style.display = 'block'\n        overlay.style.left = `${rect.left}px`\n        overlay.style.top = `${rect.top}px`\n        overlay.style.width = `${rect.width}px`\n        overlay.style.height = `${rect.height}px`\n      }\n\n      const buildPathKeys = (node) => {\n        if (!node) {\n          return []\n        }\n        const keys = []\n        let current = node\n        let depth = 0\n        while (current && current.nodeType === Node.ELEMENT_NODE && depth < 8) {\n          const tag = current.tagName ? current.tagName.toLowerCase() : 'element'\n          let descriptor = tag\n          if (current.id) {\n            descriptor += `#${current.id}`\n          } else if (current.classList && current.classList.length) {\n            descriptor += `.${Array.from(current.classList).slice(0, 2).join('.')}`\n          }\n          keys.push(descriptor)\n          current = current.parentElement\n          depth += 1\n        }\n        return keys.reverse()\n      }\n\n      const handleMove = (event) => {\n        if (!active) {\n          return\n        }\n        const target = event.target\n        updateBox(target)\n        post('update', {\n          nodeName: target && target.tagName ? target.tagName.toLowerCase() : '',\n          pathKeys: buildPathKeys(target),\n        })\n      }\n\n      const preventClick = (event) => {\n        if (!active) {\n          return\n        }\n        event.preventDefault()\n        event.stopPropagation()\n      }\n\n      const handleClick = async (event) => {\n        if (!active) {\n          return\n        }\n        event.preventDefault()\n        event.stopPropagation()\n        \n        const target = event.target\n        const html = target && target.outerHTML ? target.outerHTML : ''\n        let screenshot = null\n        \n        // Hide the overlay before taking screenshot to avoid capturing it\n        if (overlay && overlay.style) {\n          overlay.style.display = 'none'\n        }\n        \n        // Small delay to ensure overlay is hidden before screenshot\n        await new Promise(resolve => setTimeout(resolve, 50))\n        \n        try {\n          // Use html2canvas-like approach to capture actual element rendering\n          const rect = target.getBoundingClientRect()\n          \n          // Send element bounds for screenshot capture\n          const screenshotRequest = {\n            type: 'screenshot',\n            bounds: {\n              x: Math.round(rect.left),\n              y: Math.round(rect.top),\n              width: Math.max(1, Math.round(rect.width)),\n              height: Math.max(1, Math.round(rect.height))\n            },\n            devicePixelRatio: window.devicePixelRatio || 1,\n            frameUrl: window.location.href,\n            __pinokioRelayStage: 0,\n            __pinokioRelayComplete: window === window.top\n          }\n          \n          // Post screenshot request via postMessage to main page\n          try {\n            console.log('Attempting screenshot capture...')\n            console.log('electronAPI available in iframe:', !!window.electronAPI)\n            console.log('Screenshot request:', screenshotRequest)\n            \n            // Send screenshot request to parent page via postMessage\n            const response = await new Promise((resolve, reject) => {\n              const messageId = 'screenshot_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)\n              \n              const handleResponse = (event) => {\n                if (event.data && event.data.pinokioScreenshotResponse && event.data.messageId === messageId) {\n                  window.removeEventListener('message', handleResponse)\n                  if (event.data.success) {\n                    resolve(event.data.screenshot)\n                  } else {\n                    reject(new Error(event.data.error || 'Screenshot failed'))\n                  }\n                }\n              }\n              \n              window.addEventListener('message', handleResponse)\n              \n              // Send request to parent page\n              window.parent.postMessage({\n                pinokioScreenshotRequest: screenshotRequest,\n                messageId: messageId\n              }, '*')\n              \n              // Timeout after 3 seconds\n              setTimeout(() => {\n                window.removeEventListener('message', handleResponse)\n                reject(new Error('Screenshot timeout'))\n              }, 3000)\n            })\n            \n            screenshot = response\n            console.log('Screenshot captured successfully via parent page')\n          } catch (screenshotError) {\n            console.error('Screenshot capture failed:', screenshotError)\n            screenshot = null\n          }\n        } catch (error) {\n          console.warn('Screenshot capture failed:', error)\n          screenshot = null\n        }\n        \n        post('complete', {\n          outerHTML: html,\n          pathKeys: buildPathKeys(target),\n          screenshot: screenshot\n        })\n        stop()\n      }\n\n      const handleKey = (event) => {\n        if (!active) {\n          return\n        }\n        if (event.key === 'Escape') {\n          post('cancelled', {})\n          stop()\n        }\n      }\n\n      const stop = () => {\n        if (!active) {\n          return\n        }\n        active = false\n        document.removeEventListener('mousemove', handleMove, true)\n        document.removeEventListener('mouseover', handleMove, true)\n        document.removeEventListener('mousedown', preventClick, true)\n        document.removeEventListener('click', handleClick, true)\n        window.removeEventListener('keydown', handleKey, true)\n        if (overlay.parentNode) {\n          overlay.parentNode.removeChild(overlay)\n        }\n        window.__PINOKIO_INSPECTOR__ = null\n      }\n\n      document.addEventListener('mousemove', handleMove, true)\n      document.addEventListener('mouseover', handleMove, true)\n      document.addEventListener('mousedown', preventClick, true)\n      document.addEventListener('click', handleClick, true)\n      window.addEventListener('keydown', handleKey, true)\n\n      window.__PINOKIO_INSPECTOR__ = {\n        stop,\n      }\n\n      post('started', {})\n    } catch (error) {\n      try {\n        window.parent.postMessage({ pinokioInspector: { type: 'error', frameUrl: window.location.href, message: error && error.message ? error.message : String(error) } }, '*')\n      } catch (_) {}\n    }\n  }\n  return `(${source.toString()})();`\n}\n\nconst buildScreenshotRelayInjection = () => {\n  const source = function () {\n    try {\n      if (window.__PINOKIO_SCREENSHOT_RELAY__) {\n        return\n      }\n      window.__PINOKIO_SCREENSHOT_RELAY__ = true\n\n      const pending = new Map()\n      const EXPIRATION_MS = 5000\n\n      const rememberSource = (messageId, sourceWindow) => {\n        if (!messageId || !sourceWindow) {\n          return\n        }\n        pending.set(messageId, sourceWindow)\n        setTimeout(() => {\n          pending.delete(messageId)\n        }, EXPIRATION_MS)\n      }\n\n      const safeStringify = (value) => {\n        try {\n          return JSON.stringify(value)\n        } catch (_) {\n          return '\"[unserializable]\"'\n        }\n      }\n\n      const log = (label, payload) => {\n        try {\n          console.log('[Pinokio Screenshot Relay] ' + label + ' ' + safeStringify(payload))\n        } catch (_) {\n          // ignore logging failures\n        }\n      }\n\n      log('relay-installed', { href: window.location.href })\n\n      window.addEventListener('message', (event) => {\n        const data = event && event.data\n        log('message-event', {\n          href: window.location.href,\n          hasData: Boolean(data),\n          messageId: data && data.messageId ? data.messageId : null,\n          hasRequest: Boolean(data && data.pinokioScreenshotRequest),\n          hasResponse: Boolean(data && data.pinokioScreenshotResponse)\n        })\n        if (!data) {\n          return\n        }\n\n        if (data.pinokioScreenshotRequest) {\n          if (!event.source || event.source === window) {\n            log('request-ignored-no-source', {\n              href: window.location.href,\n              messageId: data.messageId || null\n            })\n            return\n          }\n\n          rememberSource(data.messageId, event.source)\n          log('request-processing', {\n            href: window.location.href,\n            messageId: data.messageId || null,\n            originalBounds: data.pinokioScreenshotRequest && data.pinokioScreenshotRequest.bounds ? data.pinokioScreenshotRequest.bounds : null,\n            originalDevicePixelRatio: data.pinokioScreenshotRequest ? data.pinokioScreenshotRequest.devicePixelRatio : null\n          })\n\n          let offsetX = 0\n          let offsetY = 0\n          let matchedFrame = false\n          try {\n            for (let index = 0; index < window.frames.length; index += 1) {\n              const childWindow = window.frames[index]\n              if (childWindow === event.source) {\n                log('matching-window-frames', {\n                  href: window.location.href,\n                  messageId: data.messageId || null,\n                  frameIndex: index\n                })\n                try {\n                  const frameElement = childWindow.frameElement\n                  if (frameElement) {\n                    const rect = frameElement.getBoundingClientRect()\n                    offsetX = rect ? rect.left || 0 : 0\n                    offsetY = rect ? rect.top || 0 : 0\n                    matchedFrame = true\n                    log('matched-window-frames', {\n                      href: window.location.href,\n                      messageId: data.messageId || null,\n                      frameIndex: index,\n                      rect: rect ? { left: rect.left, top: rect.top, width: rect.width, height: rect.height } : null\n                    })\n                    break\n                  }\n                } catch (error) {\n                  log('frame-element-access-error', {\n                    href: window.location.href,\n                    messageId: data.messageId || null,\n                    frameIndex: index,\n                    error: error && error.message ? error.message : String(error)\n                  })\n                }\n              }\n            }\n\n            if (!matchedFrame) {\n              const FRAME_SELECTOR = 'iframe, frame'\n              const frames = document.querySelectorAll ? document.querySelectorAll(FRAME_SELECTOR) : []\n              log('matching-query-selector', {\n                href: window.location.href,\n                messageId: data.messageId || null,\n                selector: FRAME_SELECTOR,\n                count: frames ? frames.length : 0\n              })\n              for (const frameEl of frames) {\n                if (!frameEl) {\n                  continue\n                }\n                try {\n                  if (frameEl.contentWindow === event.source) {\n                    const rect = frameEl.getBoundingClientRect()\n                    offsetX = rect ? rect.left || 0 : 0\n                    offsetY = rect ? rect.top || 0 : 0\n                    matchedFrame = true\n                    log('matched-query-selector', {\n                      href: window.location.href,\n                      messageId: data.messageId || null,\n                      selector: FRAME_SELECTOR,\n                      rect: rect ? { left: rect.left, top: rect.top, width: rect.width, height: rect.height } : null\n                    })\n                    break\n                  }\n                } catch (error) {\n                  log('query-selector-access-error', {\n                    href: window.location.href,\n                    messageId: data.messageId || null,\n                    selector: FRAME_SELECTOR,\n                    error: error && error.message ? error.message : String(error)\n                  })\n                }\n              }\n            }\n          } catch (error) {\n            log('frame-enumeration-error', {\n              href: window.location.href,\n              messageId: data.messageId || null,\n              error: error && error.message ? error.message : String(error)\n            })\n          }\n\n          if (!matchedFrame) {\n            log('frame-match-failed', {\n              href: window.location.href,\n              messageId: data.messageId || null,\n              offsetX,\n              offsetY\n            })\n          }\n\n          const request = data.pinokioScreenshotRequest || {}\n          const originalBounds = request.bounds || {}\n          const parentDpr = window.devicePixelRatio || 1\n          const currentDpr = request.devicePixelRatio && request.devicePixelRatio > 0 ? request.devicePixelRatio : 1\n          const nextStage = (typeof request.__pinokioRelayStage === 'number' ? request.__pinokioRelayStage : 0) + 1\n          request.__pinokioRelayStage = nextStage\n          request.__pinokioRelayComplete = window.parent === window\n\n          if (matchedFrame) {\n            const adjustedBounds = {\n              x: (originalBounds.x || 0) + offsetX,\n              y: (originalBounds.y || 0) + offsetY,\n              width: originalBounds.width || 0,\n              height: originalBounds.height || 0,\n            }\n\n            request.bounds = adjustedBounds\n            request.devicePixelRatio = Math.max(currentDpr, parentDpr)\n            request.__pinokioAdjusted = true\n\n            log('request-adjusted', {\n              href: window.location.href,\n              messageId: data.messageId || null,\n              offsetX,\n              offsetY,\n              parentDpr,\n              resultingBounds: adjustedBounds,\n              originalBounds,\n              resultingDevicePixelRatio: request.devicePixelRatio,\n              relayStage: request.__pinokioRelayStage,\n              relayComplete: request.__pinokioRelayComplete\n            })\n          } else {\n            log('request-forward-unadjusted', {\n              href: window.location.href,\n              messageId: data.messageId || null,\n              relayStage: request.__pinokioRelayStage,\n              relayComplete: request.__pinokioRelayComplete\n            })\n          }\n\n          data.pinokioScreenshotRequest = request\n\n          log('request-forward', {\n            href: window.location.href,\n            messageId: data.messageId || null,\n            matchedFrame,\n            hasParent: Boolean(window.parent && window.parent !== window)\n          })\n\n          if (window.parent && window.parent !== window) {\n            window.parent.postMessage(data, '*')\n            if (event && typeof event.stopImmediatePropagation === 'function') {\n              event.stopImmediatePropagation()\n            }\n            return\n          }\n\n          const targetSource = event.source\n          const messageId = data.messageId\n          const captureRequest = data.pinokioScreenshotRequest\n          log('top-level-capture', {\n            href: window.location.href,\n            messageId,\n            relayStage: captureRequest.__pinokioRelayStage,\n            relayComplete: captureRequest.__pinokioRelayComplete,\n            adjustedFlag: captureRequest.__pinokioAdjusted,\n            bounds: captureRequest.bounds || null\n          })\n\n          const captureApi = window.electronAPI && typeof window.electronAPI.captureScreenshot === 'function'\n            ? window.electronAPI.captureScreenshot\n            : null\n\n          if (!captureApi) {\n            log('top-level-capture-missing-api', { href: window.location.href })\n            return\n          }\n\n          Promise.resolve()\n            .then(() => captureApi(captureRequest))\n            .then((screenshot) => {\n              log('top-level-capture-success', { href: window.location.href, messageId })\n              try {\n                targetSource.postMessage({\n                  pinokioScreenshotResponse: true,\n                  messageId,\n                  success: true,\n                  screenshot\n                }, '*')\n              } catch (error) {\n                log('top-level-response-error', {\n                  href: window.location.href,\n                  messageId,\n                  error: error && error.message ? error.message : String(error)\n                })\n              }\n            })\n            .catch((error) => {\n              log('top-level-capture-error', {\n                href: window.location.href,\n                messageId,\n                error: error && error.message ? error.message : String(error)\n              })\n              try {\n                targetSource.postMessage({\n                  pinokioScreenshotResponse: true,\n                  messageId,\n                  success: false,\n                  error: error && error.message ? error.message : String(error)\n                }, '*')\n              } catch (responseError) {\n                log('top-level-response-error', {\n                  href: window.location.href,\n                  messageId,\n                  error: responseError && responseError.message ? responseError.message : String(responseError)\n                })\n              }\n            })\n          return\n        }\n\n        if (data.pinokioScreenshotResponse && data.messageId) {\n          log('response-processing', {\n            href: window.location.href,\n            messageId: data.messageId\n          })\n          const target = pending.get(data.messageId)\n          if (target && target !== event.source) {\n            pending.delete(data.messageId)\n            try {\n              log('response-forwarding-down', {\n                href: window.location.href,\n                messageId: data.messageId\n              })\n              target.postMessage(data, '*')\n              return\n            } catch (error) {\n              log('response-forwarding-error', {\n                href: window.location.href,\n                messageId: data.messageId,\n                error: error && error.message ? error.message : String(error)\n              })\n            }\n          }\n\n          log('response-forwarding-up', {\n            href: window.location.href,\n            messageId: data.messageId,\n            hasParent: Boolean(window.parent && window.parent !== window)\n          })\n          if (window.parent && window.parent !== window) {\n            window.parent.postMessage(data, '*')\n          }\n        }\n      }, true)\n    } catch (error) {\n      try {\n        console.warn('[Pinokio Screenshot Relay] relay-install-error ' + (error && error.message ? error.message : String(error)))\n      } catch (_) {\n        // ignore logging failures\n      }\n    }\n  }\n  return `(${source.toString()})();`\n}\n\nconst installScreenshotRelays = async (frame) => {\n  if (!frame) {\n    return\n  }\n\n  const topFrame = frame.top || frame\n  const frames = flattenFrameTree(topFrame, [])\n  for (const entry of frames) {\n    const candidate = entry && entry.frame\n    if (!candidate || candidate.isDestroyed && candidate.isDestroyed()) {\n      continue\n    }\n    try {\n      await candidate.executeJavaScript(buildScreenshotRelayInjection(), true)\n    } catch (error) {\n      console.warn('Screenshot relay injection failed:', error && error.message ? error.message : error)\n    }\n  }\n}\n\nconst startInspectorSession = async (webContents, payload = {}) => {\n  const existing = inspectorSessions.get(webContents.id)\n  if (existing) {\n    await stopInspectorSession(webContents)\n  }\n\n  const targetFrame = selectTargetFrame(webContents, payload)\n  if (!targetFrame) {\n    throw new Error('Unable to locate iframe to inspect.')\n  }\n\n  await installScreenshotRelays(targetFrame)\n  await targetFrame.executeJavaScript(buildInspectorInjection(), true)\n\n\n  const navigationHandler = () => {\n    const resultPromise = stopInspectorSession(webContents)\n    Promise.resolve(resultPromise).then((outcome) => {\n      if (!webContents.isDestroyed()) {\n        webContents.send('pinokio:inspector-cancelled', { frameUrl: (outcome && outcome.frameUrl) || targetFrame.url || payload.frameUrl || '' })\n      }\n    })\n  }\n\n  if (!webContents.isDestroyed()) {\n    webContents.on('did-navigate', navigationHandler)\n    webContents.on('did-navigate-in-page', navigationHandler)\n  }\n\n  inspectorSessions.set(webContents.id, {\n    frame: targetFrame,\n    navigationHandler,\n  })\n\n  return {\n    frameUrl: targetFrame.url || payload.frameUrl || '',\n  }\n}\n\nconst stopInspectorSession = async (webContents) => {\n  const session = inspectorSessions.get(webContents.id)\n  if (!session) {\n    return { frameUrl: '' }\n  }\n  inspectorSessions.delete(webContents.id)\n  if (session.navigationHandler && !webContents.isDestroyed()) {\n    webContents.removeListener('did-navigate', session.navigationHandler)\n    webContents.removeListener('did-navigate-in-page', session.navigationHandler)\n  }\n  const frameUrl = session.frame && session.frame.url ? session.frame.url : ''\n  try {\n    await session.frame.executeJavaScript('window.__PINOKIO_INSPECTOR__ && window.__PINOKIO_INSPECTOR__.stop()', true)\n  } catch (_) {}\n  return { frameUrl }\n}\n\nconst safeCaptureStringify = (value) => {\n  try {\n    return JSON.stringify(value)\n  } catch (_) {\n    return '\"[unserializable]\"'\n  }\n}\n\nconst captureLog = (label, payload) => {\n  try {\n    console.log('[Pinokio Capture] ' + label + ' ' + safeCaptureStringify(payload))\n  } catch (_) {\n    console.log('[Pinokio Capture] ' + label)\n  }\n}\n\nconst installInspectorHandlers = () => {\n  console.log('Installing inspector handlers...')\n  if (inspectorHandlersInstalled) {\n    console.log('Inspector handlers already installed, skipping')\n    return\n  }\n  inspectorHandlersInstalled = true\n  console.log('Installing pinokio:capture-screenshot handler')\n\n  ipcMain.handle('pinokio:start-inspector', async (event, payload = {}) => {\n    try {\n      const result = await startInspectorSession(event.sender, payload)\n      event.sender.send('pinokio:inspector-started', { frameUrl: result.frameUrl })\n      return { ok: true }\n    } catch (error) {\n      const message = error && error.message ? error.message : 'Unable to start inspect mode.'\n      event.sender.send('pinokio:inspector-error', { message })\n      throw new Error(message)\n    }\n  })\n\n  ipcMain.handle('pinokio:stop-inspector', async (event) => {\n    try {\n      const result = await stopInspectorSession(event.sender)\n      event.sender.send('pinokio:inspector-cancelled', { frameUrl: result.frameUrl || '' })\n      return { ok: true }\n    } catch (error) {\n      const message = error && error.message ? error.message : 'Unable to stop inspect mode.'\n      event.sender.send('pinokio:inspector-error', { message })\n      throw new Error(message)\n    }\n  })\n\n  ipcMain.handle('pinokio:capture-screenshot-debug', async (event, payload) => {\n    const { screenshotRequest } = payload\n\n    const emitDebug = (label, data) => {\n      captureLog(label, data)\n      try {\n        event.sender.send('pinokio:capture-debug-log', {\n          label,\n          payload: data\n        })\n      } catch (_) {\n        // ignore renderer emit errors\n      }\n    }\n\n    emitDebug('handler-invoked', {\n      senderId: event && event.sender ? event.sender.id : null,\n      hasRequest: Boolean(screenshotRequest),\n      bounds: screenshotRequest && screenshotRequest.bounds ? {\n        x: screenshotRequest.bounds.x,\n        y: screenshotRequest.bounds.y,\n        width: screenshotRequest.bounds.width,\n        height: screenshotRequest.bounds.height,\n      } : null,\n      devicePixelRatio: screenshotRequest ? screenshotRequest.devicePixelRatio : null,\n      adjustedFlag: Boolean(screenshotRequest && screenshotRequest.__pinokioAdjusted),\n      relayStage: screenshotRequest && typeof screenshotRequest.__pinokioRelayStage !== 'undefined' ? screenshotRequest.__pinokioRelayStage : null,\n      relayComplete: screenshotRequest && typeof screenshotRequest.__pinokioRelayComplete !== 'undefined' ? screenshotRequest.__pinokioRelayComplete : null,\n      frameOffset: screenshotRequest && screenshotRequest.frameOffset ? {\n        x: screenshotRequest.frameOffset.x,\n        y: screenshotRequest.frameOffset.y,\n      } : null\n    })\n    if (!screenshotRequest || !screenshotRequest.bounds) {\n      throw new Error('Invalid screenshot request')\n    }\n    \n    // Get the inspector session to access the target frame\n    const session = inspectorSessions.get(event.sender.id)\n    if (!session || !session.frame) {\n      throw new Error('No inspector session or frame found')\n    }\n    \n    try {\n      const bounds = screenshotRequest.bounds\n      const dpr = screenshotRequest.devicePixelRatio || 1\n      const alreadyAdjusted = Boolean(screenshotRequest.__pinokioAdjusted)\n\n      emitDebug('incoming-bounds', {\n        senderId: event && event.sender ? event.sender.id : null,\n        bounds,\n        devicePixelRatio: dpr,\n        alreadyAdjusted,\n        relayStage: screenshotRequest && typeof screenshotRequest.__pinokioRelayStage !== 'undefined' ? screenshotRequest.__pinokioRelayStage : null,\n        relayComplete: screenshotRequest && typeof screenshotRequest.__pinokioRelayComplete !== 'undefined' ? screenshotRequest.__pinokioRelayComplete : null\n      })\n\n      let framePosition = { x: 0, y: 0 }\n\n      if (!alreadyAdjusted) {\n        try {\n          framePosition = await session.frame.executeJavaScript(`\n            (function() {\n              let x = 0, y = 0;\n              let currentWindow = window;\n\n              while (currentWindow !== window.top) {\n                try {\n                  const frameElement = currentWindow.frameElement;\n                  if (frameElement) {\n                    const rect = frameElement.getBoundingClientRect();\n                    x += rect.left;\n                    y += rect.top;\n                  }\n                } catch (error) {\n                  return { x, y, crossOriginBlocked: true };\n                }\n                currentWindow = currentWindow.parent;\n              }\n\n              return { x, y };\n            })();\n          `)\n          if (framePosition && framePosition.crossOriginBlocked) {\n            framePosition = { x: framePosition.x || 0, y: framePosition.y || 0 }\n          }\n        } catch (error) {\n          console.warn('Unable to determine frame offset via DOM script:', error)\n          framePosition = { x: 0, y: 0 }\n          emitDebug('frame-position-fallback', {\n            senderId: event && event.sender ? event.sender.id : null,\n            error: error && error.message ? error.message : String(error)\n          })\n        }\n      }\n\n      emitDebug('frame-position-computed', {\n        senderId: event && event.sender ? event.sender.id : null,\n        alreadyAdjusted,\n        framePosition,\n        bounds,\n        devicePixelRatio: dpr,\n        relayStage: screenshotRequest && typeof screenshotRequest.__pinokioRelayStage !== 'undefined' ? screenshotRequest.__pinokioRelayStage : null,\n        relayComplete: screenshotRequest && typeof screenshotRequest.__pinokioRelayComplete !== 'undefined' ? screenshotRequest.__pinokioRelayComplete : null\n      })\n      \n      // Capture full page and crop to element bounds\n      const fullImage = await event.sender.capturePage()\n      const fullSize = fullImage.getSize()\n      emitDebug('capture-page-size', {\n        senderId: event && event.sender ? event.sender.id : null,\n        fullSize\n      })\n      \n      // Calculate crop bounds with frame position and device pixel ratio\n      const cropBounds = {\n        x: Math.round((bounds.x + framePosition.x) * dpr),\n        y: Math.round((bounds.y + framePosition.y) * dpr),  \n        width: Math.round(bounds.width * dpr),\n        height: Math.round(bounds.height * dpr)\n      }\n      \n      // Validate crop bounds\n      cropBounds.x = Math.max(0, Math.min(cropBounds.x, fullSize.width - 1))\n      cropBounds.y = Math.max(0, Math.min(cropBounds.y, fullSize.height - 1))\n      cropBounds.width = Math.min(cropBounds.width, fullSize.width - cropBounds.x)\n      cropBounds.height = Math.min(cropBounds.height, fullSize.height - cropBounds.y)\n      emitDebug('crop-bounds', {\n        senderId: event && event.sender ? event.sender.id : null,\n        framePosition,\n        dpr,\n        validatedCropBounds: cropBounds,\n        fullSize\n      })\n      \n      const croppedImage = fullImage.crop(cropBounds)\n      const buffer = croppedImage.toPNG()\n      emitDebug('capture-success', {\n        senderId: event && event.sender ? event.sender.id : null,\n        cropWidth: cropBounds.width,\n        cropHeight: cropBounds.height\n      })\n      \n      return 'data:image/png;base64,' + buffer.toString('base64')\n    } catch (error) {\n      console.error('Screenshot capture failed:', error)\n      emitDebug('capture-error', {\n        senderId: event && event.sender ? event.sender.id : null,\n        error: error && error.message ? error.message : String(error)\n      })\n      throw error\n    }\n  })\n}\n\nconst getFrameInjectorKey = (frame) => {\n  if (!frame) {\n    return ''\n  }\n  if (typeof frame.frameTreeNodeId === 'number') {\n    return `frame:${frame.frameTreeNodeId}`\n  }\n  const processId = typeof frame.processId === 'number' ? frame.processId : 'unknown'\n  const token = typeof frame.frameToken === 'string' && frame.frameToken\n    ? frame.frameToken\n    : String(typeof frame.routingId === 'number' ? frame.routingId : 'unknown')\n  return `${processId}:${token}`\n}\n\nconst getPinokioInjectWebContentsKey = (sender, frame = null) => {\n  if (sender && typeof sender.id === 'number') {\n    return `wc:${sender.id}`\n  }\n  if (frame && frame.hostWebContents && typeof frame.hostWebContents.id === 'number') {\n    return `wc:${frame.hostWebContents.id}`\n  }\n  return ''\n}\n\nconst serializeForJavaScript = (value) => JSON.stringify(value)\n  .replace(/\\u2028/g, '\\\\u2028')\n  .replace(/\\u2029/g, '\\\\u2029')\n\nconst normalizePinokioInjectDescriptor = (descriptor) => {\n  if (!descriptor || typeof descriptor !== 'object' || Array.isArray(descriptor)) {\n    return null\n  }\n  const src = typeof descriptor.src === 'string' ? descriptor.src.trim() : ''\n  if (!src) {\n    return null\n  }\n  const match = Array.isArray(descriptor.match) && descriptor.match.length\n    ? descriptor.match.filter((item) => typeof item === 'string' && item.trim())\n    : ['*']\n  const world = typeof descriptor.world === 'string' && descriptor.world.trim().toLowerCase() === 'isolated'\n    ? 'isolated'\n    : 'main'\n  const whenValue = typeof descriptor.when === 'string' ? descriptor.when.trim().toLowerCase() : ''\n  const when = (whenValue === 'start' || whenValue === 'end') ? whenValue : 'idle'\n  const frameValue = typeof descriptor.frame === 'string' ? descriptor.frame.trim().toLowerCase() : ''\n  const frame = frameValue === 'all' ? 'all' : 'self'\n  return {\n    src,\n    match,\n    world,\n    when,\n    frame\n  }\n}\n\nconst normalizePinokioInjectTargetRegistrations = (targets) => {\n  const values = Array.isArray(targets) ? targets : []\n  const normalized = []\n  for (const target of values) {\n    if (!target || typeof target !== 'object' || Array.isArray(target)) {\n      continue\n    }\n    const name = typeof target.name === 'string' ? target.name.trim() : ''\n    const src = normalizeInspectorUrl(typeof target.src === 'string' ? target.src.trim() : '')\n    if (!name && !src) {\n      continue\n    }\n    normalized.push({\n      name,\n      src,\n      inject: Array.isArray(target.inject)\n        ? target.inject.map((entry) => normalizePinokioInjectDescriptor(entry)).filter(Boolean)\n        : []\n    })\n  }\n  return normalized\n}\n\nconst findFramePath = (frame, target, trail = []) => {\n  if (!frame || !target) {\n    return null\n  }\n  const nextTrail = trail.concat(frame)\n  if (frame === target) {\n    return nextTrail\n  }\n  const children = Array.isArray(frame.frames) ? frame.frames : []\n  for (const child of children) {\n    const result = findFramePath(child, target, nextTrail)\n    if (result) {\n      return result\n    }\n  }\n  return null\n}\n\nconst resolvePinokioRelativeMatchTarget = (href) => {\n  try {\n    const parsed = new URL(href)\n    return `${parsed.pathname}${parsed.search}${parsed.hash}` || '/'\n  } catch (_) {\n    return href || ''\n  }\n}\n\nconst escapePinokioPattern = (value) => String(value || '').replace(/[|\\\\{}()[\\]^$+?.]/g, '\\\\$&')\nconst pinokioPatternToExpression = (value) => {\n  const input = String(value || '')\n  let expression = ''\n  for (let index = 0; index < input.length; index += 1) {\n    const char = input[index]\n    if (char === '*') {\n      while (input[index + 1] === '*') {\n        index += 1\n      }\n      expression += '.*'\n      continue\n    }\n    expression += escapePinokioPattern(char)\n  }\n  return `^${expression}$`\n}\n\nconst matchesPinokioInjectPattern = (pattern, currentUrl) => {\n  if (typeof pattern !== 'string') {\n    return false\n  }\n  const normalizedPattern = pattern.trim()\n  if (!normalizedPattern) {\n    return false\n  }\n  const sourceValue = /^[a-zA-Z][a-zA-Z\\d+\\-.]*:/.test(normalizedPattern)\n    ? currentUrl\n    : resolvePinokioRelativeMatchTarget(currentUrl)\n  const expression = pinokioPatternToExpression(normalizedPattern)\n  try {\n    return new RegExp(expression).test(sourceValue)\n  } catch (_) {\n    return false\n  }\n}\n\nconst matchPinokioInjectTargetToFrame = (targets, frame, hints = {}) => {\n  if (!Array.isArray(targets) || !targets.length) {\n    return null\n  }\n  const frameName = (frame && typeof frame.name === 'string' ? frame.name.trim() : '')\n    || (typeof hints.frameName === 'string' ? hints.frameName.trim() : '')\n  const frameUrl = normalizeInspectorUrl((frame && frame.url) || '')\n    || normalizeInspectorUrl(typeof hints.frameUrl === 'string' ? hints.frameUrl.trim() : '')\n\n  let matched = null\n  if (frameName) {\n    matched = targets.find((entry) => entry.name && entry.name === frameName && (!entry.src || urlsRoughlyMatch(entry.src, frameUrl)))\n      || targets.find((entry) => entry.name && entry.name === frameName)\n  }\n  if (!matched && frameUrl) {\n    matched = targets.find((entry) => entry.src && urlsRoughlyMatch(entry.src, frameUrl)) || null\n  }\n  return matched\n}\n\nconst resolvePinokioInjectTargetMatch = ({ registry, frame, currentUrl, targetHints, descendantDepth = 0 }) => {\n  if (!registry || !Array.isArray(registry.targets) || registry.targets.length === 0) {\n    return null\n  }\n  const target = matchPinokioInjectTargetToFrame(registry.targets, frame, targetHints)\n  if (!target) {\n    return null\n  }\n  const inject = target.inject.filter((descriptor) => {\n    if (descriptor && descriptor.frame !== 'all' && descendantDepth !== 0) {\n      return false\n    }\n    const matches = Array.isArray(descriptor.match) && descriptor.match.length\n      ? descriptor.match\n      : ['*']\n    return matches.some((pattern) => matchesPinokioInjectPattern(pattern, currentUrl))\n  })\n  return {\n    target,\n    inject\n  }\n}\n\nconst resolvePinokioInjectorsForFrame = (frame, payload = {}, sender = null) => {\n  if (!frame) {\n    return {\n      inject: [],\n      context: null\n    }\n  }\n  const requestedContext = payload && payload.context && typeof payload.context === 'object'\n    ? payload.context\n    : {}\n  const currentUrl = typeof requestedContext.currentUrl === 'string' && requestedContext.currentUrl.trim()\n    ? requestedContext.currentUrl.trim()\n    : (normalizeInspectorUrl(frame.url || '') || '')\n  let ownerFrame = frame.parent || null\n  let directChildFrame = frame\n  let descendantDepth = 0\n  const targetHints = {\n    frameName: typeof requestedContext.frameName === 'string' ? requestedContext.frameName.trim() : '',\n    frameUrl: currentUrl\n  }\n\n  while (ownerFrame) {\n    const ownerKey = getFrameInjectorKey(ownerFrame)\n    const registry = frameInjectTargetRegistry.get(ownerKey)\n    if (!registry || !Array.isArray(registry.targets) || registry.targets.length === 0) {\n      directChildFrame = ownerFrame\n      ownerFrame = ownerFrame.parent || null\n      descendantDepth += 1\n      continue\n    }\n    const match = resolvePinokioInjectTargetMatch({\n      registry,\n      frame: directChildFrame,\n      currentUrl,\n      targetHints,\n      descendantDepth\n    })\n    if (!match) {\n      directChildFrame = ownerFrame\n      ownerFrame = ownerFrame.parent || null\n      descendantDepth += 1\n      continue\n    }\n    return {\n      inject: match.inject,\n      context: {\n        frameUrl: normalizeInspectorUrl(ownerFrame.url || '') || '',\n        rootFrameUrl: normalizeInspectorUrl(directChildFrame.url || '') || '',\n        currentUrl,\n        pageUrl: normalizeInspectorUrl(frame.url || '') || currentUrl\n      }\n    }\n  }\n\n  const webContentsKey = getPinokioInjectWebContentsKey(sender, frame)\n  if (webContentsKey) {\n    const registries = Array.from(frameInjectTargetRegistry.entries())\n      .map(([ownerKey, registry]) => ({ ownerKey, registry }))\n      .filter(({ registry }) => registry && registry.webContentsKey === webContentsKey && Array.isArray(registry.targets) && registry.targets.length > 0)\n      .sort((left, right) => (right.registry.updatedAt || 0) - (left.registry.updatedAt || 0))\n    for (const entry of registries) {\n      const match = resolvePinokioInjectTargetMatch({\n        registry: entry.registry,\n        frame,\n        currentUrl,\n        targetHints,\n        descendantDepth: 0\n      })\n      if (!match) {\n        continue\n      }\n      return {\n        inject: match.inject,\n        context: {\n          frameUrl: entry.registry.pageUrl || '',\n          rootFrameUrl: normalizeInspectorUrl(frame.url || '') || currentUrl,\n          currentUrl,\n          pageUrl: entry.registry.pageUrl || currentUrl\n        }\n      }\n    }\n  }\n\n  return {\n    inject: [],\n    context: null\n  }\n}\n\nconst PINOKIO_ABSOLUTE_URL_PATTERN = /^[a-zA-Z][a-zA-Z\\d+\\-.]*:/\n\nconst resolvePinokioInjectSourceUrl = (value) => {\n  if (typeof value !== 'string') {\n    return ''\n  }\n  const trimmed = value.trim()\n  if (!trimmed) {\n    return ''\n  }\n  if (!PINOKIO_ABSOLUTE_URL_PATTERN.test(trimmed) && !trimmed.startsWith('/')) {\n    return ''\n  }\n  const baseUrl = root_url || 'http://localhost'\n  const parsed = safeParseUrl(trimmed, baseUrl)\n  if (!parsed) {\n    return ''\n  }\n  if (!['http:', 'https:', 'file:'].includes(parsed.protocol)) {\n    return ''\n  }\n  return parsed.href\n}\n\nconst buildPinokioInjectRuntimeBootstrap = () => {\n  const source = function() {\n    const resolveTargetWindow = () => {\n      try {\n        if (window.parent && window.parent !== window) {\n          return window.parent\n        }\n      } catch (_) {\n      }\n      try {\n        if (window.top && window.top !== window) {\n          return window.top\n        }\n      } catch (_) {\n      }\n      return window\n    }\n\n    const ensureApi = () => {\n      if (!window.$pinokio || typeof window.$pinokio !== 'object') {\n        window.$pinokio = {}\n      }\n      if (typeof window.$pinokio.trigger !== 'function') {\n        window.$pinokio.trigger = function(eventName, payload = {}, context = {}) {\n          if (typeof eventName !== 'string' || !eventName.trim()) {\n            return { ok: false, handled: false, reason: 'invalid_event_name' }\n          }\n          const nextContext = (context && typeof context === 'object') ? { ...context } : {}\n          if (!nextContext.frameUrl) {\n            nextContext.frameUrl = window.location.href\n          }\n          resolveTargetWindow().postMessage({\n            e: 'pinokio:event',\n            event: eventName.trim(),\n            payload: (payload && typeof payload === 'object') ? payload : {},\n            context: nextContext\n          }, '*')\n          return { ok: true, handled: true, event: eventName.trim() }\n        }\n      }\n      window.$pinokio.inject = function(definition) {\n        return window.__PINOKIO_INJECT_RUNTIME__.register(definition)\n      }\n      return window.$pinokio\n    }\n\n    const buildMountContext = (descriptor, sourceContext) => {\n      const currentUrl = (window && window.location && window.location.href) ? window.location.href : ''\n      const baseContext = (sourceContext && typeof sourceContext === 'object') ? { ...sourceContext } : {}\n      if (!baseContext.frameUrl) {\n        baseContext.frameUrl = currentUrl\n      }\n      if (!baseContext.currentUrl) {\n        baseContext.currentUrl = currentUrl\n      }\n      if (!baseContext.rootFrameUrl) {\n        baseContext.rootFrameUrl = currentUrl\n      }\n      return {\n        ...baseContext,\n        descriptor,\n        trigger(eventName, payload = {}, context = {}) {\n          const nextContext = (context && typeof context === 'object')\n            ? { ...baseContext, ...context }\n            : { ...baseContext }\n          return window.$pinokio.trigger(eventName, payload, nextContext)\n        }\n      }\n    }\n\n    if (!window.__PINOKIO_INJECT_RUNTIME__) {\n      const state = {\n        current: null,\n        cleanups: new Map()\n      }\n      window.__PINOKIO_INJECT_RUNTIME__ = {\n        register(definition) {\n          const current = state.current\n          if (!current) {\n            throw new Error('window.$pinokio.inject() must be called while an injector is loading.')\n          }\n          if (!definition || typeof definition !== 'object' || typeof definition.mount !== 'function') {\n            throw new Error('Pinokio injectors must provide a mount(ctx) function.')\n          }\n          if (current.registered) {\n            throw new Error('Injector registered more than once during a single mount.')\n          }\n          const cleanup = definition.mount(buildMountContext(current.descriptor, current.context))\n          current.registered = true\n          if (typeof cleanup === 'function') {\n            state.cleanups.set(current.descriptor.runtimeId, cleanup)\n          } else {\n            state.cleanups.delete(current.descriptor.runtimeId)\n          }\n          return { ok: true, id: current.descriptor.runtimeId || '' }\n        },\n        run(descriptor, context, runSource) {\n          ensureApi()\n          state.current = {\n            descriptor: descriptor || {},\n            context: (context && typeof context === 'object') ? context : {},\n            registered: false\n          }\n          try {\n            if (typeof runSource === 'function') {\n              runSource()\n            }\n            if (!state.current.registered) {\n              throw new Error('Injector did not call window.$pinokio.inject(...).')\n            }\n          } finally {\n            state.current = null\n          }\n        },\n        unmountAll() {\n          for (const cleanup of state.cleanups.values()) {\n            if (typeof cleanup !== 'function') {\n              continue\n            }\n            try {\n              cleanup()\n            } catch (error) {\n              try {\n                console.warn('[pinokio][inject] cleanup failed', error && error.message ? error.message : String(error))\n              } catch (_) {\n              }\n            }\n          }\n          state.cleanups.clear()\n        }\n      }\n    }\n\n    ensureApi()\n  }\n  return `(${source.toString()})();`\n}\n\nconst buildPinokioInjectUnmountScript = () => `(() => {\n  const runtime = window.__PINOKIO_INJECT_RUNTIME__\n  if (runtime && typeof runtime.unmountAll === 'function') {\n    runtime.unmountAll()\n  }\n})();`\n\nconst buildPinokioInjectExecution = ({ descriptor, context, source }) => {\n  const bootstrap = buildPinokioInjectRuntimeBootstrap()\n  return `(() => {\n${bootstrap}\nwindow.__PINOKIO_INJECT_RUNTIME__.run(${serializeForJavaScript(descriptor)}, ${serializeForJavaScript(context || {})}, () => {\n${source}\n})\n})();\n//# sourceURL=${descriptor.src}`\n}\n\nconst resetPinokioInjectorsInFrame = async (frame) => {\n  if (!frame || (typeof frame.isDestroyed === 'function' && frame.isDestroyed())) {\n    return\n  }\n  const code = buildPinokioInjectUnmountScript()\n  const tasks = []\n  if (typeof frame.executeJavaScript === 'function') {\n    tasks.push(frame.executeJavaScript(code, false))\n  }\n  if (typeof frame.executeJavaScriptInIsolatedWorld === 'function') {\n    tasks.push(frame.executeJavaScriptInIsolatedWorld(\n      PINOKIO_INJECT_ISOLATED_WORLD_ID,\n      [{ code }],\n      false\n    ))\n  }\n  await Promise.allSettled(tasks)\n}\n\nconst executePinokioInjectDescriptor = async (frame, descriptor, context) => {\n  if (!frame || (typeof frame.isDestroyed === 'function' && frame.isDestroyed())) {\n    throw new Error('Target frame is not available.')\n  }\n  const sourceDescriptor = descriptor\n  const sourceUrl = resolvePinokioInjectSourceUrl(descriptor.src)\n  if (!sourceUrl) {\n    throw new Error(`Invalid injector source URL: ${descriptor.src}`)\n  }\n  const response = await fetch(sourceUrl, { cache: 'no-store' })\n  if (!response || !response.ok) {\n    const status = response ? response.status : 'unknown'\n    throw new Error(`Unable to load injector source: ${status}`)\n  }\n  const source = await response.text()\n  const resolvedDescriptor = {\n    ...sourceDescriptor,\n    src: sourceUrl\n  }\n  const code = buildPinokioInjectExecution({ descriptor: resolvedDescriptor, context, source })\n  if (descriptor.world === 'isolated') {\n    if (typeof frame.executeJavaScriptInIsolatedWorld !== 'function') {\n      throw new Error('Isolated-world frame injection is not supported by this Electron frame API.')\n    }\n    return frame.executeJavaScriptInIsolatedWorld(\n      PINOKIO_INJECT_ISOLATED_WORLD_ID,\n      [{ code, url: sourceUrl }],\n      false\n    )\n  }\n  return frame.executeJavaScript(code, false)\n}\n\nconst installInjectorHandlers = () => {\n  if (injectorHandlersInstalled) {\n    return\n  }\n  injectorHandlersInstalled = true\n\n  const updatePinokioInjectTargets = (ownerFrame, sender, payload = {}) => {\n    if (!ownerFrame || (typeof ownerFrame.isDestroyed === 'function' && ownerFrame.isDestroyed())) {\n      return { ok: false, reason: 'missing_frame', targets: [] }\n    }\n    const ownerKey = getFrameInjectorKey(ownerFrame)\n    const webContentsKey = getPinokioInjectWebContentsKey(sender, ownerFrame)\n    const targets = normalizePinokioInjectTargetRegistrations(payload && payload.targets)\n    frameInjectTargetRegistry.set(ownerKey, {\n      targets,\n      pageUrl: payload && payload.pageUrl ? payload.pageUrl : '',\n      webContentsKey,\n      updatedAt: Date.now()\n    })\n    return { ok: true, targets }\n  }\n\n  ipcMain.on('pinokio:update-inject-targets', (event, payload = {}) => {\n    updatePinokioInjectTargets(event.senderFrame, event.sender, payload)\n  })\n\n  ipcMain.on('pinokio:update-inject-targets-sync', (event, payload = {}) => {\n    event.returnValue = updatePinokioInjectTargets(event.senderFrame, event.sender, payload)\n  })\n\n  ipcMain.handle('pinokio:resolve-injectors', async (event, payload = {}) => {\n    const frame = event.senderFrame\n    if (!frame || (typeof frame.isDestroyed === 'function' && frame.isDestroyed())) {\n      return { ok: false, reason: 'missing_frame', inject: [], context: null }\n    }\n    const resolved = resolvePinokioInjectorsForFrame(frame, payload, event.sender)\n    return {\n      ok: true,\n      inject: resolved.inject,\n      context: resolved.context\n    }\n  })\n\n  ipcMain.handle('pinokio:reset-injectors', async (event, payload = {}) => {\n    const frame = event.senderFrame\n    if (!frame || (typeof frame.isDestroyed === 'function' && frame.isDestroyed())) {\n      return { ok: false, reason: 'missing_frame' }\n    }\n    const frameKey = getFrameInjectorKey(frame)\n    const syncId = typeof payload.syncId === 'number' ? payload.syncId : 0\n    frameInjectorSyncState.set(frameKey, syncId)\n    await resetPinokioInjectorsInFrame(frame)\n    return { ok: true, syncId }\n  })\n\n  ipcMain.handle('pinokio:mount-injectors', async (event, payload = {}) => {\n    const frame = event.senderFrame\n    if (!frame || (typeof frame.isDestroyed === 'function' && frame.isDestroyed())) {\n      return { ok: false, reason: 'missing_frame', applied: [], failed: [] }\n    }\n    const frameKey = getFrameInjectorKey(frame)\n    const syncId = typeof payload.syncId === 'number' ? payload.syncId : 0\n    if (syncId && frameInjectorSyncState.get(frameKey) !== syncId) {\n      return { ok: true, skipped: true, reason: 'stale_sync', applied: [], failed: [], syncId }\n    }\n    const baseContext = payload && payload.context && typeof payload.context === 'object'\n      ? { ...payload.context }\n      : {}\n    const injectList = Array.isArray(payload.inject) ? payload.inject : []\n    const applied = []\n    const failed = []\n\n    for (let index = 0; index < injectList.length; index += 1) {\n      if (syncId && frameInjectorSyncState.get(frameKey) !== syncId) {\n        return { ok: true, skipped: true, reason: 'stale_sync', applied, failed, syncId }\n      }\n      const normalizedDescriptor = normalizePinokioInjectDescriptor(injectList[index])\n      if (!normalizedDescriptor) {\n        continue\n      }\n      const descriptor = {\n        ...normalizedDescriptor,\n        runtimeId: `${frameKey}:${syncId}:${index}:${normalizedDescriptor.src}`\n      }\n      try {\n        await executePinokioInjectDescriptor(frame, descriptor, baseContext)\n        applied.push({\n          src: descriptor.src,\n          world: descriptor.world,\n          runtimeId: descriptor.runtimeId\n        })\n      } catch (error) {\n        const message = error && error.message ? error.message : String(error)\n        failed.push({\n          src: descriptor.src,\n          world: descriptor.world,\n          error: message\n        })\n        console.warn('[pinokio][main] injector mount failed', {\n          src: descriptor.src,\n          world: descriptor.world,\n          error: message\n        })\n      }\n    }\n\n    return {\n      ok: failed.length === 0,\n      applied,\n      failed,\n      syncId\n    }\n  })\n}\n\nconst normalizePermissionList = (value) => {\n  if (!value) return []\n  const list = Array.isArray(value) ? value : [value]\n  return list.map((item) => typeof item === 'string' ? item.trim() : '').filter(Boolean)\n}\n\nconst permissionLabels = {\n  microphone: 'Microphone',\n  camera: 'Camera',\n  screen: 'Screen Recording',\n  screen_capture: 'Screen Recording'\n}\n\nconst logPermission = (...args) => {\n  console.log('[PERMISSION]', ...args)\n}\n\nconst permissionHints = {\n  darwin: {\n    microphone: 'System Settings → Privacy & Security → Microphone',\n    camera: 'System Settings → Privacy & Security → Camera',\n    screen: 'System Settings → Privacy & Security → Screen Recording',\n    screen_capture: 'System Settings → Privacy & Security → Screen Recording'\n  },\n  win32: {\n    microphone: 'Settings → Privacy & security → Microphone (allow desktop apps)',\n    camera: 'Settings → Privacy & security → Camera (allow desktop apps)',\n    screen: 'Settings → Privacy & security → Screen recording',\n    screen_capture: 'Settings → Privacy & security → Screen recording'\n  },\n  linux: {\n    microphone: 'Check your sound settings (PipeWire/PulseAudio) and app permissions.',\n    camera: 'Check your video device permissions in system settings.',\n    screen: 'Check your desktop portal or compositor screen capture permissions.',\n    screen_capture: 'Check your desktop portal or compositor screen capture permissions.'\n  }\n}\n\nconst getMediaAccessStatusSafe = (mediaType) => {\n  if (!systemPreferences || typeof systemPreferences.getMediaAccessStatus !== 'function') {\n    return 'unsupported'\n  }\n  try {\n    return systemPreferences.getMediaAccessStatus(mediaType)\n  } catch (_) {\n    return 'unknown'\n  }\n}\n\nconst requestMediaPermission = async (permission) => {\n  const platform = process.platform\n  if (permission === 'microphone' || permission === 'camera') {\n    const preStatus = getMediaAccessStatusSafe(permission)\n    const canAsk = platform === 'darwin' && systemPreferences && typeof systemPreferences.askForMediaAccess === 'function'\n    logPermission('requestMediaPermission', permission, { platform, preStatus, canAsk })\n    let granted = false\n    if (platform === 'darwin' && systemPreferences && typeof systemPreferences.askForMediaAccess === 'function') {\n      granted = await systemPreferences.askForMediaAccess(permission)\n    }\n    const status = getMediaAccessStatusSafe(permission)\n    if (status === 'granted') {\n      granted = true\n    }\n    logPermission('requestMediaPermission result', permission, { status, granted })\n    return { status, granted }\n  }\n  if (permission === 'screen' || permission === 'screen_capture') {\n    const status = getMediaAccessStatusSafe('screen')\n    logPermission('requestMediaPermission screen', permission, { status })\n    return { status, granted: status === 'granted' }\n  }\n  logPermission('requestMediaPermission unsupported', permission)\n  return { status: 'unsupported', granted: false }\n}\n\nconst buildPermissionMessage = (platform, denied) => {\n  if (!denied.length) return ''\n  const items = denied.map((permission) => permissionLabels[permission] || permission)\n  const label = items.length === 1 ? items[0] : items.join(', ')\n  const hints = permissionHints[platform] || permissionHints.linux\n  const hint = denied.length === 1\n    ? (hints[denied[0]] || '')\n    : ''\n  if (hint) {\n    return `Pinokio needs ${label} access. Enable it in ${hint}.`\n  }\n  return `Pinokio needs ${label} access. Please enable it in your OS privacy settings.`\n}\n\nconst installPermissionHandlers = () => {\n  if (permissionHandlersInstalled) {\n    return\n  }\n  permissionHandlersInstalled = true\n  ipcMain.handle('pinokio:request-permissions', async (event, payload = {}) => {\n    const permissions = normalizePermissionList(payload.permissions)\n    if (permissions.length === 0) {\n      return { ok: true, permissions: [], results: {}, denied: [] }\n    }\n    const results = {}\n    const denied = []\n    for (const permission of permissions) {\n      const result = await requestMediaPermission(permission)\n      results[permission] = result\n      if (!result.granted) {\n        denied.push(permission)\n      }\n    }\n    return {\n      ok: denied.length === 0,\n      permissions,\n      denied,\n      results,\n      platform: process.platform,\n      message: denied.length ? buildPermissionMessage(process.platform, denied) : ''\n    }\n  })\n}\n\nconst canRequestPermission = (permission) => {\n  if (process.platform !== 'darwin') {\n    return false\n  }\n  return permission === 'microphone' || permission === 'camera'\n}\n\nconst promptForProjectPermissions = async (webContents, project, permissions) => {\n  if (!permissions.length) {\n    return\n  }\n  const promptKey = `${project}:${permissions.join(',')}`\n  if (permissionPromptInFlight.has(promptKey) || permissionPrompted.has(promptKey)) {\n    logPermission('prompt skipped (already prompted)', { project, permissions })\n    return\n  }\n  logPermission('prompt start', { project, permissions })\n  const pending = []\n  const blocked = []\n  const statusInfo = []\n  for (const permission of permissions) {\n    const statusTarget = (permission === 'screen' || permission === 'screen_capture') ? 'screen' : permission\n    const status = getMediaAccessStatusSafe(statusTarget)\n    if (status === 'granted') {\n      statusInfo.push({ permission, status, action: 'skip' })\n      continue\n    }\n    if (status === 'denied') {\n      blocked.push(permission)\n      statusInfo.push({ permission, status, action: 'blocked' })\n    } else if (canRequestPermission(permission)) {\n      pending.push(permission)\n      statusInfo.push({ permission, status, action: 'pending' })\n    } else {\n      blocked.push(permission)\n      statusInfo.push({ permission, status, action: 'blocked' })\n    }\n  }\n  logPermission('prompt status', statusInfo)\n  logPermission('prompt lists', { pending, blocked })\n  if (pending.length === 0 && blocked.length === 0) {\n    return\n  }\n  permissionPromptInFlight.add(promptKey)\n  try {\n    const owner = webContents && !webContents.isDestroyed()\n      ? BrowserWindow.fromWebContents(webContents)\n      : null\n    const denied = blocked.slice()\n    if (pending.length > 0) {\n      const label = pending.map((permission) => permissionLabels[permission] || permission).join(', ')\n      const { response } = await dialog.showMessageBox(owner, {\n        type: 'info',\n        buttons: ['Allow', 'Not now'],\n        defaultId: 0,\n        cancelId: 1,\n        title: 'Permission required',\n        message: `Allow ${label} access?`,\n        detail: `This app requests ${label} access. Click \"Allow\" to show the OS permission prompt.`,\n        noLink: true\n      })\n      logPermission('prompt response', { project, permissions: pending, response })\n      if (response === 0) {\n        for (const permission of pending) {\n          const result = await requestMediaPermission(permission)\n          if (!result.granted) {\n            denied.push(permission)\n          }\n        }\n      }\n    }\n    if (denied.length > 0) {\n      logPermission('prompt denied', { project, denied })\n      const message = buildPermissionMessage(process.platform, denied)\n      if (message) {\n        await dialog.showMessageBox(owner, {\n          type: 'warning',\n          buttons: ['OK'],\n          defaultId: 0,\n          message,\n          noLink: true\n        })\n      }\n    }\n  } finally {\n    permissionPromptInFlight.delete(promptKey)\n    permissionPrompted.add(promptKey)\n  }\n}\n\n// Screenshot capture function for inspect mode\nconst captureScreenshotRegion = async (bounds) => {\n  try {\n    const { nativeImage } = require('electron')\n    \n    // Get all displays to find the correct one\n    const displays = screen.getAllDisplays()\n    const primaryDisplay = screen.getPrimaryDisplay()\n    \n    // Get desktop capturer sources with full resolution\n    const sources = await desktopCapturer.getSources({ \n      types: ['screen'],\n      thumbnailSize: {\n        width: primaryDisplay.bounds.width * primaryDisplay.scaleFactor,\n        height: primaryDisplay.bounds.height * primaryDisplay.scaleFactor\n      }\n    })\n    \n    if (sources.length === 0) {\n      throw new Error('No screen sources available')\n    }\n    \n    // Find the screen source that matches our primary display\n    let screenSource = sources[0] // fallback to first source\n    \n    // Try to find the exact screen source by name or use the first one\n    for (const source of sources) {\n      if (source.name.includes('Entire Screen') || source.name.includes('Screen 1')) {\n        screenSource = source\n        break\n      }\n    }\n    \n    // Get the full resolution screenshot from thumbnail\n    const thumbnailImage = screenSource.thumbnail\n    const fullScreenshotBuffer = thumbnailImage.toPNG()\n    const fullScreenshot = nativeImage.createFromBuffer(fullScreenshotBuffer)\n    \n    // Calculate the actual pixel bounds accounting for device pixel ratio\n    const scaleFactor = primaryDisplay.scaleFactor\n    const actualBounds = {\n      x: Math.max(0, Math.round(bounds.x * scaleFactor)),\n      y: Math.max(0, Math.round(bounds.y * scaleFactor)),\n      width: Math.min(\n        Math.round(bounds.width * scaleFactor),\n        fullScreenshot.getSize().width - Math.round(bounds.x * scaleFactor)\n      ),\n      height: Math.min(\n        Math.round(bounds.height * scaleFactor),\n        fullScreenshot.getSize().height - Math.round(bounds.y * scaleFactor)\n      )\n    }\n    \n    // Ensure minimum size\n    actualBounds.width = Math.max(1, actualBounds.width)\n    actualBounds.height = Math.max(1, actualBounds.height)\n    \n    // Crop the screenshot to the element bounds\n    const croppedImage = fullScreenshot.crop(actualBounds)\n    \n    // Convert to PNG buffer and then to data URL\n    const croppedBuffer = croppedImage.toPNG()\n    const dataUrl = 'data:image/png;base64,' + croppedBuffer.toString('base64')\n    \n    console.log(`Screenshot captured: ${actualBounds.width}x${actualBounds.height} at (${actualBounds.x},${actualBounds.y})`)\n    \n    return dataUrl\n  } catch (error) {\n    console.warn('Screenshot capture failed:', error)\n    throw error\n  }\n}\n\n\n//function enable_cors(win) {\n//  win.webContents.session.webRequest.onBeforeSendHeaders((details, callback) => {\n//    details.requestHeaders['Origin'] = null;\n//    details.headers['Origin'] = null;\n//    callback({ requestHeaders: details.requestHeaders })\n//  });\n////  win.webContents.session.webRequest.onBeforeSendHeaders(\n////    (details, callback) => {\n////      const { requestHeaders } = details;\n////      UpsertKeyValue(requestHeaders, 'Access-Control-Allow-Origin', ['*']);\n////      callback({ requestHeaders });\n////    },\n////  );\n////\n////  win.webContents.session.webRequest.onHeadersReceived((details, callback) => {\n////    const { responseHeaders } = details;\n////    UpsertKeyValue(responseHeaders, 'Access-Control-Allow-Origin', ['*']);\n////    UpsertKeyValue(responseHeaders, 'Access-Control-Allow-Headers', ['*']);\n////    callback({\n////      responseHeaders,\n////    });\n////  });\n//}\n\n\nconst pushContextMenuSeparator = (template) => {\n  if (!template.length) {\n    return\n  }\n  if (template[template.length - 1].type === 'separator') {\n    return\n  }\n  template.push({ type: 'separator' })\n}\nconst buildBrowserContextMenuTemplate = (webContents, params = {}) => {\n  const template = []\n  const linkURL = typeof params.linkURL === 'string' ? params.linkURL : ''\n  const srcURL = typeof params.srcURL === 'string' ? params.srcURL : ''\n  const selectionText = typeof params.selectionText === 'string' ? params.selectionText : ''\n  const hasSelection = selectionText.trim().length > 0\n  const editFlags = params.editFlags || {}\n  const isEditable = Boolean(params.isEditable)\n  const hasMediaSource = typeof params.mediaType === 'string' && params.mediaType !== 'none' && srcURL\n  const canSuggestSpelling = Array.isArray(params.dictionarySuggestions) && params.dictionarySuggestions.length > 0\n  const hasMisspelledWord = typeof params.misspelledWord === 'string' && params.misspelledWord.length > 0\n  const owner = webContents && !webContents.isDestroyed() ? webContents.getOwnerBrowserWindow() : null\n  const canGoBack = Boolean(webContents && webContents.canGoBack && webContents.canGoBack())\n  const canGoForward = Boolean(webContents && webContents.canGoForward && webContents.canGoForward())\n\n  if (linkURL) {\n    template.push({\n      label: 'Open Link in New Window',\n      click: () => {\n        try {\n          if (typeof loadNewWindow === 'function' && PORT) {\n            if (popupShellManager.isPinokioWindowUrl(linkURL, root_url)) {\n              loadNewWindow(linkURL, PORT)\n            } else {\n              popupShellManager.openExternalWindow({ url: linkURL })\n            }\n            return\n          }\n        } catch (error) {\n        }\n        shell.openExternal(linkURL).catch(() => {})\n      }\n    })\n    template.push({\n      label: 'Open Link in Browser',\n      click: () => {\n        shell.openExternal(linkURL).catch(() => {})\n      }\n    })\n    template.push({\n      label: 'Copy Link Address',\n      click: () => clipboard.writeText(linkURL)\n    })\n    pushContextMenuSeparator(template)\n  }\n\n  if (hasMediaSource) {\n    template.push({\n      label: 'Open Media in Browser',\n      click: () => {\n        shell.openExternal(srcURL).catch(() => {})\n      }\n    })\n    template.push({\n      label: 'Copy Media Address',\n      click: () => clipboard.writeText(srcURL)\n    })\n    pushContextMenuSeparator(template)\n  }\n\n  if (!isEditable) {\n    template.push({\n      label: 'Back',\n      enabled: canGoBack,\n      click: () => {\n        if (webContents && !webContents.isDestroyed() && webContents.canGoBack()) {\n          webContents.goBack()\n        }\n      }\n    })\n    template.push({\n      label: 'Forward',\n      enabled: canGoForward,\n      click: () => {\n        if (webContents && !webContents.isDestroyed() && webContents.canGoForward()) {\n          webContents.goForward()\n        }\n      }\n    })\n    template.push({\n      label: 'Reload',\n      click: () => {\n        if (webContents && !webContents.isDestroyed()) {\n          webContents.reload()\n        }\n      }\n    })\n    pushContextMenuSeparator(template)\n  }\n\n  if (isEditable) {\n    if (canSuggestSpelling && hasMisspelledWord) {\n      for (const suggestion of params.dictionarySuggestions.slice(0, 5)) {\n        template.push({\n          label: suggestion,\n          click: () => {\n            if (webContents && !webContents.isDestroyed()) {\n              webContents.replaceMisspelling(suggestion)\n            }\n          }\n        })\n      }\n      template.push({\n        label: 'Add to Dictionary',\n        click: () => {\n          try {\n            if (webContents && !webContents.isDestroyed() && webContents.session && typeof webContents.session.addWordToSpellCheckerDictionary === 'function') {\n              webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord)\n            }\n          } catch (error) {\n          }\n        }\n      })\n      pushContextMenuSeparator(template)\n    }\n    template.push({ role: 'undo', enabled: editFlags.canUndo !== false })\n    template.push({ role: 'redo', enabled: editFlags.canRedo !== false })\n    pushContextMenuSeparator(template)\n    template.push({ role: 'cut', enabled: editFlags.canCut !== false })\n    template.push({ role: 'copy', enabled: editFlags.canCopy !== false })\n    template.push({ role: 'paste', enabled: editFlags.canPaste !== false })\n    template.push({ role: 'delete', enabled: editFlags.canDelete !== false })\n    pushContextMenuSeparator(template)\n    template.push({ role: 'selectAll' })\n  } else {\n    if (hasSelection) {\n      template.push({ role: 'copy' })\n    }\n    template.push({ role: 'selectAll' })\n  }\n\n  pushContextMenuSeparator(template)\n  template.push({\n    label: 'Inspect Element',\n    click: () => {\n      if (!webContents || webContents.isDestroyed()) {\n        return\n      }\n      if (!webContents.isDevToolsOpened()) {\n        webContents.openDevTools({ mode: 'detach' })\n      }\n      const x = typeof params.x === 'number' ? params.x : null\n      const y = typeof params.y === 'number' ? params.y : null\n      if (x !== null && y !== null) {\n        webContents.inspectElement(x, y)\n      }\n    }\n  })\n\n  if (template.length && template[template.length - 1].type === 'separator') {\n    template.pop()\n  }\n\n  if (owner && owner.isDestroyed()) {\n    return []\n  }\n  return template\n}\nconst attach = (event, webContents) => {\n  let wc = webContents\n\n  if (ENABLE_BROWSER_CONSOLE_LOG && !attachedConsoleListeners.has(webContents)) {\n    attachedConsoleListeners.add(webContents)\n    webContents.on('console-message', (event, level, message, line, sourceId) => {\n      if (!root_url) {\n        return\n      }\n      const state = browserConsoleState.get(webContents)\n      let pageUrl = state && state.url ? state.url : ''\n      if (!pageUrl) {\n        try {\n          pageUrl = webContents.getURL()\n        } catch (err) {\n          pageUrl = ''\n        }\n      }\n      if (!pageUrl || !pageUrl.startsWith(root_url)) {\n        return\n      }\n      const targetFile = ensureBrowserLogFile()\n      if (!targetFile) {\n        return\n      }\n      const logUrl = resolveConsoleSourceUrl(sourceId, pageUrl)\n      if (!logUrl || !shouldLogUrl(logUrl)) {\n        return\n      }\n      const timestamp = new Date().toISOString()\n      const levelLabel = consoleLevelLabels[level] || 'log'\n      let location = ''\n      if (sourceId) {\n        location = ` (${sourceId}${line ? `:${line}` : ''})`\n      } else if (line) {\n        location = ` (:${line})`\n      }\n      const entry = `[${timestamp}]\\t${logUrl}\\t[${levelLabel}] ${message}${location}\\n`\n      browserLogBuffer.push(entry)\n      if (browserLogBuffer.length > 100) {\n        browserLogBuffer.shift()\n      }\n      browserLogWritePromise = browserLogWritePromise.then(() => fs.promises.writeFile(targetFile, browserLogBuffer.join(''))).catch((err) => {\n        console.error('[BROWSER LOG] Failed to persist console output', err)\n      })\n    })\n    webContents.once('destroyed', () => {\n      clearBrowserConsoleState(webContents)\n    })\n  }\n  // Enable screen capture permissions for all webContents\n  webContents.session.setPermissionRequestHandler((webContents, permission, callback) => {\n    callback(true)\n    //console.log(`[PERMISSION DEBUG] Permission requested: \"${permission}\" from webContents`)\n    //if (permission === 'media' || permission === 'display-capture' || permission === 'desktopCapture') {\n    //  console.log(`[PERMISSION DEBUG] Granting permission: \"${permission}\"`)\n    //  callback(true)\n    //} else {\n    //  console.log(`[PERMISSION DEBUG] Denying permission: \"${permission}\"`)\n    //  callback(false)\n    //}\n  })\n\n  webContents.session.setPermissionCheckHandler((webContents, permission) => {\n    return true\n    //console.log(`[PERMISSION DEBUG] Permission check for: \"${permission}\"`)\n    //return permission === 'media' || permission === 'display-capture' || permission === 'desktopCapture'\n  })\n\n  webContents.session.setDisplayMediaRequestHandler((request, callback) => {\n    console.log('[DISPLAY MEDIA DEBUG] Display media request received')\n    desktopCapturer.getSources({ types: ['screen', 'window'] }).then((sources) => {\n      console.log('[DISPLAY MEDIA DEBUG] Available sources:', sources.length)\n      if (sources.length > 0) {\n        callback({ video: sources[0], audio: 'loopback' })\n      } else {\n        callback({})\n      }\n    }).catch(err => {\n      console.error('[DISPLAY MEDIA DEBUG] Error getting sources:', err)\n      callback({})\n    })\n  })\n\n  webContents.on('will-prevent-unload', (event) => {\n    event.preventDefault()\n  })\n\n  webContents.on('will-navigate', (event, url) => {\n    if (!webContents.opened) {\n      // The first time this view is being used, set the \"opened\" to true, and don't do anything\n      // The next time the view navigates, \"the \"opened\" is already true, so trigger the URL open logic\n      //  - if the new URL has the same host as the app's url, open in app\n      //  - if it's a remote host, open in external browser\n      webContents.opened = true\n    } else {\n//      console.log(\"will-navigate\", { event, url })\n      const owner = webContents.getOwnerBrowserWindow()\n      if (openNonPinokioNavigationInPopup({ event, owner, url })) {\n        return\n      }\n      const target = safeParseUrl(url, root_url || undefined)\n      if (target && !popupShellManager.isPinokioWindowUrl(target.href, root_url) && target.protocol !== 'http:' && target.protocol !== 'https:') {\n        event.preventDefault()\n        shell.openExternal(target.href)\n      }\n    }\n  })\n  webContents.on('will-frame-navigate', (event) => {\n    const owner = webContents.getOwnerBrowserWindow()\n    const frame = event && event.frame\n    const isDirectChildFrame = Boolean(\n      frame &&\n      webContents.mainFrame &&\n      frame.parent &&\n      !frame.parent.parent &&\n      frame.parent === webContents.mainFrame\n    )\n    if (!isDirectChildFrame) {\n      return\n    }\n    const currentUrl = (() => {\n      try {\n        return webContents.getURL()\n      } catch (_) {\n        return ''\n      }\n    })()\n    if (!isRootShellUrl(currentUrl)) {\n      return\n    }\n    openNonPinokioNavigationInPopup({\n      event,\n      owner,\n      url: event && event.url,\n      frame\n    })\n  })\n//  webContents.session.defaultSession.loadExtension('path/to/unpacked/extension').then(({ id }) => {\n//  })\n\n\n  webContents.session.webRequest.onHeadersReceived((details, callback) => {\n//    console.log(\"details\", details)\n//    console.log(\"responseHeaders\", JSON.stringify(details.responseHeaders, null, 2))\n\n\n\n    // 1. Remove X-Frame-Options\n    if (details.responseHeaders[\"X-Frame-Options\"]) {\n      delete details.responseHeaders[\"X-Frame-Options\"] \n    } else if (details.responseHeaders[\"x-frame-options\"]) {\n      delete details.responseHeaders[\"x-frame-options\"] \n    }\n\n    // 2. Remove Content-Security-Policy \"frame-ancestors\" attribute\n    let csp\n    let csp_type;\n    if (details.responseHeaders[\"Content-Security-Policy\"]) {\n      csp = details.responseHeaders[\"Content-Security-Policy\"]\n      csp_type = 0\n    } else if (details.responseHeaders['content-security-policy']) {\n      csp = details.responseHeaders[\"content-security-policy\"]\n      csp_type = 1\n    }\n\n    if (details.responseHeaders[\"cross-origin-opener-policy-report-only\"]) {\n      delete details.responseHeaders[\"cross-origin-opener-policy-report-only\"]\n    } else if (details.responseHeaders[\"Cross-Origin-Opener-Policy-Report-Only\"]) {\n      delete details.responseHeaders[\"Cross-Origin-Opener-Policy-Report-Only\"]\n    }\n\n\n    if (csp) {\n//      console.log(\"CSP\", csp)\n      // find /frame-ancestors ;$/\n      let new_csp = csp.map((c) => {\n        return c.replaceAll(/frame-ancestors[^;]+;?/gi, \"\")\n      })\n\n//      console.log(\"new_csp = \", new_csp)\n\n      const r = {\n        responseHeaders: details.responseHeaders\n      }\n      if (csp_type === 0) {\n        r.responseHeaders[\"Content-Security-Policy\"] = new_csp\n      } else if (csp_type === 1) {\n        r.responseHeaders[\"content-security-policy\"] = new_csp\n      }\n//      console.log(\"R\", JSON.stringify(r, null, 2))\n\n      callback(r)\n    } else {\n//      console.log(\"RH\", details.responseHeaders)\n      callback({\n        responseHeaders: details.responseHeaders\n      })\n    }\n  })\n\n\n\n  webContents.session.webRequest.onBeforeSendHeaders((details, callback) => {\n\n    let ua = details.requestHeaders['User-Agent']\n//    console.log(\"User Agent Before\", ua)\n    if (ua) {\n      ua = ua.replace(/ pinokio\\/[0-9.]+/i, '');\n      ua = ua.replace(/Electron\\/.+ /i,'');\n//      console.log(\"User Agent After\", ua)\n      details.requestHeaders['User-Agent'] = ua;\n    }\n\n\n//    console.log(\"REQ\", details)\n//    console.log(\"HEADER BEFORE\", details.requestHeaders)\n//    // Remove all sec-fetch-* headers\n//    for(let key in details.requestHeaders) {\n//      if (key.toLowerCase().startsWith(\"sec-\")) {\n//        delete details.requestHeaders[key]\n//      }\n//    }\n//    console.log(\"HEADER AFTER\", details.requestHeaders)\n    callback({ cancel: false, requestHeaders: details.requestHeaders });\n  });\n\n\n//  webContents.session.webRequest.onBeforeSendHeaders(\n//    (details, callback) => {\n//      const { requestHeaders } = details;\n//      UpsertKeyValue(requestHeaders, 'Access-Control-Allow-Origin', ['*']);\n//      callback({ requestHeaders });\n//    },\n//  );\n//\n//  webContents.session.webRequest.onHeadersReceived((details, callback) => {\n//    const { responseHeaders } = details;\n//    UpsertKeyValue(responseHeaders, 'Access-Control-Allow-Origin', ['*']);\n//    UpsertKeyValue(responseHeaders, 'Access-Control-Allow-Headers', ['*']);\n//    callback({\n//      responseHeaders,\n//    });\n//  });\n\n//  webContents.session.webRequest.onBeforeSendHeaders((details, callback) => {\n//    //console.log(\"Before\", { details })\n//    if (details.requestHeaders) details.requestHeaders['Origin'] = null;\n//    if (details.requestHeaders) details.requestHeaders['Referer'] = null;\n//    if (details.requestHeaders) details.requestHeaders['referer'] = null;\n//    if (details.headers) details.headers['Origin'] = null;\n//    if (details.headers) details.headers['Referer'] = null;\n//    if (details.headers) details.headers['referer'] = null;\n//\n//    if (details.referrer) details.referrer = null\n//    //console.log(\"After\", { details })\n//    callback({ requestHeaders: details.requestHeaders })\n//  });\n\n//  webContents.on(\"did-create-window\", (parentWindow, details) => {\n//    const view = new BrowserView();\n//    parentWindow.setBrowserView(view);\n//    view.setBounds({ x: 0, y: 30, width: parentWindow.getContentBounds().width, height: parentWindow.getContentBounds().height - 30 });\n//    view.setAutoResize({ width: true, height: true });\n//    view.webContents.loadURL(details.url);\n//  })\n  webContents.on('did-navigate', (event, url) => {\n    let win = webContents.getOwnerBrowserWindow()\n    if (win && typeof win.setTitleBarOverlay === \"function\") {\n      const overlay = titleBarOverlay(colors)\n      setWindowTitleBarOverlay(win, overlay)\n    }\n    launched = true\n\n    updateBrowserConsoleTarget(webContents, url)\n\n  })\n  webContents.on('did-navigate-in-page', (event, url) => {\n    updateBrowserConsoleTarget(webContents, url)\n  })\n  webContents.on('context-menu', (event, params) => {\n    const template = buildBrowserContextMenuTemplate(webContents, params)\n    if (!template.length) {\n      return\n    }\n    const menu = Menu.buildFromTemplate(template)\n    const win = webContents.getOwnerBrowserWindow()\n    if (win && !win.isDestroyed()) {\n      menu.popup({ window: win })\n      return\n    }\n    menu.popup()\n  })\n  webContents.setWindowOpenHandler((config) => {\n    let url = config.url\n    let features = config.features || \"\"\n    let disposition = config.disposition || \"\"\n    let params = new URLSearchParams(features.split(\",\").join(\"&\"))\n    let win = wc.getOwnerBrowserWindow()\n    let [width, height] = win.getSize()\n    let [x,y] = win.getPosition()\n\n    // if the origin is the same as the pinokio host,\n    // always open in new window\n\n    // if not, check the features\n    // if features exists and it's app or self, open in pinokio\n    // otherwise if it's file, \n\n    if (/(^|,)\\s*pinokio\\s*(,|$)/i.test(features)) {\n      const targetUrl = popupShellManager.resolveTargetUrl({\n        url,\n        openerWebContents: wc,\n        rootUrl: root_url\n      })\n      if (targetUrl) {\n        if (popupShellManager.isPinokioWindowUrl(targetUrl, root_url)) {\n          loadNewWindow(targetUrl, PORT)\n        } else {\n          popupShellManager.openExternalWindow({ url: targetUrl })\n        }\n      }\n      return { action: 'deny' };\n    }\n\n    if (features === \"browser\") {\n      shell.openExternal(url);\n      return { action: 'deny' };\n    } else if (disposition === \"foreground-tab\" || disposition === \"background-tab\") {\n      const targetUrl = popupShellManager.resolveTargetUrl({\n        url,\n        openerWebContents: wc,\n        rootUrl: root_url\n      })\n      if (targetUrl) {\n        popupShellManager.openExternalWindow({ url: targetUrl })\n      }\n      return { action: 'deny' };\n    } else if (popupShellManager.isPinokioWindowUrl(url, root_url)) {\n      return {\n        action: 'allow',\n        outlivesOpener: true,\n        overrideBrowserWindowOptions: {\n          width: (params.get(\"width\") ? parseInt(params.get(\"width\")) : width),\n          height: (params.get(\"height\") ? parseInt(params.get(\"height\")) : height),\n          x: x + 30,\n          y: y + 30,\n\n          parent: null,\n          titleBarStyle : \"hidden\",\n          titleBarOverlay : titleBarOverlay(colors),\n          webPreferences: {\n            session: session.defaultSession,\n            webSecurity: false,\n            spellcheck: false,\n            nativeWindowOpen: true,\n            contextIsolation: false,\n            nodeIntegrationInSubFrames: true,\n            preload: path.join(__dirname, 'preload.js')\n          },\n        }\n      }\n    } else {\n      if (features.startsWith(\"app\") || features.startsWith(\"self\")) {\n        return popupShellManager.createPopupResponse({ params, width, height, x, y })\n      }\n      if (features.startsWith(\"file\")) {\n        let u = features.replace(\"file://\", \"\")\n        shell.showItemInFolder(u)\n        return { action: 'deny' };\n      }\n      const targetUrl = popupShellManager.resolveTargetUrl({\n        url,\n        openerWebContents: wc,\n        rootUrl: root_url\n      })\n      if (targetUrl) {\n        popupShellManager.openExternalWindow({ url: targetUrl })\n      }\n      return { action: 'deny' };\n    }\n\n//    if (origin === root_url) {\n//      // if the origin is the same as pinokio, open in pinokio\n//      // otherwise open in external browser\n//      if (features) {\n//        if (features.startsWith(\"app\") || features.startsWith(\"self\")) {\n//          return {\n//            action: 'allow',\n//            outlivesOpener: true,\n//            overrideBrowserWindowOptions: {\n//              width: (params.get(\"width\") ? parseInt(params.get(\"width\")) : width),\n//              height: (params.get(\"height\") ? parseInt(params.get(\"height\")) : height),\n//              x: x + 30,\n//              y: y + 30,\n//\n//              parent: null,\n//              titleBarStyle : \"hidden\",\n//              titleBarOverlay : titleBarOverlay(\"default\"),\n//            }\n//          }\n//        } else if (features.startsWith(\"file\")) {\n//          let u = features.replace(\"file://\", \"\")\n//          shell.showItemInFolder(u)\n//          return { action: 'deny' };\n//        } else {\n//          return { action: 'deny' };\n//        }\n//      } else {\n//        if (features.startsWith(\"file\")) {\n//          let u = features.replace(\"file://\", \"\")\n//          shell.showItemInFolder(u)\n//          return { action: 'deny' };\n//        } else {\n//          shell.openExternal(url);\n//          return { action: 'deny' };\n//        }\n//      }\n//    } else {\n//      if (features.startsWith(\"file\")) {\n//        let u = features.replace(\"file://\", \"\")\n//        shell.showItemInFolder(u)\n//        return { action: 'deny' };\n//      } else {\n//        shell.openExternal(url);\n//        return { action: 'deny' };\n//      }\n//    }\n  });\n}\nconst getWinState = (url, options) => {\n  let filename\n  try {\n    let pathname = new URL(url).pathname.slice(1)\n    filename = pathname.slice(\"/\").join(\"-\")\n  } catch {\n    filename = \"index.json\"\n  }\n  let state = windowStateKeeper({\n    file: filename,\n    ...options\n  });\n  return state\n}\nconst createWindow = (port) => {\n\n\n  let mainWindowState = windowStateKeeper({\n//    file: \"index.json\",\n    defaultWidth: 1000,\n    defaultHeight: 800\n  });\n\n  mainWindow = new BrowserWindow({\n    titleBarStyle : \"hidden\",\n    titleBarOverlay : titleBarOverlay(colors),\n    x: mainWindowState.x,\n    y: mainWindowState.y,\n    width: mainWindowState.width,\n    height: mainWindowState.height,\n    minWidth: 190,\n    webPreferences: {\n      session: session.defaultSession,\n      webSecurity: false,\n      spellcheck: false,\n      nativeWindowOpen: true,\n      contextIsolation: false,\n      nodeIntegrationInSubFrames: true,\n      enableRemoteModule: false,\n      experimentalFeatures: true,\n      preload: path.join(__dirname, 'preload.js')\n    },\n  })\n  mainWindow.on('closed', () => {\n    mainWindow = null\n  })\n\n  // Debug media device availability\n  mainWindow.webContents.once('did-finish-load', () => {\n    console.log('[MEDIA DEBUG] Main window loaded, checking media devices availability...')\n    mainWindow.webContents.executeJavaScript(`\n      console.log('[MEDIA DEBUG] navigator.mediaDevices available:', !!navigator.mediaDevices);\n      console.log('[MEDIA DEBUG] getDisplayMedia available:', !!(navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia));\n      console.log('[MEDIA DEBUG] getUserMedia available:', !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia));\n      if (navigator.mediaDevices && navigator.mediaDevices.getSupportedConstraints) {\n        console.log('[MEDIA DEBUG] Supported constraints:', navigator.mediaDevices.getSupportedConstraints());\n      }\n    `).catch(err => console.error('[MEDIA DEBUG] Error checking media devices:', err))\n    if (updateBannerPayload && !(updateBannerPayload.state === 'available' && updateBannerDismissed)) {\n      mainWindow.webContents.send('pinokio:update-banner', updateBannerPayload)\n    }\n  })\n\n  // Enable screen capture permissions\n  mainWindow.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => {\n    callback(true)\n    //console.log(`[PERMISSION DEBUG] MainWindow permission requested: \"${permission}\"`)\n    //if (permission === 'media' || permission === 'display-capture' || permission === 'desktopCapture') {\n    //  console.log(`[PERMISSION DEBUG] MainWindow granting permission: \"${permission}\"`)\n    //  callback(true)\n    //} else {\n    //  console.log(`[PERMISSION DEBUG] MainWindow denying permission: \"${permission}\"`)\n    //  callback(false)\n    //}\n  })\n//  enable_cors(mainWindow)\n  if(\"\" + port === \"80\") {\n    root_url = `http://localhost`\n  } else {\n    root_url = `http://localhost:${port}`\n  }\n  mainWindow.loadURL(root_url)\n//  mainWindow.maximize();\n  mainWindowState.manage(mainWindow);\n\n}\nconst loadNewWindow = (url, port) => {\n\n\n  let winState = windowStateKeeper({\n//    file: \"index.json\",\n    defaultWidth: 1000,\n    defaultHeight: 800\n  });\n\n  let win = new BrowserWindow({\n    titleBarStyle : \"hidden\",\n    titleBarOverlay : titleBarOverlay(colors),\n    x: winState.x,\n    y: winState.y,\n    width: winState.width,\n    height: winState.height,\n    minWidth: 190,\n    webPreferences: {\n      session: session.defaultSession,\n      webSecurity: false,\n      spellcheck: false,\n      nativeWindowOpen: true,\n      contextIsolation: false,\n      nodeIntegrationInSubFrames: true,\n      enableRemoteModule: false,\n      experimentalFeatures: true,\n      preload: path.join(__dirname, 'preload.js')\n    },\n  })\n  installForceDestroyOnClose(win)\n\n  // Enable screen capture permissions\n  win.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => {\n    callback(true)\n    //console.log(`[PERMISSION DEBUG] New window permission requested: \"${permission}\"`)\n    //if (permission === 'media' || permission === 'display-capture' || permission === 'desktopCapture') {\n    //  console.log(`[PERMISSION DEBUG] New window granting permission: \"${permission}\"`)\n    //  callback(true)\n    //} else {\n    //  console.log(`[PERMISSION DEBUG] New window denying permission: \"${permission}\"`)\n    //  callback(false)\n    //}\n  })\n\n//  enable_cors(win)\n  win.focus()\n  win.loadURL(url)\n  winState.manage(win)\n\n}\npopupShellManager.setPinokioHomeWindowOpener(() => {\n  if (root_url && PORT) {\n    loadNewWindow(root_url, PORT)\n  }\n})\n\n\nif (process.defaultApp) {\n  if (process.argv.length >= 2) {\n    app.setAsDefaultProtocolClient('pinokio', process.execPath, [path.resolve(process.argv[1])])\n  }\n} else {\n  app.setAsDefaultProtocolClient('pinokio')\n}\n\nconst gotTheLock = app.requestSingleInstanceLock()\n\n\nif (!gotTheLock) {\n  app.quit()\n} else {\n  app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {\n    \n      // Prevent having error\n      event.preventDefault()\n      // and continue\n      callback(true)\n\n  })\n\n  app.on('second-instance', (event, argv) => {\n\n    if (!mainWindow || mainWindow.isDestroyed()) {\n      createWindow(PORT)\n    } else {\n      if (mainWindow.isMinimized()) mainWindow.restore()\n      mainWindow.focus()\n    }\n    const url = [...argv].reverse().find(arg => typeof arg === 'string' && arg.startsWith('pinokio:'))\n    if (!url) {\n      return\n    }\n    //let u = new URL(url).search\n    let u = url.replace(/pinokio:[\\/]+/, \"\")\n    loadNewWindow(`${root_url}/pinokio/${u}`, PORT)\n//    if (BrowserWindow.getAllWindows().length === 0 || !mainWindow) createWindow(PORT)\n//    mainWindow.focus()\n//    mainWindow.loadURL(`${root_url}/pinokio/${u}`)\n  })\n\n  // Create mainWindow, load the rest of the app, etc...\n  // Enable desktop capture for getDisplayMedia support (must be before app ready)\n  app.commandLine.appendSwitch('disable-features', 'LazyImageLoading')\n  app.commandLine.appendSwitch('enable-experimental-web-platform-features');\n  app.commandLine.appendSwitch('enable-features', 'GetDisplayMediaSet,GetDisplayMediaSetAutoSelectAllScreens');\n  \n  app.whenReady().then(async () => {\n    console.log('App is ready, about to install inspector handlers...')\n    app.userAgentFallback = \"Pinokio\"\n\n    installInspectorHandlers()\n    installInjectorHandlers()\n    installPermissionHandlers()\n    installClosePopupOnDownload(session.defaultSession)\n    installClosePopupOnDownload(popupShellManager.getPopupBrowserSession())\n\n    ipcMain.on('pinokio:update-banner-action', (_event, payload = {}) => {\n      const action = payload && payload.action\n      if (!action) {\n        return\n      }\n      if (updateTestMode) {\n        if (action === 'update') {\n          startUpdateBannerTestDownload()\n          return\n        }\n        if (action === 'restart') {\n          simulateUpdateBannerRestart()\n          return\n        }\n        if (action === 'dismiss') {\n          updateBannerDismissed = true\n          hideUpdateBanner()\n          return\n        }\n        if (action === 'release-notes') {\n          const target = payload && payload.releaseUrl ? payload.releaseUrl : UPDATE_RELEASES_URL\n          shell.openExternal(target)\n          return\n        }\n      }\n      if (action === 'update') {\n        if (updateDownloadInFlight) {\n          return\n        }\n        updateDownloadInFlight = true\n        updateBannerDismissed = false\n        showUpdateBanner(buildUpdateBannerPayload('downloading', updateInfo, { progressPercent: 0 }))\n        updater.downloadUpdate().catch((err) => {\n          updateDownloadInFlight = false\n          const message = err && err.message ? err.message : 'Update failed'\n          showUpdateBanner(buildUpdateBannerPayload('error', updateInfo, { errorMessage: message }))\n        })\n        return\n      }\n      if (action === 'restart') {\n        updater.quitAndInstall()\n        return\n      }\n      if (action === 'dismiss') {\n        updateBannerDismissed = true\n        hideUpdateBanner()\n        return\n      }\n      if (action === 'release-notes') {\n        const target = payload && payload.releaseUrl ? payload.releaseUrl : UPDATE_RELEASES_URL\n        shell.openExternal(target)\n      }\n    })\n\n    // PROMPT\n    let promptResponse\n    ipcMain.on('prompt', function(eventRet, arg) {\n      promptResponse = null\n      const point = screen.getCursorScreenPoint()\n      const display = screen.getDisplayNearestPoint(point)\n      const bounds = display.bounds\n\n//      const bounds = focused.getBounds()\n      let promptWindow = new BrowserWindow({\n        x: bounds.x + bounds.width/2 - 200,\n        y: bounds.y + bounds.height/2 - 60,\n        width: 400,\n        height: 120,\n        //width: 1000,\n        //height: 500,\n        show: false,\n        resizable: false,\n//        movable: false,\n//        alwaysOnTop: true,\n        frame: false,\n        webPreferences: {\n          session: session.defaultSession,\n          webSecurity: false,\n          spellcheck: false,\n          nativeWindowOpen: true,\n          contextIsolation: false,\n          nodeIntegrationInSubFrames: true,\n          preload: path.join(__dirname, 'preload.js')\n        },\n      })\n      arg.val = arg.val || ''\n      const promptHtml = `<html><body><form><label for=\"val\">${arg.title}</label>\n<input id=\"val\" value=\"${arg.val}\" autofocus />\n<button id='ok'>OK</button>\n<button id='cancel'>Cancel</button></form>\n<style>body {font-family: sans-serif;} form {padding: 5px; } button {float:right; margin-left: 10px;} label { display: block; margin-bottom: 5px; width: 100%; } input {margin-bottom: 10px; padding: 5px; width: 100%; display:block;}</style>\n<script>\ndocument.querySelector(\"#cancel\").addEventListener(\"click\", (e) => {\n  debugger\n  e.preventDefault()\n  e.stopPropagation()\n  window.close()\n})\ndocument.querySelector(\"form\").addEventListener(\"submit\", (e) => {\n  e.preventDefault()\n  e.stopPropagation()\n  debugger\n  window.electronAPI.send('prompt-response', document.querySelector(\"#val\").value)\n  window.close()\n})\n</script></body></html>`\n\n//      promptWindow.loadFile(\"prompt.html\")\n      promptWindow.loadURL('data:text/html,' + encodeURIComponent(promptHtml))\n      promptWindow.show()\n      promptWindow.on('closed', function() {\n        console.log({ promptResponse })\n        debugger\n        eventRet.returnValue = promptResponse\n        promptWindow = null\n      })\n\n    })\n    ipcMain.on('prompt-response', function(event, arg) {\n      if (arg === ''){ arg = null }\n      console.log(\"prompt-response\", { arg})\n      promptResponse = arg\n    })\n\n\n    updateSplashWindow({\n      state: 'loading',\n      message: 'Starting Pinokio…',\n      icon: getSplashIcon()\n    })\n    try {\n      await restoreSessionCookies()\n      await clearSessionCaches()\n      try {\n        const portInUse = await pinokiod.running(pinokiod.port)\n        if (portInUse) {\n          showStartupError({\n            message: 'Pinokio is already running',\n            detail: `Pinokio detected another instance listening on port ${pinokiod.port}. Please close the other instance before launching a new one.`\n          })\n          return\n        }\n      } catch (checkError) {\n        console.warn('Failed to verify pinokio port availability', checkError)\n      }\n      await pinokiod.start({\n        onquit: () => {\n          app.quit()\n        },\n        onrestart: () => {\n          persistSessionCookies().finally(() => {\n            app.relaunch()\n            app.exit()\n          })\n        },\n        onrefresh: (payload) => {\n          try {\n            updateThemeColors(payload || { theme: pinokiod.theme, colors: pinokiod.colors })\n          } catch (err) {\n            console.error('Failed to sync title bar theme', err)\n          }\n        },\n        browser: {\n          clearCache: async () => {\n            console.log('clear cache from all sessions')\n            \n            // Clear default session\n            await session.defaultSession.clearStorageData()\n            \n            // Clear all custom sessions from active windows\n            const windows = BrowserWindow.getAllWindows()\n            for (const window of windows) {\n              if (window.webContents && window.webContents.session) {\n                await window.webContents.session.clearStorageData()\n              }\n            }\n\n            await clearPersistedSessionCookies()\n\n            console.log(\"cleared all sessions\")\n          },\n          requestPermissions: async (payload = {}) => {\n            try {\n              const project = typeof payload.name === 'string' ? payload.name.trim() : ''\n              const permissions = normalizePermissionList(payload.permissions)\n              logPermission('callback received', { project, permissions })\n              if (!project || permissions.length === 0) {\n                logPermission('callback skipped (missing project or permissions)', { project, permissions })\n                return { ok: true, skipped: true }\n              }\n              const owner = BrowserWindow.getFocusedWindow() || mainWindow || BrowserWindow.getAllWindows()[0] || null\n              const webContents = owner && owner.webContents ? owner.webContents : null\n              if (!webContents || webContents.isDestroyed()) {\n                logPermission('callback failed (no webContents)', { project, permissions })\n                return { ok: false, error: 'no-webcontents' }\n              }\n              await promptForProjectPermissions(webContents, project, permissions)\n              return { ok: true }\n            } catch (err) {\n              console.error('[PERMISSION] Failed to prompt via callback', err)\n              return { ok: false, error: err && err.message ? err.message : String(err) }\n            }\n          }\n        }\n      })\n    } catch (error) {\n      console.error('Failed to start pinokiod', error)\n      showStartupError({ error })\n      return\n    }\n    closeSplashWindow()\n    PORT = pinokiod.port\n    app.on('web-contents-created', attach)\n    app.on('activate', function () {\n      if (BrowserWindow.getAllWindows().length === 0) createWindow(PORT)\n    })\n    app.on('before-quit', function(e) {\n      if (pinokiod.kernel.kill) {\n        if (isQuitting) {\n          return\n        }\n        e.preventDefault()\n        isQuitting = true\n        persistSessionCookies().finally(() => {\n          console.log('Cleaning up before quit', process.pid)\n          pinokiod.kernel.kill()\n        })\n      }\n    });\n    app.on('window-all-closed', function () {\n      console.log(\"window-all-closed\")\n      if (process.platform !== 'darwin') {\n        // Reset all shells before quitting\n        pinokiod.kernel.shell.reset()\n        // wait 1 second before quitting the app\n        // otherwise the app.quit() fails because the subprocesses are running\n        setTimeout(() => {\n          console.log(\"app.quit()\")\n          app.quit()\n        }, 1000)\n      }\n    })\n    app.on('browser-window-created', (event, win) => {\n      const parentWindow = (win && typeof win.getParentWindow === 'function') ? win.getParentWindow() : null\n      if (parentWindow && !parentWindow.isDestroyed()) installForceDestroyOnClose(win)\n      if (win.type !== \"splash\") {\n        if (win && typeof win.setTitleBarOverlay === 'function') {\n          const overlay = titleBarOverlay(colors)\n          setWindowTitleBarOverlay(win, overlay)\n        }\n      }\n    })\n    app.on('open-url', (event, url) => {\n      let u = url.replace(/pinokio:[\\/]+/, \"\")\n  //    let u = new URL(url).search\n  //    console.log(\"u\", u)\n      loadNewWindow(`${root_url}/pinokio/${u}`, PORT)\n\n//      if (BrowserWindow.getAllWindows().length === 0 || !mainWindow) createWindow(PORT)\n//      const topWindow = BrowserWindow.getFocusedWindow();\n//      console.log(\"top window\", topWindow)\n//      //mainWindow.focus()\n//      //mainWindow.loadURL(`${root_url}/pinokio/${u}`)\n//      topWindow.focus()\n//      topWindow.loadURL(`${root_url}/pinokio/${u}`)\n    })\n//    app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors')\n\n    let all = BrowserWindow.getAllWindows()\n    for(win of all) {\n      try {\n        if (win && typeof win.setTitleBarOverlay === 'function') {\n          const overlay = titleBarOverlay(colors)\n          setWindowTitleBarOverlay(win, overlay)\n        }\n      } catch (e) {\n  //      console.log(\"E2\", e)\n      }\n    }\n    createWindow(PORT)\n    if (updateTestMode) {\n      setTimeout(() => {\n        showUpdateBannerTestAvailable()\n      }, 400)\n    } else {\n      updater.setHandlers({\n        onUpdateAvailable: (info) => {\n          updateInfo = info\n          updateDownloadInFlight = false\n          updateBannerDismissed = false\n          showUpdateBanner(buildUpdateBannerPayload('available', info))\n        },\n        onUpdateNotAvailable: () => {\n          updateInfo = null\n          updateDownloadInFlight = false\n          hideUpdateBanner()\n        },\n        onDownloadProgress: (progress) => {\n          const payload = buildUpdateBannerPayload('downloading', updateInfo, {\n            progressPercent: progress && typeof progress.percent === 'number' ? progress.percent : 0,\n            notesPreview: buildProgressLabel(progress)\n          })\n          showUpdateBanner(payload)\n        },\n        onUpdateDownloaded: (info) => {\n          updateInfo = info\n          updateDownloadInFlight = false\n          showUpdateBanner(buildUpdateBannerPayload('ready', info))\n        },\n        onError: (err) => {\n          const wasDownloading = updateDownloadInFlight\n          updateDownloadInFlight = false\n          if (!wasDownloading) {\n            console.warn('Update check error:', err)\n            return\n          }\n          const message = err && err.message ? err.message : 'Update error'\n          showUpdateBanner(buildUpdateBannerPayload('error', updateInfo, { notesPreview: message }))\n        }\n      })\n      updater.run(mainWindow)\n    }\n  })\n\n}\n"
  },
  {
    "path": "linux_build.sh",
    "content": "docker run --rm -ti \\\n  -v \"$PWD:/project\" \\\n  -w /project \\\n  -e SNAPCRAFT_BUILD_ENVIRONMENT=host \\\n  -e SNAP_DESTRUCTIVE_MODE=true \\\n  electronuserland/builder \\\n  bash -lc \"rm -rf node_modules && npm install && npm run dist\"\n"
  },
  {
    "path": "main.js",
    "content": "const { app } = require('electron')\nconst Pinokiod = require(\"pinokiod\")\nconst config = require('./config')\nconst pinokiod = new Pinokiod(config)\n\nif (process.platform === 'linux') {\n  console.log('[PINOKIO DEBUG] Linux startup')\n  console.log('[PINOKIO DEBUG] ELECTRON_OZONE_PLATFORM_HINT:', process.env.ELECTRON_OZONE_PLATFORM_HINT || '<unset>')\n  console.log('[PINOKIO DEBUG] ELECTRON_DISABLE_GPU:', process.env.ELECTRON_DISABLE_GPU || '<unset>')\n  console.log('[PINOKIO DEBUG] DISPLAY:', process.env.DISPLAY || '<unset>')\n  console.log('[PINOKIO DEBUG] WAYLAND_DISPLAY:', process.env.WAYLAND_DISPLAY || '<unset>')\n  console.log('[PINOKIO DEBUG] argv:', process.argv.join(' '))\n  app.disableHardwareAcceleration()\n}\n\nlet mode = pinokiod.kernel.store.get(\"mode\") || \"full\"\n//iprocess.env.PINOKIO_MODE = process.env.PINOKIO_MODE || 'desktop';\nif (mode === 'minimal' || mode === 'background') {\n  require('./minimal');\n} else {\n  require('./full');\n}\n"
  },
  {
    "path": "minimal.js",
    "content": "const { app, Tray, Menu, shell, nativeImage, BrowserWindow, session, Notification } = require('electron');\nconst path = require('path')\nconst os = require('os')\nconst fs = require('fs')\nconst Pinokiod = require(\"pinokiod\")\nconst config = require('./config')\nconst Updater = require('./updater')\nconst pinokiod = new Pinokiod(config)\nconst updater = new Updater()\nlet tray\nlet hiddenWindow\nlet rootUrl\nlet splashWindow\nlet splashIcon\n\nconst getLogFileHint = () => {\n  try {\n    if (pinokiod && pinokiod.kernel && pinokiod.kernel.homedir) {\n      return path.resolve(pinokiod.kernel.homedir, \"logs\", \"stdout.txt\")\n    }\n  } catch (err) {\n  }\n  return path.resolve(os.homedir(), \".pinokio\", \"logs\", \"stdout.txt\")\n}\nconst ensureSplashWindow = () => {\n  if (splashWindow && !splashWindow.isDestroyed()) {\n    return splashWindow\n  }\n  splashWindow = new BrowserWindow({\n    width: 420,\n    height: 320,\n    frame: false,\n    resizable: false,\n    transparent: true,\n    show: false,\n    alwaysOnTop: true,\n    skipTaskbar: true,\n    fullscreenable: false,\n    webPreferences: {\n      backgroundThrottling: false\n    }\n  })\n  splashWindow.on('closed', () => {\n    splashWindow = null\n  })\n  return splashWindow\n}\nconst getSplashIcon = () => {\n  if (splashIcon) {\n    return splashIcon\n  }\n  const candidates = [\n    path.join('assets', 'icon.png'),\n    path.join('assets', 'icon_small@2x.png'),\n    path.join('assets', 'icon_small.png'),\n    'icon2.png'\n  ]\n  for (const relative of candidates) {\n    const absolute = path.join(__dirname, relative)\n    if (fs.existsSync(absolute)) {\n      splashIcon = relative.split(path.sep).join('/')\n      return splashIcon\n    }\n  }\n  splashIcon = path.join('assets', 'icon_small.png').split(path.sep).join('/')\n  return splashIcon\n}\nconst updateSplashWindow = ({ state = 'loading', message, detail, logPath, icon } = {}) => {\n  const win = ensureSplashWindow()\n  const query = { state }\n  if (message) {\n    query.message = message\n  }\n  if (detail) {\n    const trimmed = detail.length > 800 ? `${detail.slice(0, 800)}…` : detail\n    query.detail = trimmed\n  }\n  if (logPath) {\n    query.log = logPath\n  }\n  if (icon) {\n    query.icon = icon\n  }\n  win.loadFile(path.join(__dirname, 'splash.html'), { query }).finally(() => {\n    if (!win.isDestroyed()) {\n      win.show()\n    }\n  })\n}\nconst closeSplashWindow = () => {\n  if (splashWindow && !splashWindow.isDestroyed()) {\n    splashWindow.close()\n  }\n}\nconst showStartupError = ({ message, detail, error } = {}) => {\n  const formatted = detail || formatStartupError(error)\n  updateSplashWindow({\n    state: 'error',\n    message: message || 'Pinokio could not start',\n    detail: formatted,\n    logPath: getLogFileHint(),\n    icon: getSplashIcon()\n  })\n}\nconst formatStartupError = (error) => {\n  if (!error) return ''\n  if (error.stack) {\n    return `${error.message || 'Unknown error'}\\n\\n${error.stack}`\n  }\n  if (error.message) return error.message\n  if (typeof error === 'string') return error\n  try {\n    return JSON.stringify(error, null, 2)\n  } catch (err) {\n    return String(error)\n  }\n}\nconst gotTheLock = app.requestSingleInstanceLock()\n\nif (!gotTheLock) {\n  app.quit()\n}\n\napp.on('second-instance', () => {\n  if (rootUrl) {\n    shell.openExternal(rootUrl)\n  }\n})\n\napp.whenReady().then(async () => {\n  if (!gotTheLock) {\n    return\n  }\n  updateSplashWindow({\n    state: 'loading',\n    message: 'Starting Pinokio…',\n    icon: getSplashIcon()\n  })\n  try {\n    try {\n      const portInUse = await pinokiod.running(pinokiod.port)\n      if (portInUse) {\n        showStartupError({\n          message: 'Pinokio is already running',\n          detail: `An existing Pinokio instance is using port ${pinokiod.port}. Please close it before launching another.`\n        })\n        return\n      }\n    } catch (checkError) {\n      console.warn('Failed to verify pinokio port availability', checkError)\n    }\n    await pinokiod.start({\n      onquit: () => {\n        app.quit()\n      },\n      onrestart: () => {\n        app.relaunch();\n        app.exit()\n      },\n      browser: {\n        clearCache: async () => {\n          console.log('clear cache', session.defaultSession)\n          await session.defaultSession.clearStorageData()\n          console.log(\"cleared\")\n        }\n      }\n    })\n  } catch (error) {\n    console.error('Failed to start pinokiod', error)\n    showStartupError({ error })\n    return\n  }\n  let quitting = false\n  app.on('before-quit', (e) => {\n    if (quitting) {\n      return\n    }\n    if (pinokiod && pinokiod.kernel && typeof pinokiod.kernel.kill === 'function') {\n      quitting = true\n      e.preventDefault()\n      try {\n        pinokiod.kernel.kill()\n      } catch (err) {\n        console.warn('Failed to terminate pinokiod on quit', err)\n      }\n    }\n  })\n  closeSplashWindow()\n  rootUrl = `http://localhost:${pinokiod.port}`\n  if (process.platform === 'darwin') app.dock.hide();\n  const assetsRoot = app.isPackaged ? process.resourcesPath : __dirname\n  const iconPath = path.resolve(assetsRoot, \"assets/icon_small.png\")\n  let icon = nativeImage.createFromPath(iconPath)\n  icon = icon.resize({\n    height: 24,\n    width: 24 \n  });\n  console.log('Tray icon path:', iconPath, 'isEmpty:', icon.isEmpty()); // if true, image failed to load\n  tray = new Tray(icon)\n  const contextMenu = Menu.buildFromTemplate([\n    { label: 'Open in Browser', click: () => shell.openExternal(rootUrl) },\n    { label: 'Restart', click: () => { app.relaunch(); app.exit(); } },\n    { label: 'Quit', click: () => app.quit() }\n  ]);\n  tray.setToolTip('Pinokio');\n  tray.setContextMenu(contextMenu);\n  const showNotification = (options = {}) => {\n    try {\n      new Notification({\n        title: 'Pinokio',\n        body: 'Running in background',\n        ...options\n      }).show()\n    } catch (err) {\n      console.warn('Failed to show background notification', err)\n    }\n  }\n  const announceTray = () => {\n    const platformHandlers = {\n      darwin: () => {\n        try {\n          tray.setHighlightMode('always')\n          tray.setTitle('Pinokio running')\n          setTimeout(() => tray.setHighlightMode('selection'), 4000)\n          setTimeout(() => tray.popUpContextMenu(contextMenu), 150)\n        } catch (err) {\n          console.warn('Failed to signal tray/notification on macOS', err)\n        }\n        showNotification()\n      },\n      win32: () => {\n        try {\n          app.setAppUserModelId('Pinokio')\n        } catch (err) {\n          console.warn('Failed to set AppUserModelID', err)\n        }\n        showNotification({ icon: iconPath })\n      },\n      default: () => {\n        showNotification()\n      }\n    }\n    const handler = platformHandlers[process.platform] || platformHandlers.default\n    handler()\n  }\n  announceTray()\n  tray.on('click', () => {\n    tray.popUpContextMenu(contextMenu);\n  });\n  shell.openExternal(rootUrl);\n  hiddenWindow = new BrowserWindow({ show: false });\n\n  updater.run(hiddenWindow)\n});\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"Pinokio\",\n  \"private\": true,\n  \"version\": \"7.0.0\",\n  \"homepage\": \"https://pinokio.co\",\n  \"description\": \"pinokio\",\n  \"main\": \"main.js\",\n  \"email\": \"cocktailpeanuts@proton.me\",\n  \"author\": \"https://twitter.com/cocktailpeanut\",\n  \"scripts\": {\n    \"start\": \"electron .\",\n    \"test:update-banner\": \"node script/run-update-banner-test.js\",\n    \"pack\": \"./node_modules/.bin/electron-builder --dir\",\n    \"eject\": \"hdiutil info | grep '/dev/disk' | awk '{print $1}' | xargs -I {} hdiutil detach {}\",\n    \"l\": \"docker run --rm -ti -v $PWD:/project -w /project -e SNAPCRAFT_BUILD_ENVIRONMENT=host -e SNAP_DESTRUCTIVE_MODE=true electronuserland/builder bash -lc 'rm -rf node_modules && npm install && npm run monkeypatch && ./node_modules/.bin/electron-builder install-app-deps && ./node_modules/.bin/electron-builder -l'\",\n    \"mw\": \"rm -rf node_modules && npm install && npm run monkeypatch && ./node_modules/.bin/electron-builder install-app-deps && ./node_modules/.bin/electron-builder -mw && npm run zip\",\n    \"build2\": \"npm run l && npm run mw\",\n    \"dist\": \"npm run monkeypatch && ./node_modules/.bin/electron-builder install-app-deps && export SNAPCRAFT_BUILD_ENVIRONMENT=host && export SNAP_DESTRUCTIVE_MODE='true' && ./node_modules/.bin/electron-builder -l && npm run zip\",\n    \"dist2\": \"npm run monkeypatch && export USE_SYSTEM_FPM=true && ./node_modules/.bin/electron-builder install-app-deps && export SNAPCRAFT_BUILD_ENVIRONMENT=host && export SNAP_DESTRUCTIVE_MODE='true' && ./node_modules/.bin/electron-builder -mwl && npm run zip\",\n    \"zip\": \"node script/zip\",\n    \"monkeypatch\": \"cp temp/yarn.js node_modules/app-builder-lib/out/util/yarn.js && cp temp/rebuild.js node_modules/@electron/rebuild/lib/src/rebuild.js\",\n    \"postinstall2\": \"npm run monkeypatch && ./node_modules/.bin/electron-builder install-app-deps\",\n    \"fix\": \"brew install fpm\"\n  },\n  \"build\": {\n    \"appId\": \"computer.pinokio\",\n    \"afterPack\": \"after-pack.js\",\n    \"afterSign\": \"electron-builder-notarize\",\n    \"directories\": {\n      \"output\": \"dist-${platform}\"\n    },\n    \"publish\": [\n      {\n        \"provider\": \"github\",\n        \"owner\": \"pinokiocomputer\",\n        \"repo\": \"pinokio\"\n      }\n    ],\n    \"asarUnpack\": [\n      \"node_modules/go-get-folder-size/**/*\",\n      \"node_modules/7zip-bin/**/*\",\n      \"node_modules/sweetalert2/**/*\",\n      \"node_modules/@homebridge/**/*\",\n      \"node_modules/pinokiod/server/public/**/*\",\n      \"node_modules/pinokiod/server/scripts/**/*\",\n      \"node_modules/toasted-notifier/vendor/mac.noindex/**/*\"\n    ],\n    \"nsis\": {\n      \"include\": \"build/installer.nsh\"\n    },\n    \"extraResources\": [\n      \"./script/**\",\n      {\n        \"from\": \"assets/icon_small.png\",\n        \"to\": \"assets/icon_small.png\"\n      }\n    ],\n    \"protocols\": [\n      {\n        \"name\": \"pinokio\",\n        \"schemes\": [\n          \"pinokio\"\n        ]\n      }\n    ],\n    \"mac\": {\n      \"category\": \"utility\",\n      \"target\": [\n        {\n          \"target\": \"default\",\n          \"arch\": [\n            \"x64\",\n            \"arm64\"\n          ]\n        }\n      ],\n      \"hardenedRuntime\": true,\n      \"entitlements\": \"build/entitlements.mac.plist\",\n      \"entitlementsInherit\": \"build/entitlements.mac.inherit.plist\",\n      \"extendInfo\": {\n        \"NSMicrophoneUsageDescription\": \"Pinokio needs microphone access for apps that use audio.\",\n        \"NSCameraUsageDescription\": \"Pinokio needs camera access for apps that use video.\"\n      }\n    },\n    \"dmg\": {\n      \"backgroundColor\": \"#ffffff\",\n      \"icon\": null,\n      \"window\": {\n        \"width\": 520,\n        \"height\": 320\n      },\n      \"iconSize\": 120,\n      \"contents\": [\n        {\n          \"type\": \"file\",\n          \"x\": 160,\n          \"y\": 170\n        },\n        {\n          \"type\": \"link\",\n          \"name\": \"Applications\",\n          \"path\": \"/Applications\",\n          \"x\": 360,\n          \"y\": 170\n        }\n      ]\n    },\n    \"linux\": {\n      \"maintainer\": \"Cocktail Peanut <cocktailpeanuts@proton.me>\",\n      \"target\": [\n        {\n          \"target\": \"deb\",\n          \"arch\": [\n            \"x64\",\n            \"arm64\"\n          ]\n        },\n        {\n          \"target\": \"rpm\",\n          \"arch\": [\n            \"x64\",\n            \"arm64\"\n          ]\n        },\n        {\n          \"target\": \"AppImage\",\n          \"arch\": [\n            \"x64\",\n            \"arm64\"\n          ]\n        }\n      ]\n    },\n    \"win\": {\n      \"artifactName\": \"Pinokio.${ext}\",\n      \"signtoolOptions\": {\n        \"sign\": \"./build/sign.js\"\n      },\n      \"target\": [\n        {\n          \"target\": \"nsis\",\n          \"arch\": [\n            \"x64\"\n          ]\n        }\n      ]\n    }\n  },\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"electron-progressbar\": \"^2.2.1\",\n    \"electron-store\": \"^8.1.0\",\n    \"electron-updater\": \"^6.6.2\",\n    \"electron-window-state\": \"^5.0.3\",\n    \"pinokiod\": \"^7.0.0\"\n  },\n  \"devDependencies\": {\n    \"@electron/rebuild\": \"3.2.10\",\n    \"electron\": \"39.2.3\",\n    \"electron-builder\": \"26.2.0\",\n    \"electron-builder-notarize\": \"^1.5.2\"\n  }\n}\n"
  },
  {
    "path": "patch-linux-arm64-natives.js",
    "content": "const fs = require('fs')\nconst path = require('path')\n\nconst ARCH_BY_ENUM = {\n  0: 'ia32',\n  1: 'x64',\n  2: 'armv7l',\n  3: 'arm64',\n  4: 'universal'\n}\n\nconst ELF_MACHINE_AARCH64 = 183\n\nconst resolveArch = (context) => {\n  if (typeof context.arch === 'string') {\n    return context.arch.toLowerCase()\n  }\n\n  if (typeof context.arch === 'number') {\n    const mapped = ARCH_BY_ENUM[context.arch]\n    if (mapped) {\n      return mapped\n    }\n  }\n\n  if (typeof context.appOutDir === 'string' && /arm64/i.test(context.appOutDir)) {\n    return 'arm64'\n  }\n\n  return ''\n}\n\nconst ensureAarch64Elf = (filePath, label) => {\n  const data = fs.readFileSync(filePath)\n\n  if (data.length < 20) {\n    throw new Error(`[linux-arm64-native-fix] ${label} is too small to be an ELF binary: ${filePath}`)\n  }\n\n  if (!(data[0] === 0x7f && data[1] === 0x45 && data[2] === 0x4c && data[3] === 0x46)) {\n    throw new Error(`[linux-arm64-native-fix] ${label} is not an ELF binary: ${filePath}`)\n  }\n\n  const isLittleEndian = data[5] !== 2\n  const machine = isLittleEndian ? data.readUInt16LE(18) : data.readUInt16BE(18)\n\n  if (machine !== ELF_MACHINE_AARCH64) {\n    throw new Error(`[linux-arm64-native-fix] ${label} is not aarch64 (e_machine=${machine}): ${filePath}`)\n  }\n}\n\nconst copyWithValidation = (source, destination, label) => {\n  if (!fs.existsSync(source)) {\n    throw new Error(`[linux-arm64-native-fix] Missing ${label} source file: ${source}`)\n  }\n\n  ensureAarch64Elf(source, `${label} source`)\n\n  fs.mkdirSync(path.dirname(destination), { recursive: true })\n  fs.copyFileSync(source, destination)\n\n  ensureAarch64Elf(destination, `${label} destination`)\n  console.log(`[linux-arm64-native-fix] Patched ${label}: ${destination}`)\n}\n\nconst existingDirectories = (candidates) => candidates.filter((candidate) => fs.existsSync(candidate))\n\nmodule.exports = async (context) => {\n  if (context.electronPlatformName !== 'linux') {\n    return\n  }\n\n  const arch = resolveArch(context)\n  if (arch !== 'arm64') {\n    return\n  }\n\n  const unpackedRoot = path.join(context.appOutDir, 'resources', 'app.asar.unpacked')\n\n  const ptyBases = existingDirectories([\n    path.join(unpackedRoot, 'node_modules', '@homebridge', 'node-pty-prebuilt-multiarch'),\n    path.join(unpackedRoot, 'node_modules', 'pinokiod', 'node_modules', '@homebridge', 'node-pty-prebuilt-multiarch')\n  ])\n\n  if (ptyBases.length === 0) {\n    throw new Error('[linux-arm64-native-fix] Could not find @homebridge/node-pty-prebuilt-multiarch in app.asar.unpacked')\n  }\n\n  for (const ptyBase of ptyBases) {\n    const source = path.join(ptyBase, 'prebuilds', 'linux-arm64', 'node.abi131.node')\n    const destination = path.join(ptyBase, 'build', 'Release', 'pty.node')\n    copyWithValidation(source, destination, 'node-pty')\n  }\n\n  const watcherBases = existingDirectories([\n    path.join(unpackedRoot, 'node_modules', '@parcel', 'watcher'),\n    path.join(unpackedRoot, 'node_modules', 'pinokiod', 'node_modules', '@parcel', 'watcher')\n  ])\n\n  if (watcherBases.length === 0) {\n    throw new Error('[linux-arm64-native-fix] Could not find @parcel/watcher in app.asar.unpacked')\n  }\n\n  for (const watcherBase of watcherBases) {\n    const source = path.join(path.dirname(watcherBase), 'watcher-linux-arm64-glibc', 'watcher.node')\n    const destination = path.join(watcherBase, 'build', 'Release', 'watcher.node')\n    copyWithValidation(source, destination, 'parcel-watcher')\n  }\n}\n"
  },
  {
    "path": "popup-shell.js",
    "content": "const path = require('path')\nconst windowStateKeeper = require('electron-window-state')\nconst { BrowserWindow, WebContentsView, session } = require('electron')\n\nconst parseUrl = (value, base) => {\n  try {\n    return new URL(value, base)\n  } catch (_) {\n    return null\n  }\n}\n\nconst isHttpUrl = (value) => {\n  return Boolean(value && (value.protocol === 'http:' || value.protocol === 'https:'))\n}\n\nconst getFeatureDimension = (params, key, fallback) => {\n  const value = parseInt(params.get(key), 10)\n  return Number.isFinite(value) ? value : fallback\n}\n\nmodule.exports = ({\n  contentPreloadPath = path.join(__dirname, 'preload.js'),\n  toolbarHtmlPath = path.join(__dirname, 'popup-toolbar.html'),\n  toolbarHeight = 46,\n  installForceDestroyOnClose\n} = {}) => {\n  let openPinokioHomeWindow = null\n  const popupBrowserPartition = 'persist:pinokio-popup-browser'\n  const buildBrowserLikeUserAgent = () => {\n    const chromeVersion = process.versions.chrome || '140.0.0.0'\n    if (process.platform === 'darwin') {\n      return `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`\n    }\n    if (process.platform === 'win32') {\n      const arch = process.arch === 'arm64' ? 'ARM64' : 'x64'\n      return `Mozilla/5.0 (Windows NT 10.0; Win64; ${arch}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`\n    }\n    const arch = process.arch === 'arm64' ? 'aarch64' : 'x86_64'\n    return `Mozilla/5.0 (X11; Linux ${arch}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`\n  }\n  const browserLikeUserAgent = buildBrowserLikeUserAgent()\n  const getPopupBrowserSession = () => {\n    const popupSession = session.fromPartition(popupBrowserPartition)\n    if (!popupSession.__pinokioPopupBrowserConfigured) {\n      popupSession.__pinokioPopupBrowserConfigured = true\n      popupSession.setUserAgent(browserLikeUserAgent, 'en-US,en')\n    }\n    return popupSession\n  }\n  const buildAppPopupContentWebPreferences = (overrides = {}) => {\n    const next = (overrides && typeof overrides === 'object') ? { ...overrides } : {}\n    return {\n      ...next,\n      session: session.defaultSession,\n      webSecurity: false,\n      spellcheck: false,\n      nativeWindowOpen: true,\n      contextIsolation: false,\n      nodeIntegrationInSubFrames: true,\n      enableRemoteModule: false,\n      experimentalFeatures: true,\n      preload: contentPreloadPath\n    }\n  }\n  const buildBrowserPopupContentWebPreferences = (overrides = {}) => {\n    const next = (overrides && typeof overrides === 'object') ? { ...overrides } : {}\n    delete next.session\n    delete next.preload\n    delete next.partition\n    delete next.nodeIntegration\n    delete next.nodeIntegrationInSubFrames\n    delete next.contextIsolation\n    delete next.experimentalFeatures\n    delete next.webSecurity\n    return {\n      ...next,\n      partition: popupBrowserPartition,\n      sandbox: true,\n      webSecurity: true,\n      allowRunningInsecureContent: false,\n      nativeWindowOpen: true,\n      contextIsolation: true,\n      nodeIntegration: false,\n      nodeIntegrationInSubFrames: false,\n      enableRemoteModule: false,\n      experimentalFeatures: false\n    }\n  }\n\n  const unwrapContainerTarget = (target, rootParsed) => {\n    let next = target\n    while (next && next.pathname === '/container') {\n      const innerUrl = next.searchParams.get('url')\n      if (!innerUrl) {\n        break\n      }\n      const unwrapped = parseUrl(innerUrl, rootParsed ? rootParsed.origin : undefined)\n      if (!isHttpUrl(unwrapped) || unwrapped.href === next.href) {\n        break\n      }\n      next = unwrapped\n    }\n    return next\n  }\n\n  const isPinokioWindowUrl = (value, rootUrl) => {\n    const rootParsed = parseUrl(rootUrl)\n    const target = unwrapContainerTarget(\n      parseUrl(value, rootParsed ? rootParsed.origin : undefined),\n      rootParsed\n    )\n    if (!rootParsed || !isHttpUrl(target)) {\n      return false\n    }\n    return target.origin === rootParsed.origin\n  }\n\n  const resolveTargetUrl = ({ url, openerWebContents, rootUrl } = {}) => {\n    const openerUrl = (() => {\n      try {\n        return openerWebContents && !openerWebContents.isDestroyed()\n          ? openerWebContents.getURL()\n          : (rootUrl || '')\n      } catch (_) {\n        return rootUrl || ''\n      }\n    })()\n    const target = parseUrl(url, openerUrl || (rootUrl || undefined))\n    return isHttpUrl(target) ? target.href : ''\n  }\n\n  const buildRegularWindowOptions = ({ x, y, width, height, overlay } = {}) => {\n    const options = {\n      x,\n      y,\n      width: width || 1000,\n      height: height || 800,\n      minWidth: 190,\n      parent: null,\n      titleBarStyle: 'hidden',\n      webPreferences: buildAppPopupContentWebPreferences()\n    }\n    if (overlay) {\n      options.titleBarOverlay = overlay\n    }\n    return options\n  }\n\n  const createRegularWindow = ({ x, y, width, height, overlay } = {}) => {\n    const win = new BrowserWindow(buildRegularWindowOptions({ x, y, width, height, overlay }))\n    installForceDestroyOnClose(win)\n    return win\n  }\n\n  const layoutPopupShell = (shellState) => {\n    if (!shellState || !shellState.win || shellState.win.isDestroyed()) {\n      return\n    }\n    const bounds = shellState.win.getContentBounds()\n    const width = Math.max(bounds.width || 0, 0)\n    const height = Math.max(bounds.height || 0, 0)\n    shellState.toolbarView.setBounds({\n      x: 0,\n      y: 0,\n      width,\n      height: toolbarHeight\n    })\n    shellState.contentView.setBounds({\n      x: 0,\n      y: toolbarHeight,\n      width,\n      height: Math.max(height - toolbarHeight, 0)\n    })\n  }\n\n  const buildPopupShellState = (shellState) => {\n    const target = shellState && shellState.contentView ? shellState.contentView.webContents : null\n    let url = ''\n    let title = ''\n    try {\n      if (target && !target.isDestroyed()) {\n        url = target.getURL() || ''\n        title = target.getTitle() || ''\n      }\n    } catch (_) {\n    }\n    return {\n      url,\n      title: title || url || 'Pinokio',\n      canGoBack: Boolean(target && !target.isDestroyed() && target.canGoBack()),\n      canGoForward: Boolean(target && !target.isDestroyed() && target.canGoForward())\n    }\n  }\n\n  const sendPopupShellState = (shellState) => {\n    if (!shellState || !shellState.toolbarView || !shellState.contentView) {\n      return\n    }\n    const toolbarContents = shellState.toolbarView.webContents\n    if (!toolbarContents || toolbarContents.isDestroyed()) {\n      return\n    }\n    const state = buildPopupShellState(shellState)\n    toolbarContents.send('pinokio:popup-shell-state', state)\n    if (shellState.win && !shellState.win.isDestroyed()) {\n      shellState.win.setTitle(state.title)\n    }\n  }\n\n  const createPopupShellWindow = ({\n    x,\n    y,\n    width,\n    height,\n    adoptedWebContents = null,\n    contentWebPreferences = {},\n    browserLike = false,\n    initialUrl = ''\n  } = {}) => {\n    const win = new BrowserWindow({\n      frame: true,\n      x,\n      y,\n      width: width || 1000,\n      height: height || 800,\n      minWidth: 190,\n      backgroundColor: '#ffffff'\n    })\n    win.__pinokioPopupShell = true\n    win.__pinokioCloseOnFirstDownload = Boolean(browserLike && initialUrl)\n    installForceDestroyOnClose(win)\n\n    const toolbarView = new WebContentsView({\n      webPreferences: {\n        nodeIntegration: true,\n        contextIsolation: false,\n        spellcheck: false,\n        backgroundThrottling: false\n      }\n    })\n    const contentView = adoptedWebContents\n      ? new WebContentsView({ webContents: adoptedWebContents })\n      : new WebContentsView({\n          webPreferences: browserLike\n            ? buildBrowserPopupContentWebPreferences(contentWebPreferences)\n            : buildAppPopupContentWebPreferences(contentWebPreferences)\n        })\n\n    const shellState = {\n      win,\n      toolbarView,\n      contentView\n    }\n\n    win.contentView.addChildView(contentView)\n    win.contentView.addChildView(toolbarView)\n    layoutPopupShell(shellState)\n\n    const syncShellState = () => {\n      layoutPopupShell(shellState)\n      sendPopupShellState(shellState)\n    }\n    const focusContent = () => {\n      if (contentView.webContents && !contentView.webContents.isDestroyed()) {\n        contentView.webContents.focus()\n      }\n    }\n\n    toolbarView.webContents.on('did-finish-load', () => {\n      sendPopupShellState(shellState)\n    })\n    toolbarView.webContents.on('ipc-message', (_event, channel) => {\n      const target = contentView.webContents\n      if (!target || target.isDestroyed()) {\n        return\n      }\n      if (channel === 'pinokio:popup-shell-back') {\n        if (target.canGoBack()) {\n          target.goBack()\n        }\n        return\n      }\n      if (channel === 'pinokio:popup-shell-forward') {\n        if (target.canGoForward()) {\n          target.goForward()\n        }\n        return\n      }\n      if (channel === 'pinokio:popup-shell-refresh') {\n        target.reload()\n        return\n      }\n      if (channel === 'pinokio:popup-shell-open-home') {\n        if (typeof openPinokioHomeWindow === 'function') {\n          openPinokioHomeWindow()\n        }\n      }\n    })\n    if (browserLike && contentView.webContents && !contentView.webContents.isDestroyed()) {\n      getPopupBrowserSession()\n      contentView.webContents.setUserAgent(browserLikeUserAgent)\n    }\n    contentView.webContents.on('did-finish-load', () => {\n      if (shellState.win && !shellState.win.isDestroyed()) {\n        shellState.win.__pinokioCloseOnFirstDownload = false\n      }\n      syncShellState()\n      focusContent()\n    })\n    contentView.webContents.on('did-navigate', syncShellState)\n    contentView.webContents.on('did-navigate-in-page', syncShellState)\n    contentView.webContents.on('page-title-updated', (event) => {\n      event.preventDefault()\n      sendPopupShellState(shellState)\n    })\n    win.on('focus', focusContent)\n    win.on('resize', syncShellState)\n\n    toolbarView.webContents.loadFile(toolbarHtmlPath).catch((error) => {\n      console.error('[pinokio][popup-shell] failed to load toolbar', error)\n    })\n    if (initialUrl) {\n      contentView.webContents.loadURL(initialUrl).catch((error) => {\n        console.error('[pinokio][popup-shell] failed to load content url', { initialUrl, error })\n      })\n    }\n    return shellState\n  }\n\n  const allowPermissions = (targetSession) => {\n    if (!targetSession) {\n      return\n    }\n    targetSession.setPermissionRequestHandler((_webContents, _permission, callback) => {\n      callback(true)\n    })\n  }\n\n  const createPopupWindowState = () => {\n    if (typeof windowStateKeeper !== 'function') {\n      return {\n        x: undefined,\n        y: undefined,\n        width: 1000,\n        height: 800,\n        manage: () => {}\n      }\n    }\n    return windowStateKeeper({\n//    file: \"index.json\",\n      defaultWidth: 1000,\n      defaultHeight: 800\n    })\n  }\n\n  const createPopupResponse = ({ params, width, height, x, y } = {}) => {\n    return {\n      action: 'allow',\n      outlivesOpener: true,\n      overrideBrowserWindowOptions: {\n        webPreferences: buildBrowserPopupContentWebPreferences()\n      },\n      createWindow: (options = {}) => {\n        const shellState = createPopupShellWindow({\n          width: getFeatureDimension(params, 'width', width),\n          height: getFeatureDimension(params, 'height', height),\n          x: x + 30,\n          y: y + 30,\n          adoptedWebContents: options.webContents || null,\n          contentWebPreferences: options.webPreferences || {},\n          browserLike: true\n        })\n        return shellState.contentView.webContents\n      }\n    }\n  }\n\n  const openExternalWindow = ({ url, windowState } = {}) => {\n    const nextWindowState = windowState || createPopupWindowState()\n    const shellState = createPopupShellWindow({\n      x: nextWindowState.x,\n      y: nextWindowState.y,\n      width: nextWindowState.width,\n      height: nextWindowState.height,\n      browserLike: true,\n      initialUrl: url\n    })\n    const win = shellState.win\n    allowPermissions(shellState.contentView.webContents.session)\n    win.focus()\n    nextWindowState.manage(win)\n    return win\n  }\n\n  return {\n    createPopupResponse,\n    getPopupBrowserSession,\n    isPinokioWindowUrl,\n    resolveTargetUrl,\n    openExternalWindow,\n    setPinokioHomeWindowOpener: (nextOpener) => {\n      openPinokioHomeWindow = typeof nextOpener === 'function' ? nextOpener : null\n    }\n  }\n}\n"
  },
  {
    "path": "popup-toolbar.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta\n      http-equiv=\"Content-Security-Policy\"\n      content=\"default-src 'self' 'unsafe-inline'; img-src 'self' data:;\"\n    />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <title>Popup Toolbar</title>\n    <style>\n      :root {\n        color-scheme: light;\n        font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n      }\n      html, body {\n        margin: 0;\n        padding: 0;\n        background: #f8fafc;\n        color: #0f172a;\n        overflow: hidden;\n      }\n      body {\n        height: 100vh;\n        border-bottom: 1px solid rgba(15, 23, 42, 0.08);\n      }\n      .popup-toolbar {\n        height: 100%;\n        display: flex;\n        align-items: center;\n        gap: 10px;\n        padding: 8px 12px;\n        box-sizing: border-box;\n      }\n      .popup-toolbar__group {\n        display: flex;\n        align-items: center;\n        gap: 8px;\n        flex: 0 0 auto;\n      }\n      .popup-toolbar__logo {\n        width: 18px;\n        height: 18px;\n        display: block;\n      }\n      .popup-toolbar__url {\n        flex: 1 1 auto;\n        min-width: 0;\n        padding: 7px 10px;\n        border-radius: 8px;\n        background: #ffffff;\n        border: 1px solid rgba(15, 23, 42, 0.12);\n        font-size: 12px;\n        line-height: 1.3;\n        overflow: hidden;\n        white-space: nowrap;\n        text-overflow: ellipsis;\n        user-select: text;\n      }\n      .popup-toolbar__button {\n        flex: 0 0 auto;\n        border: 0;\n        border-radius: 6px;\n        padding: 6px 8px;\n        background: transparent;\n        color: #0f172a;\n        cursor: pointer;\n        font: inherit;\n        font-size: 12px;\n        font-weight: 500;\n      }\n      .popup-toolbar__button:hover {\n        background: rgba(15, 23, 42, 0.06);\n      }\n      .popup-toolbar__button--icon {\n        display: inline-flex;\n        align-items: center;\n        justify-content: center;\n        min-width: 30px;\n        padding: 6px;\n      }\n      .popup-toolbar__button:disabled {\n        cursor: default;\n        opacity: 0.55;\n      }\n    </style>\n  </head>\n  <body>\n    <div class=\"popup-toolbar\">\n      <div class=\"popup-toolbar__group\">\n        <button class=\"popup-toolbar__button popup-toolbar__button--icon\" id=\"open-home\" type=\"button\" title=\"Open Pinokio Home\">\n          <img class=\"popup-toolbar__logo\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAIKADAAQAAAABAAAAIAAAAABfvA/wAAAACXBIWXMAAAsTAAALEwEAmpwYAAACyGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj43MjwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+NzI8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj42NDwvZXhpZjpQaXhlbFhEaW1lbnNpb24+CiAgICAgICAgIDxleGlmOkNvbG9yU3BhY2U+MTwvZXhpZjpDb2xvclNwYWNlPgogICAgICAgICA8ZXhpZjpQaXhlbFlEaW1lbnNpb24+NjQ8L2V4aWY6UGl4ZWxZRGltZW5zaW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4Kiv76YwAABgZJREFUWAmtV21I1VcYP/dmojNlihDOLF2ZOVEaZmVlmZLULEJbkVYQNAQHMjYYUxesj/s4kMYYNDJoflgTpGBzqB8cK8zKUVhZrRfNd2umWOh9Ofv9Hv/n31/tXnX0wLnnf57zvD/Pec65LrVwcIHUjcHZj6ExCAbPtRMvm2/jh0qXLEIQacmzIKD1gcB45iNBWVlZ7JMnT7bevn07q6enJwmoSItxbNWqVY9SU1PbV69e/dfp06efWfgQzF7re9GTCbU6cOBAclpaWg0kDGAwzHr79u26tLRUl5SU6JycHMFZe/0ZGRnfged9rAnGienVAn/t8G3evPlr8Exg6JMnT+qWlhZfd3e3Z3x83Ds1NSVjbGzMi8h4mpubfdXV1caYcfB+5dBny3Tg3vgphHv27IlKT0v/jYorKip0V1eXR2vtxwgKfsCdO3c85eXlYgii0bBv3753LE3zGiH1cOzYsYjk5OQ2Kq+rq5vyeDy2Yp/Pp6FDhrHErLlnANHxnzt3booyIOvP48ePh1lGiA7re84kFqanp/9CxosXL04agU7hBsfZKOdswElbX18/SVmQed7SFjAKcsy2bNlSToYzZ85MORUY4f9nNpHYmp19wjJCdFnfMklYUNGxWPWxul+9eiXxdHpmlBvc6Oiovnv3rr5//75++PChdno+MTEhe8TfunXLl52dzZrohuxoS/GMVPC8qg0bNnyJSV+5coUFN0OgUe7Eg85UvC4sLKTRNhmK1t7DkdQXLlzwUDbq4TPqAohOfjAnXnC6rl27dujw4cNq/fr1kie3O2C6yKfMPpqQWrZsmXK5XjvFvfDwcJWYmKieP3+uEAEXClEhWoeE2dGgRMvRo0dTsZEBT1RYWJgLBll0gSejMDo6WoWEhCgnD9KkEBHBDw4OUqZ7586dFPbhwYMHUyypYrGEAs0lA8jQlJQUXiZuCjMKLGJ7Mp6jstXQ0JDQERcaGmrTrF27VqFByR6NiYqKcqEnUHY48EmYuzIzM0OuX7/uEQP6+vriyR0TEyMGBFJOGgPwip6ZpcwMd3t7u1q6dKlasmS62OkMDURBCg265on8/PwOdM5BIKbzDOu/iYuL0729vVKAptLBHBRIx2FOACreLj4In/MNo3ixEd+LyysZ83Q1Io9eHCsFQcQtGEykzLxmzRrV398v3sPyGXK4hgFuRGBy48aN74HuUxB8LiGA98MsmhcvXkhhzGaeISnIgqFm+FmUnM3gminBWiPafq4BrxsSqp8lqhsbG6WnmpAGjb9j09DPkwL/8uXLTVqGcMV/QCvElJUrV/6N7/62tra4goICDU8kEiRYCNBzQnx8vLp06ZIUHaPIlFrR1DU1Na6mpqYRKP4+IiLi/NWrV++BRRglFGCuBWLRhTg5OalR/TKQQv3y5UsZbMcGhoeHPbGxsRqp/oGGWiDKp00HBuf0J240NDQIzrLcop07mX0eL/YE3PsqLy9P4XyrTZs2qaSkJHXvHp1Uqra21j0yMsI9Oqlyc3MZeR55G0TpunXrfgVG37x5U46j18su/WYwRzVY3p8+farxQJF3AUL/s6XtdfHZ6q1c7N69OxG4Zzt27NDocqLdFNibzdAanU3jocpXk9yAvCE7Ozv148eP9YMHD7yUBZmDe/fulWaHbzvq+J4BUpDo2R8Bq4uKijTOqhhhmo3xOpAxTjzy7i0uLpaq37VrV76lSXTM0DprIQTbtm0rpRG4G/Tly5f5OLGfPMYYRsY5jHLg/K2trRJ2yPAj3x9bOuZVbmwRQvTrHCAeYciLuKOjw4MKZ0RojG2QWXPvxo0bnqqqKvEa3fEfyMieT3mg804jvHzBIKfVEPwJ1u/u379f8VpNSEjgDSeyeevhNlV4siu8I4n7Nysr60cU3bdnz54dxVpkCfEif+xqxUMlwXox/QEZ/IPCxyb/9XDwm7jf0eO/OHLkyAp8G7BlGMTsOVAEDB33WbX2LVVZWRk9MDCwAs1HQhAZGTmGp1bPqVOn6K0BKuY5ZzreCtAQhjKYwQuhmWNMMIFziC0EeWbzSeEFYgiG/w8u2Su57ZNoZAAAAABJRU5ErkJggg==\" alt=\"Pinokio\" />\n        </button>\n        <button class=\"popup-toolbar__button popup-toolbar__button--icon\" id=\"go-back\" type=\"button\" title=\"Back\" disabled>&larr;</button>\n        <button class=\"popup-toolbar__button popup-toolbar__button--icon\" id=\"go-forward\" type=\"button\" title=\"Forward\" disabled>&rarr;</button>\n        <button class=\"popup-toolbar__button popup-toolbar__button--icon\" id=\"refresh\" type=\"button\" title=\"Refresh\">&#8635;</button>\n      </div>\n      <div class=\"popup-toolbar__url\" id=\"popup-url\" title=\"Loading...\">Loading...</div>\n      <button class=\"popup-toolbar__button\" id=\"open-browser\" type=\"button\" disabled>Open in Browser</button>\n    </div>\n    <script>\n      const { ipcRenderer, shell } = require('electron')\n\n      const state = { url: '', canGoBack: false, canGoForward: false }\n      const urlNode = document.getElementById('popup-url')\n      const homeButton = document.getElementById('open-home')\n      const backButton = document.getElementById('go-back')\n      const forwardButton = document.getElementById('go-forward')\n      const refreshButton = document.getElementById('refresh')\n      const openButton = document.getElementById('open-browser')\n\n      const render = () => {\n        const value = state.url || 'Loading...'\n        urlNode.textContent = value\n        urlNode.title = value\n        backButton.disabled = !state.canGoBack\n        forwardButton.disabled = !state.canGoForward\n        openButton.disabled = !state.url\n      }\n\n      ipcRenderer.on('pinokio:popup-shell-state', (_event, payload = {}) => {\n        state.url = typeof payload.url === 'string' ? payload.url : ''\n        state.canGoBack = Boolean(payload.canGoBack)\n        state.canGoForward = Boolean(payload.canGoForward)\n        render()\n      })\n\n      homeButton.addEventListener('click', () => {\n        ipcRenderer.send('pinokio:popup-shell-open-home')\n      })\n      backButton.addEventListener('click', () => {\n        ipcRenderer.send('pinokio:popup-shell-back')\n      })\n      forwardButton.addEventListener('click', () => {\n        ipcRenderer.send('pinokio:popup-shell-forward')\n      })\n      refreshButton.addEventListener('click', () => {\n        ipcRenderer.send('pinokio:popup-shell-refresh')\n      })\n      openButton.addEventListener('click', () => {\n        if (!state.url) {\n          return\n        }\n        shell.openExternal(state.url).catch(() => {})\n      })\n\n      render()\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "preload.js",
    "content": "// put this preload for main-window to give it prompt()\nconst { ipcRenderer, } = require('electron')\nwindow.prompt = function(title, val){\n  return ipcRenderer.sendSync('prompt', {title, val})\n}\ntry {\n} catch (_) {\n}\nconst sendPinokio = (action) => {\n  if (!action) {\n    return\n  }\n  try {\n    if (window.parent === window.top) {\n      window.parent.postMessage({ action }, \"*\")\n    }\n  } catch (_) {\n  }\n}\n\n// Only apply frame bridge hooks inside embedded pages.\nlet isEmbeddedFrame = false\nlet isDirectChildFrame = false\ntry {\n  isEmbeddedFrame = window.parent !== window\n  isDirectChildFrame = isEmbeddedFrame && window.parent === window.top\n} catch (_) {\n  isEmbeddedFrame = false\n  isDirectChildFrame = false\n}\nlet previousFrameUrl = isEmbeddedFrame ? document.location.href : ''\nconst publishFrameLocation = () => {\n  if (!isEmbeddedFrame) {\n    return\n  }\n  const currentUrl = document.location.href\n  if (currentUrl === previousFrameUrl) {\n    return\n  }\n  previousFrameUrl = currentUrl\n  if (isDirectChildFrame) {\n    sendPinokio({\n      type: 'location',\n      url: currentUrl\n    })\n  }\n  syncPinokioInjectors('location').catch(() => {})\n}\nif (isEmbeddedFrame) {\n  if (isDirectChildFrame) {\n    sendPinokio({\n      type: 'location',\n      url: previousFrameUrl\n    })\n  }\n  const originalPushState = history.pushState\n  history.pushState = function pushStateWithPinokioLocation(...args) {\n    const result = originalPushState.apply(this, args)\n    publishFrameLocation()\n    return result\n  }\n  const originalReplaceState = history.replaceState\n  history.replaceState = function replaceStateWithPinokioLocation(...args) {\n    const result = originalReplaceState.apply(this, args)\n    publishFrameLocation()\n    return result\n  }\n  window.addEventListener('popstate', publishFrameLocation)\n  window.addEventListener('hashchange', publishFrameLocation)\n  window.addEventListener('beforeunload', () => {\n    resetPinokioInjectors('unload').catch(() => {})\n  }, { once: true })\n  window.addEventListener('message', (event) => {\n    if (event.data) {\n      if (event.data.action === 'back') {\n        history.back()\n      } else if (event.data.action === 'forward') {\n        history.forward()\n      } else if (event.data.action === 'refresh') {\n        location.reload()\n      }\n    }\n  })\n}\n\n\n//document.addEventListener(\"DOMContentLoaded\", (e) => {\n//  if (window.parent === window.top) {\n//    window.parent.postMessage({\n//      action: {\n//        type: \"title\",\n//        text: document.title\n//      }\n//    }, \"*\")\n//  }\n//})\nwindow.electronAPI = {\n  send: (type, msg) => {\n    ipcRenderer.send(type, msg)\n  },\n  sendSync: (type, msg) => ipcRenderer.sendSync(type, msg),\n  requestPermissions: (payload) => ipcRenderer.invoke('pinokio:request-permissions', payload || {}),\n  startInspector: (payload) => ipcRenderer.invoke('pinokio:start-inspector', payload || {}),\n  stopInspector: () => ipcRenderer.invoke('pinokio:stop-inspector'),\n  captureScreenshot: (screenshotRequest) => {\n    return ipcRenderer.invoke('pinokio:capture-screenshot-debug', { screenshotRequest })\n  }\n}\nconst resolvePinokioTargetWindow = () => {\n  try {\n    if (window.parent && window.parent !== window) {\n      return window.parent\n    }\n  } catch (_) {\n  }\n  try {\n    if (window.top && window.top !== window) {\n      return window.top\n    }\n  } catch (_) {\n  }\n  return window\n}\nconst postPinokioEvent = (eventName, payload = {}, context = {}) => {\n  const target = resolvePinokioTargetWindow()\n  const nextContext = (context && typeof context === 'object') ? { ...context } : {}\n  if (!nextContext.frameUrl) {\n    nextContext.frameUrl = window.location.href\n  }\n  if (!nextContext.workspace) {\n    const workspaceHint = resolvePinokioWorkspaceHint()\n    if (workspaceHint) {\n      nextContext.workspace = workspaceHint\n    }\n  }\n  target.postMessage({\n    e: 'pinokio:event',\n    event: eventName,\n    payload: (payload && typeof payload === 'object') ? payload : {},\n    context: nextContext\n  }, '*')\n}\nconst ensurePinokioApi = () => {\n  const api = (window.$pinokio && typeof window.$pinokio === 'object')\n    ? window.$pinokio\n    : {}\n  api.trigger = (eventName, payload = {}, context = {}) => {\n    if (typeof eventName !== 'string' || !eventName.trim()) {\n      return { ok: false, handled: false, reason: 'invalid_event_name' }\n    }\n    const normalizedEvent = eventName.trim()\n    postPinokioEvent(\n      normalizedEvent,\n      (payload && typeof payload === 'object') ? payload : {},\n      (context && typeof context === 'object') ? context : {}\n    )\n    return { ok: true, handled: true, event: normalizedEvent }\n  }\n  window.$pinokio = api\n  return api\n}\nensurePinokioApi()\nconst extractWorkspaceFromPathname = (pathname) => {\n  if (typeof pathname !== 'string') {\n    return ''\n  }\n  const value = pathname.trim()\n  if (!value) {\n    return ''\n  }\n  const patterns = [\n    /^\\/pinokio\\/([^/?#]+)/i,\n    /^\\/p\\/([^/?#]+)/i,\n    /^\\/api\\/([^/?#]+)/i,\n    /^\\/_api\\/([^/?#]+)/i,\n    /^\\/raw\\/api\\/([^/?#]+)/i,\n    /^\\/asset\\/api\\/([^/?#]+)/i,\n    /^\\/files\\/api\\/([^/?#]+)/i,\n    /^\\/env\\/api\\/([^/?#]+)/i,\n    /^\\/run\\/api\\/([^/?#]+)/i,\n  ]\n  for (const pattern of patterns) {\n    const match = value.match(pattern)\n    if (!match || !match[1]) {\n      continue\n    }\n    try {\n      return decodeURIComponent(match[1]).trim()\n    } catch (_) {\n      return String(match[1] || '').trim()\n    }\n  }\n  return ''\n}\nconst resolvePinokioWorkspaceHint = () => {\n  const candidates = []\n  try {\n    const ref = (typeof document !== 'undefined' && document.referrer) ? document.referrer : ''\n    if (ref) {\n      candidates.push(ref)\n    }\n  } catch (_) {\n  }\n  try {\n    candidates.push(window.location.href)\n  } catch (_) {\n  }\n  for (const candidate of candidates) {\n    if (typeof candidate !== 'string' || !candidate.trim()) {\n      continue\n    }\n    try {\n      const parsed = new URL(candidate, 'http://localhost')\n      const workspaceQueryHint = (parsed.searchParams.get('__pinokio_workspace') || parsed.searchParams.get('workspace') || '').trim()\n      if (workspaceQueryHint) {\n        return workspaceQueryHint\n      }\n      const workspace = extractWorkspaceFromPathname(parsed.pathname || '')\n      if (workspace) {\n        return workspace\n      }\n    } catch (_) {\n      const workspace = extractWorkspaceFromPathname(candidate)\n      if (workspace) {\n        return workspace\n      }\n    }\n  }\n  return ''\n}\nlet pinokioInjectSyncId = 0\n\nconst buildPinokioContext = (reason = 'load', responseContext = {}) => {\n  const currentUrl = window.location.href\n  const referrerUrl = (typeof document !== 'undefined' && document.referrer) ? document.referrer : ''\n  const workspaceHint = resolvePinokioWorkspaceHint()\n  const frameName = typeof window.name === 'string' ? window.name.trim() : ''\n  const rootFrameUrl = responseContext && typeof responseContext.frameUrl === 'string' && responseContext.frameUrl.trim()\n    ? responseContext.frameUrl.trim()\n    : currentUrl\n  return {\n    frameUrl: currentUrl,\n    rootFrameUrl,\n    currentUrl,\n    pageUrl: referrerUrl || rootFrameUrl,\n    referrerUrl,\n    frameName: frameName || undefined,\n    workspace: workspaceHint || undefined,\n    reason\n  }\n}\n\nconst waitForPinokioDocumentEnd = () => {\n  if (document.readyState === 'loading') {\n    return new Promise((resolve) => {\n      window.addEventListener('DOMContentLoaded', resolve, { once: true })\n    })\n  }\n  return Promise.resolve()\n}\n\nconst waitForPinokioDocumentIdle = async () => {\n  await waitForPinokioDocumentEnd()\n  await new Promise((resolve) => {\n    if (typeof window.requestIdleCallback === 'function') {\n      window.requestIdleCallback(() => resolve(), { timeout: 120 })\n      return\n    }\n    setTimeout(resolve, 32)\n  })\n}\n\nconst requestPinokioInjectDescriptors = (reason = 'load') => {\n  if (!isEmbeddedFrame) {\n    return Promise.resolve(null)\n  }\n  const context = buildPinokioContext(reason)\n  return ipcRenderer.invoke('pinokio:resolve-injectors', {\n    reason,\n    context\n  }).then((result) => result && typeof result === 'object' ? result : null).catch(() => null)\n}\n\nconst resetPinokioInjectors = async (reason = 'sync', syncId = pinokioInjectSyncId) => {\n  if (!isEmbeddedFrame) {\n    return\n  }\n  try {\n    await ipcRenderer.invoke('pinokio:reset-injectors', {\n      syncId,\n      reason,\n      context: buildPinokioContext(reason)\n    })\n  } catch (error) {\n    try {\n      console.warn('[pinokio][preload] injector reset failed', {\n        reason,\n        error: error && error.message ? error.message : String(error)\n      })\n    } catch (_) {\n    }\n  }\n}\n\nconst mountPinokioInjectGroup = async (syncId, descriptors, responseContext, reason) => {\n  if (!descriptors.length || syncId !== pinokioInjectSyncId) {\n    return\n  }\n  try {\n    await ipcRenderer.invoke('pinokio:mount-injectors', {\n      syncId,\n      reason,\n      inject: descriptors,\n      context: buildPinokioContext(reason, responseContext)\n    })\n  } catch (error) {\n    try {\n      console.warn('[pinokio][preload] injector mount failed', {\n        reason,\n        descriptors: descriptors.map((item) => item && item.src).filter(Boolean),\n        error: error && error.message ? error.message : String(error)\n      })\n    } catch (_) {\n    }\n  }\n}\n\nasync function syncPinokioInjectors(reason = 'load') {\n  if (!isEmbeddedFrame) {\n    return\n  }\n  const syncId = ++pinokioInjectSyncId\n  const response = await requestPinokioInjectDescriptors(reason)\n  if (syncId !== pinokioInjectSyncId) {\n    return\n  }\n  const descriptors = Array.isArray(response && response.inject) ? response.inject : []\n  const groups = {\n    start: [],\n    end: [],\n    idle: []\n  }\n  for (const descriptor of descriptors) {\n    const when = descriptor && (descriptor.when === 'start' || descriptor.when === 'end')\n      ? descriptor.when\n      : 'idle'\n    groups[when].push(descriptor)\n  }\n  await resetPinokioInjectors(reason, syncId)\n  if (syncId !== pinokioInjectSyncId) {\n    return\n  }\n  await mountPinokioInjectGroup(syncId, groups.start, response && response.context, reason)\n  await waitForPinokioDocumentEnd()\n  await mountPinokioInjectGroup(syncId, groups.end, response && response.context, reason)\n  await waitForPinokioDocumentIdle()\n  await mountPinokioInjectGroup(syncId, groups.idle, response && response.context, reason)\n}\n\nif (isEmbeddedFrame) {\n  syncPinokioInjectors('load').catch(() => {})\n}\n\n;(function initUpdateBanner() {\n  if (typeof document === 'undefined') {\n    return\n  }\n  if (window !== window.top) {\n    return\n  }\n\n  const BANNER_HEIGHT = 72\n  const state = {\n    payload: null,\n    banner: null,\n    style: null,\n    layoutActive: false,\n    layoutTick: null,\n    ready: false\n  }\n\n  const ensureStyle = () => {\n    if (state.style) {\n      return\n    }\n    const style = document.createElement('style')\n    style.id = 'pinokio-update-banner-style'\n    style.textContent = `\n      body.pinokio-update-banner-active[data-pinokio-update-layout-root=\"1\"] #layout-root {\n        height: calc(100% - var(--layout-dragger-height, 0px) - var(--pinokio-update-banner-height, 0px));\n      }\n      body.pinokio-update-banner-active:not([data-pinokio-update-layout-root=\"1\"]) {\n        padding-bottom: var(--pinokio-update-banner-height, 0px) !important;\n      }\n      #pinokio-update-banner {\n        position: fixed;\n        bottom: 0;\n        left: 0;\n        right: 0;\n        height: var(--pinokio-update-banner-height, 72px);\n        display: none;\n        align-items: center;\n        justify-content: space-between;\n        gap: 16px;\n        padding: 10px 16px 12px;\n        box-sizing: border-box;\n        background: linear-gradient(90deg, rgba(24, 24, 30, 0.94), rgba(30, 30, 38, 0.98));\n        border-top: 1px solid rgba(255, 255, 255, 0.12);\n        box-shadow: 0 -12px 26px rgba(0, 0, 0, 0.35);\n        z-index: 2147483646;\n        color: #f5f5f7;\n        font-family: \"SF Pro Text\", \"Segoe UI\", \"Helvetica Neue\", Arial, sans-serif;\n        border-radius: 0;\n      }\n      #pinokio-update-banner .pinokio-update-left {\n        min-width: 0;\n        display: flex;\n        flex-direction: column;\n        gap: 4px;\n      }\n      #pinokio-update-banner .pinokio-update-title {\n        font-size: 15px;\n        font-weight: 600;\n        letter-spacing: 0.1px;\n      }\n      #pinokio-update-banner .pinokio-update-title.danger {\n        color: #ff7b72;\n      }\n      #pinokio-update-banner .pinokio-update-details {\n        font-size: 12px;\n        color: rgba(255, 255, 255, 0.68);\n        white-space: nowrap;\n        overflow: hidden;\n        text-overflow: ellipsis;\n        max-width: 520px;\n      }\n      #pinokio-update-banner .pinokio-update-actions {\n        display: flex;\n        align-items: center;\n        gap: 8px;\n        flex-shrink: 0;\n      }\n      #pinokio-update-banner button {\n        appearance: none;\n        border: 1px solid transparent;\n        border-radius: 0;\n        padding: 6px 12px;\n        font-size: 12px;\n        font-weight: 600;\n        cursor: pointer;\n        transition: background 120ms ease, border-color 120ms ease, transform 120ms ease;\n      }\n      #pinokio-update-banner button:disabled {\n        opacity: 0.6;\n        cursor: default;\n      }\n      #pinokio-update-banner .pinokio-update-primary {\n        color: #1b1b1f;\n        background: #f6c251;\n      }\n      #pinokio-update-banner .pinokio-update-primary:hover:not(:disabled) {\n        background: #ffcc66;\n        transform: translateY(-1px);\n      }\n      #pinokio-update-banner .pinokio-update-ghost {\n        color: #f5f5f7;\n        background: transparent;\n        border-color: rgba(255, 255, 255, 0.18);\n      }\n      #pinokio-update-banner .pinokio-update-ghost:hover:not(:disabled) {\n        border-color: rgba(255, 255, 255, 0.35);\n      }\n      #pinokio-update-banner .pinokio-update-progress {\n        position: absolute;\n        left: 0;\n        top: 0;\n        width: 100%;\n        height: 3px;\n        background: rgba(255, 255, 255, 0.15);\n      }\n      #pinokio-update-banner .pinokio-update-progress-bar {\n        height: 100%;\n        width: 0%;\n        background: #f6c251;\n        transition: width 150ms ease;\n      }\n      #pinokio-update-banner .pinokio-update-hidden {\n        display: none !important;\n      }\n    `\n    const target = document.head || document.documentElement\n    target.appendChild(style)\n    state.style = style\n  }\n\n  const ensureBanner = () => {\n    if (state.banner) {\n      return state.banner\n    }\n    if (!document.body) {\n      return null\n    }\n    ensureStyle()\n    const container = document.createElement('div')\n    container.id = 'pinokio-update-banner'\n    container.innerHTML = `\n      <div class=\"pinokio-update-left\">\n        <div class=\"pinokio-update-title\">Update available</div>\n        <div class=\"pinokio-update-details\"></div>\n      </div>\n      <div class=\"pinokio-update-actions\">\n        <button class=\"pinokio-update-primary\" data-action=\"update\">Update now</button>\n        <button class=\"pinokio-update-primary pinokio-update-hidden\" data-action=\"restart\">Restart now</button>\n        <button class=\"pinokio-update-ghost pinokio-update-hidden\" data-action=\"release-notes\">Release notes</button>\n        <button class=\"pinokio-update-ghost\" data-action=\"dismiss\">Later</button>\n      </div>\n      <div class=\"pinokio-update-progress pinokio-update-hidden\">\n        <div class=\"pinokio-update-progress-bar\"></div>\n      </div>\n    `\n    container.addEventListener('click', (event) => {\n      const button = event.target.closest('button')\n      if (!button) {\n        return\n      }\n      const action = button.getAttribute('data-action')\n      if (!action) {\n        return\n      }\n      if (action === 'release-notes' && state.payload && state.payload.releaseUrl) {\n        ipcRenderer.send('pinokio:update-banner-action', { action, releaseUrl: state.payload.releaseUrl })\n        return\n      }\n      ipcRenderer.send('pinokio:update-banner-action', { action })\n    })\n    document.body.appendChild(container)\n    state.banner = container\n    return container\n  }\n\n  const notifyLayoutResize = () => {\n    if (state.layoutTick) {\n      cancelAnimationFrame(state.layoutTick)\n    }\n    state.layoutTick = requestAnimationFrame(() => {\n      state.layoutTick = null\n      try {\n        window.dispatchEvent(new CustomEvent('pinokio:viewport-change', {\n          detail: { height: window.innerHeight }\n        }))\n      } catch (_) {\n        window.dispatchEvent(new Event('resize'))\n      }\n    })\n  }\n\n  const applyLayoutOffset = (active) => {\n    if (!document.body) {\n      return\n    }\n    const hasLayoutRoot = Boolean(document.getElementById('layout-root'))\n    if (hasLayoutRoot) {\n      document.body.setAttribute('data-pinokio-update-layout-root', '1')\n    } else {\n      document.body.removeAttribute('data-pinokio-update-layout-root')\n    }\n    document.documentElement.style.setProperty('--pinokio-update-banner-height', `${BANNER_HEIGHT}px`)\n    document.body.classList.toggle('pinokio-update-banner-active', Boolean(active))\n    if (state.layoutActive !== Boolean(active)) {\n      state.layoutActive = Boolean(active)\n      notifyLayoutResize()\n    }\n  }\n\n  const setHidden = (node, hidden) => {\n    if (!node) return\n    node.classList.toggle('pinokio-update-hidden', Boolean(hidden))\n  }\n\n  const render = (payload) => {\n    state.payload = payload\n    if (!payload || payload.state === 'hidden') {\n      if (state.banner) {\n        state.banner.style.display = 'none'\n      }\n      applyLayoutOffset(false)\n      return\n    }\n    const banner = ensureBanner()\n    if (!banner) {\n      return\n    }\n    banner.style.display = 'flex'\n    applyLayoutOffset(true)\n\n    const title = banner.querySelector('.pinokio-update-title')\n    const details = banner.querySelector('.pinokio-update-details')\n    const updateNow = banner.querySelector('[data-action=\"update\"]')\n    const restartNow = banner.querySelector('[data-action=\"restart\"]')\n    const releaseNotes = banner.querySelector('[data-action=\"release-notes\"]')\n    const progress = banner.querySelector('.pinokio-update-progress')\n    const progressBar = banner.querySelector('.pinokio-update-progress-bar')\n\n    const stateKey = payload.state || 'available'\n    const version = payload.version ? `Version ${payload.version}` : ''\n    const notes = payload.notesPreview || ''\n    const detail = [version, notes].filter(Boolean).join(' - ')\n\n    let titleText = 'Update available'\n    if (stateKey === 'downloading') titleText = 'Downloading update'\n    if (stateKey === 'ready') titleText = 'Update ready'\n    if (stateKey === 'error') titleText = 'Update failed'\n\n    if (title) {\n      title.textContent = titleText\n      if (stateKey === 'error') {\n        title.classList.add('danger')\n      } else {\n        title.classList.remove('danger')\n      }\n    }\n    if (details) {\n      details.textContent = detail\n    }\n\n    if (updateNow) {\n      updateNow.textContent = stateKey === 'error' ? 'Retry' : 'Update now'\n      updateNow.disabled = stateKey === 'downloading'\n    }\n\n    setHidden(updateNow, stateKey === 'ready')\n    setHidden(restartNow, stateKey !== 'ready')\n    setHidden(progress, stateKey !== 'downloading')\n    setHidden(releaseNotes, !payload.releaseUrl)\n\n    if (progressBar) {\n      if (stateKey === 'downloading' && typeof payload.progressPercent === 'number') {\n        const percent = Math.max(0, Math.min(100, payload.progressPercent))\n        progressBar.style.width = `${percent}%`\n      } else {\n        progressBar.style.width = '0%'\n      }\n    }\n  }\n\n  ipcRenderer.on('pinokio:update-banner', (_event, payload) => {\n    render(payload)\n  })\n\n  const ready = () => {\n    state.ready = true\n    if (state.payload) {\n      render(state.payload)\n    }\n  }\n\n  if (document.readyState === 'loading') {\n    document.addEventListener('DOMContentLoaded', ready, { once: true })\n  } else {\n    ready()\n  }\n})()\n\n;(function initInspector() {\n  if (typeof document === 'undefined') {\n    return\n  }\n\n  const log = (message) => {\n    try {\n      console.log(`[Inspector] ${message}`)\n    } catch (_) {\n      // ignore\n    }\n  }\n\n  const state = {\n    active: false,\n    button: null,\n    lastFrameOrdinal: null,\n    lastRelativeOrdinal: null,\n    lastUrl: null,\n    lastDomPath: null,\n    displayUrl: null,\n    overlay: null,\n    instructionsVisible: false,\n    closing: false,\n  }\n\n  const normalizeUrl = (value) => {\n    if (!value) {\n      return null\n    }\n    try {\n      return new URL(value, window.location.href).toString()\n    } catch (err) {\n      return value\n    }\n  }\n\n  const findIframeCandidates = () => {\n    const list = Array.from(document.querySelectorAll('iframe')).map((iframe, index) => {\n      const style = window.getComputedStyle ? window.getComputedStyle(iframe) : null\n      const rect = iframe.getBoundingClientRect()\n      const area = rect ? Math.max(rect.width, 0) * Math.max(rect.height, 0) : 0\n      const visible = Boolean(\n        rect &&\n        rect.width > 2 &&\n        rect.height > 2 &&\n        !iframe.classList.contains('hidden') &&\n        !iframe.hasAttribute('hidden') &&\n        (!style || (style.display !== 'none' && style.visibility !== 'hidden' && Number(style.opacity || '1') > 0))\n      )\n      return {\n        element: iframe,\n        index,\n        rect,\n        area,\n        visible,\n        src: normalizeUrl(iframe.getAttribute('src') || iframe.src || ''),\n      }\n    })\n    return list\n  }\n\n  const selectVisibleIframe = () => {\n    const candidates = findIframeCandidates()\n    if (!candidates.length) {\n      log('no iframe candidates discovered')\n      return null\n    }\n    candidates.slice(0, 3).forEach((candidate) => {\n      const rect = candidate.rect || { width: 0, height: 0 }\n      log(`candidate[${candidate.index}] src=${candidate.src || '<empty>'} visible=${candidate.visible ? 'yes' : 'no'} size=${Math.round(rect.width)}x${Math.round(rect.height)}`)\n    })\n    const visible = candidates.filter((candidate) => candidate.visible)\n    const ranked = (visible.length ? visible : candidates).slice().sort((a, b) => {\n      if (b.area === a.area) {\n        return (b.rect ? b.rect.width : 0) - (a.rect ? a.rect.width : 0)\n      }\n      return b.area - a.area\n    })\n    const chosen = ranked[0]\n    if (!chosen) {\n      log('no suitable iframe found after ranking')\n      return null\n    }\n    log(`selected iframe src=${chosen.src || '<empty>'} visible=${chosen.visible ? 'yes' : 'no'} size=${Math.round(chosen.rect?.width || 0)}x${Math.round(chosen.rect?.height || 0)}`)\n    const siblingsWithSameUrl = candidates.filter((candidate) => candidate.src === chosen.src)\n    const relativeOrdinal = siblingsWithSameUrl.indexOf(chosen)\n    return {\n      element: chosen.element,\n      index: chosen.index,\n      relativeOrdinal: relativeOrdinal >= 0 ? relativeOrdinal : null,\n      url: chosen.src,\n    }\n  }\n\n  const ensureOverlay = () => {\n    if (state.overlay && state.overlay.container && document.body.contains(state.overlay.container)) {\n      return state.overlay\n    }\n\n    // Remove stale overlays left over from previous script executions\n    const orphaned = Array.from(document.querySelectorAll('.pinokio-inspector-overlay'))\n    for (const node of orphaned) {\n      if (!state.overlay || node !== state.overlay.container) {\n        node.remove()\n      }\n    }\n\n    const container = document.createElement('div')\n    container.className = 'pinokio-inspector-overlay'\n    Object.assign(container.style, {\n      position: 'fixed',\n      right: '16px',\n      bottom: '16px',\n      maxWidth: 'min(420px, 92vw)',\n      maxHeight: '70vh',\n      padding: '12px 14px',\n      borderRadius: '10px',\n      border: '1px solid rgba(255, 255, 255, 0.12)',\n      background: 'rgba(9, 12, 20, 0.92)',\n      color: '#fefefe',\n      boxShadow: '0 20px 42px rgba(0,0,0,0.45)',\n      display: 'none',\n      flexDirection: 'column',\n      gap: '8px',\n      zIndex: '2147483646',\n      fontFamily: \"-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif\",\n      fontSize: '12px',\n    })\n\n    const header = document.createElement('div')\n    Object.assign(header.style, {\n      display: 'flex',\n      alignItems: 'center',\n      justifyContent: 'space-between',\n      gap: '8px',\n    })\n\n    const title = document.createElement('strong')\n    title.textContent = 'Inspect Mode'\n    Object.assign(title.style, {\n      fontSize: '12px',\n      letterSpacing: '0.06em',\n      textTransform: 'uppercase',\n    })\n\n    const closeButton = document.createElement('button')\n    closeButton.type = 'button'\n    closeButton.textContent = '×'\n    closeButton.dataset.role = 'close'\n    Object.assign(closeButton.style, {\n      background: 'transparent',\n      border: 'none',\n      color: '#fefefe',\n      fontSize: '18px',\n      lineHeight: '1',\n      cursor: 'pointer',\n    })\n\n    header.append(title, closeButton)\n\n    const status = document.createElement('div')\n    status.dataset.role = 'status'\n    status.style.color = '#ccd5ff'\n\n    const urlRow = document.createElement('div')\n    urlRow.dataset.role = 'url'\n    Object.assign(urlRow.style, {\n      color: '#9aa7c2',\n      fontSize: '11px',\n      wordBreak: 'break-all',\n    })\n\n    const htmlSection = document.createElement('div')\n    htmlSection.dataset.role = 'html-container'\n    Object.assign(htmlSection.style, {\n      display: 'none',\n      margin: '8px 0',\n      padding: '8px',\n      borderRadius: '8px',\n      background: 'rgba(255,255,255,0.06)',\n      border: '1px solid rgba(255,255,255,0.12)',\n    })\n\n    const htmlHeader = document.createElement('div')\n    Object.assign(htmlHeader.style, {\n      display: 'flex',\n      alignItems: 'center',\n      justifyContent: 'space-between',\n      gap: '8px',\n      marginBottom: '6px',\n    })\n\n    const htmlLabel = document.createElement('div')\n    htmlLabel.textContent = 'Element Snippet'\n    Object.assign(htmlLabel.style, {\n      fontSize: '11px',\n      color: '#9aa7c2',\n      textTransform: 'uppercase',\n      letterSpacing: '0.06em',\n    })\n\n    const buttonBaseStyle = {\n      display: 'none',\n      background: 'rgba(77,163,255,0.2)',\n      border: '1px solid rgba(77,163,255,0.4)',\n      borderRadius: '6px',\n      padding: '4px 12px',\n      fontSize: '11px',\n      cursor: 'pointer',\n      color: '#ccd5ff',\n      fontWeight: '600',\n    }\n\n    const copyButton = document.createElement('button')\n    copyButton.dataset.role = 'copy'\n    copyButton.type = 'button'\n    copyButton.textContent = 'Copy snippet'\n    Object.assign(copyButton.style, buttonBaseStyle)\n\n    htmlHeader.append(htmlLabel, copyButton)\n\n    const htmlBlock = document.createElement('textarea')\n    htmlBlock.dataset.role = 'html'\n    Object.assign(htmlBlock.style, {\n      margin: '0',\n      padding: '10px',\n      maxHeight: '28vh',\n      overflow: 'auto',\n      borderRadius: '8px',\n      background: 'rgba(255,255,255,0.08)',\n      display: 'none',\n      fontFamily: \"'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace\",\n      fontSize: '11px',\n      border: '1px solid rgba(255,255,255,0.18)',\n      color: '#fefefe',\n      resize: 'vertical',\n      minHeight: '140px',\n      width: '100%',\n      boxSizing: 'border-box',\n    })\n    htmlBlock.spellcheck = false\n\n    htmlSection.append(htmlHeader, htmlBlock)\n\n    const screenshotBlock = document.createElement('div')\n    screenshotBlock.dataset.role = 'screenshot-container'\n    Object.assign(screenshotBlock.style, {\n      margin: '8px 0',\n      padding: '8px',\n      borderRadius: '8px',\n      background: 'rgba(255,255,255,0.06)',\n      border: '1px solid rgba(255,255,255,0.12)',\n      display: 'none',\n      textAlign: 'center',\n    })\n\n    const screenshotHeader = document.createElement('div')\n    Object.assign(screenshotHeader.style, {\n      display: 'flex',\n      alignItems: 'center',\n      justifyContent: 'space-between',\n      gap: '8px',\n      marginBottom: '8px',\n    })\n\n    const screenshotImg = document.createElement('img')\n    screenshotImg.dataset.role = 'screenshot'\n    Object.assign(screenshotImg.style, {\n      maxWidth: '100%',\n      maxHeight: '200px',\n      borderRadius: '4px',\n      boxShadow: '0 2px 8px rgba(0,0,0,0.3)',\n    })\n\n    const screenshotLabel = document.createElement('div')\n    screenshotLabel.textContent = 'Element Screenshot'\n    Object.assign(screenshotLabel.style, {\n      fontSize: '11px',\n      color: '#9aa7c2',\n      textTransform: 'uppercase',\n      letterSpacing: '0.06em',\n    })\n\n    const copyScreenshotButton = document.createElement('button')\n    copyScreenshotButton.dataset.role = 'copy-screenshot'\n    copyScreenshotButton.type = 'button'\n    copyScreenshotButton.textContent = 'Copy screenshot'\n    Object.assign(copyScreenshotButton.style, buttonBaseStyle)\n\n    screenshotHeader.append(screenshotLabel, copyScreenshotButton)\n    screenshotBlock.append(screenshotHeader, screenshotImg)\n\n    container.append(header, status, urlRow, htmlSection, screenshotBlock)\n    document.body.appendChild(container)\n\n    const overlay = {\n      container,\n      status,\n      urlRow,\n      htmlSection,\n      htmlBlock,\n      screenshotBlock,\n      screenshotImg,\n      copyButton,\n      copyScreenshotButton,\n      closeButton,\n    }\n\n    closeButton.addEventListener('click', () => {\n      stopInspector()\n    })\n\n    copyButton.addEventListener('click', async () => {\n      const text = overlay.htmlBlock.value || ''\n      if (!text) {\n        return\n      }\n      try {\n        if (navigator.clipboard && navigator.clipboard.writeText) {\n          await navigator.clipboard.writeText(text)\n        } else {\n          const textarea = document.createElement('textarea')\n          textarea.value = text\n          textarea.setAttribute('readonly', '')\n          textarea.style.position = 'absolute'\n          textarea.style.left = '-9999px'\n          document.body.appendChild(textarea)\n          textarea.select()\n          document.execCommand('copy')\n          document.body.removeChild(textarea)\n        }\n        overlay.copyButton.textContent = 'Copied'\n        setTimeout(() => {\n          overlay.copyButton.textContent = 'Copy snippet'\n        }, 1500)\n      } catch (err) {\n        overlay.copyButton.textContent = 'Copy failed'\n        setTimeout(() => {\n          overlay.copyButton.textContent = 'Copy snippet'\n        }, 1500)\n      }\n    })\n\n    copyScreenshotButton.addEventListener('click', async () => {\n      const img = overlay.screenshotImg\n      if (!img.src) {\n        return\n      }\n      try {\n        // Convert data URL to blob\n        const response = await fetch(img.src)\n        const blob = await response.blob()\n        \n        if (navigator.clipboard && navigator.clipboard.write) {\n          const clipboardItem = new ClipboardItem({ 'image/png': blob })\n          await navigator.clipboard.write([clipboardItem])\n        } else {\n          throw new Error('Clipboard API not available')\n        }\n        \n        overlay.copyScreenshotButton.textContent = 'Copied'\n        setTimeout(() => {\n          overlay.copyScreenshotButton.textContent = 'Copy screenshot'\n        }, 1500)\n      } catch (err) {\n        console.warn('Screenshot copy failed:', err)\n        overlay.copyScreenshotButton.textContent = 'Copy failed'\n        setTimeout(() => {\n          overlay.copyScreenshotButton.textContent = 'Copy screenshot'\n        }, 1500)\n      }\n    })\n\n    state.overlay = overlay\n    return overlay\n  }\n\n  const showOverlay = (message, frameUrl, html, screenshot) => {\n    const overlay = ensureOverlay()\n    if (overlay.container.parentNode) {\n      overlay.container.parentNode.appendChild(overlay.container)\n    }\n    for (const node of document.querySelectorAll('.pinokio-inspector-overlay')) {\n      if (node !== overlay.container) {\n        node.style.display = 'none'\n      }\n    }\n    overlay.container.style.display = 'flex'\n    overlay.status.textContent = message || ''\n    overlay.urlRow.textContent = frameUrl ? `Page: ${frameUrl}` : ''\n    state.displayUrl = frameUrl || null\n    \n    // Handle HTML content\n    if (html) {\n      const pageUrl = frameUrl || state.displayUrl || state.lastUrl || ''\n      const domPath = state.lastDomPath || ''\n      const lines = []\n      if (pageUrl) {\n        lines.push(`Page: ${pageUrl}`)\n      }\n      if (domPath) {\n        lines.push(`DOM: ${domPath}`)\n      }\n      lines.push(`HTML: ${html}`)\n      overlay.htmlSection.style.display = 'block'\n      overlay.htmlBlock.style.display = 'block'\n      overlay.htmlBlock.value = lines.join('\\n')\n      overlay.copyButton.style.display = 'inline-flex'\n      overlay.copyButton.textContent = 'Copy snippet'\n    } else {\n      overlay.htmlSection.style.display = 'none'\n      overlay.htmlBlock.style.display = 'none'\n      overlay.htmlBlock.value = ''\n      overlay.copyButton.style.display = 'none'\n    }\n    \n    // Handle screenshot content\n    if (screenshot) {\n      overlay.screenshotImg.src = screenshot\n      overlay.screenshotBlock.style.display = 'block'\n      overlay.copyScreenshotButton.style.display = 'inline-flex'\n      overlay.copyScreenshotButton.textContent = 'Copy screenshot'\n    } else {\n      overlay.screenshotImg.src = ''\n      overlay.screenshotBlock.style.display = 'none'\n      overlay.copyScreenshotButton.style.display = 'none'\n    }\n  }\n\n  const hideOverlay = () => {\n    const overlay = state.overlay\n    if (overlay && overlay.container) {\n      overlay.container.style.display = 'none'\n      overlay.status.textContent = ''\n      overlay.urlRow.textContent = ''\n      overlay.htmlBlock.value = ''\n      overlay.htmlSection.style.display = 'none'\n      overlay.htmlBlock.style.display = 'none'\n      overlay.copyButton.style.display = 'none'\n      overlay.screenshotImg.src = ''\n      overlay.screenshotBlock.style.display = 'none'\n      overlay.copyScreenshotButton.style.display = 'none'\n    }\n    state.instructionsVisible = false\n    state.closing = false\n    state.displayUrl = null\n  }\n\n  const startInspector = async (button) => {\n    if (state.active) {\n      log('inspector already active')\n      return\n    }\n    const target = selectVisibleIframe()\n    if (!target) {\n      showOverlay('No visible iframe found to inspect.', '', null)\n      log('startInspector aborted: no target iframe')\n      return\n    }\n\n    hideOverlay()\n\n    state.active = true\n    state.button = button || null\n    state.lastFrameOrdinal = target.index\n    state.lastRelativeOrdinal = target.relativeOrdinal\n    state.lastUrl = target.url\n    state.lastDomPath = null\n\n    if (state.button) {\n      state.button.classList.add('inspector-active')\n      state.button.setAttribute('aria-pressed', 'true')\n    }\n\n    showOverlay('Inspect mode enabled – hover items and click to capture.', target.url || '', null)\n\n    try {\n      await window.electronAPI.startInspector({\n        frameIndex: target.index,\n        frameUrl: target.url,\n        candidateOrdinal: target.index,\n        candidateRelativeOrdinal: target.relativeOrdinal,\n      })\n      log('startInspector IPC resolved')\n    } catch (error) {\n      const message = error && error.message ? error.message : 'Unable to start inspect mode.'\n      showOverlay(message, target.url || '', null)\n      log(`startInspector IPC error: ${message}`)\n      resetState()\n    }\n  }\n\n  const stopInspector = () => {\n    if (!state.active) {\n      hideOverlay()\n      return\n    }\n    window.electronAPI.stopInspector().catch(() => {})\n    resetState()\n    hideOverlay()\n  }\n\n  const resetState = () => {\n    state.active = false\n    state.lastFrameOrdinal = null\n    state.lastRelativeOrdinal = null\n    state.lastUrl = null\n    state.lastDomPath = null\n    if (state.button) {\n      state.button.classList.remove('inspector-active')\n      state.button.removeAttribute('aria-pressed')\n      state.button = null\n    }\n  }\n\n  const handleToggleClick = (event) => {\n    const button = event.target.closest('button')\n    if (!button) {\n      return\n    }\n    const isTrigger = (\n      button.id === 'inspector' ||\n      button.hasAttribute('data-pinokio-inspector') ||\n      button.classList.contains('pinokio-inspector-button') ||\n      (button.dataset && button.dataset.tippyContent === 'Switch to inspect mode')\n    )\n    if (!isTrigger) {\n      return\n    }\n    event.preventDefault()\n    event.stopPropagation()\n    if (state.active) {\n      stopInspector()\n    } else {\n      startInspector(button)\n    }\n  }\n\n  const handleInspectorMessage = (event) => {\n    const data = event && event.data && event.data.pinokioInspector\n    if (!data) {\n      return\n    }\n\n    const frameUrl = typeof data.frameUrl === 'string' ? data.frameUrl : state.lastUrl\n\n    if (data.type === 'started') {\n      showOverlay('Inspect mode enabled – hover items and click to capture.', frameUrl || '', null)\n      return\n    }\n\n    if (data.type === 'update') {\n      const label = data.nodeName ? `<${String(data.nodeName).toLowerCase()}>` : ''\n      if (Array.isArray(data.pathKeys) && data.pathKeys.length) {\n        state.lastDomPath = data.pathKeys.join(' > ')\n      }\n      showOverlay(label ? `Hovering ${label}` : 'Inspect mode enabled – hover items and click to capture.', frameUrl || '', null)\n      return\n    }\n\n    if (data.type === 'complete') {\n      const html = typeof data.outerHTML === 'string' ? data.outerHTML : ''\n      const screenshot = typeof data.screenshot === 'string' ? data.screenshot : null\n      if (Array.isArray(data.pathKeys) && data.pathKeys.length) {\n        state.lastDomPath = data.pathKeys.join(' > ')\n      }\n      showOverlay('Element captured. Inspect again or close.', frameUrl || '', html, screenshot)\n      state.closing = true\n      window.electronAPI.stopInspector().catch(() => {}).finally(() => {\n        state.closing = false\n      })\n      resetState()\n      return\n    }\n\n    if (data.type === 'cancelled') {\n      window.electronAPI.stopInspector().catch(() => {})\n      resetState()\n      hideOverlay()\n      return\n    }\n\n    if (data.type === 'error') {\n      const message = data.message || 'Failed to inspect element.'\n      if (Array.isArray(data.pathKeys) && data.pathKeys.length) {\n        state.lastDomPath = data.pathKeys.join(' > ')\n      }\n      showOverlay(message, frameUrl || '', null)\n      window.electronAPI.stopInspector().catch(() => {})\n      resetState()\n      return\n    }\n  }\n\n  ipcRenderer.on('pinokio:inspector-cancelled', () => {\n    if (state.closing) {\n      state.closing = false\n      return\n    }\n    resetState()\n    hideOverlay()\n  })\n\n  ipcRenderer.on('pinokio:inspector-error', (_event, payload) => {\n    const message = payload && payload.message ? payload.message : 'Inspect mode ended.'\n    hideOverlay()\n    showOverlay(message, payload && payload.frameUrl ? payload.frameUrl : '', null)\n    resetState()\n  })\n\n  ipcRenderer.on('pinokio:inspector-started', (_event, payload) => {\n    const url = payload && payload.frameUrl ? payload.frameUrl : state.lastUrl\n    showOverlay('Inspect mode enabled – hover items and click to capture.', url || '', null)\n  })\n\n  ipcRenderer.on('pinokio:capture-debug-log', (_event, payload) => {\n    try {\n      const serialized = JSON.stringify(payload)\n      console.log('[Pinokio Capture]', serialized)\n    } catch (error) {\n      console.log('[Pinokio Capture]', payload)\n    }\n  })\n\n  const logCaptureEvent = (label, payload) => {\n    try {\n      console.log('[Pinokio Capture]', JSON.stringify({ label, payload }))\n    } catch (error) {\n      console.log('[Pinokio Capture]', label)\n    }\n  }\n\n  const processScreenshotRequest = async (screenshotRequest, messageId, source) => {\n    logCaptureEvent('renderer-process-start', {\n      messageId,\n      relayStage: screenshotRequest && screenshotRequest.__pinokioRelayStage,\n      relayComplete: screenshotRequest && screenshotRequest.__pinokioRelayComplete,\n      adjustedFlag: screenshotRequest && screenshotRequest.__pinokioAdjusted,\n      bounds: screenshotRequest && screenshotRequest.bounds ? screenshotRequest.bounds : null\n    })\n    try {\n      const screenshot = await window.electronAPI.captureScreenshot(screenshotRequest)\n\n      source.postMessage({\n        pinokioScreenshotResponse: true,\n        messageId: messageId,\n        success: true,\n        screenshot: screenshot\n      }, '*')\n    } catch (error) {\n      console.error('Screenshot capture failed:', error)\n\n      source.postMessage({\n        pinokioScreenshotResponse: true,\n        messageId,\n        success: false,\n        error: error.message || 'Screenshot failed'\n      }, '*')\n    }\n  }\n\n  // Handle screenshot requests from iframes  \n  const handleScreenshotMessage = async (event) => {\n    if (event.data && event.data.pinokioScreenshotRequest) {\n      if (window !== window.top) {\n        logCaptureEvent('renderer-ignored-non-top', {\n          currentHref: window.location.href\n        })\n        return\n      }\n      const screenshotRequest = event.data.pinokioScreenshotRequest\n      const messageId = event.data.messageId\n      const source = event.source\n\n      logCaptureEvent('renderer-message-received', {\n        messageId,\n        relayStage: screenshotRequest.__pinokioRelayStage,\n        relayComplete: screenshotRequest.__pinokioRelayComplete,\n        adjustedFlag: screenshotRequest.__pinokioAdjusted,\n        bounds: screenshotRequest && screenshotRequest.bounds ? screenshotRequest.bounds : null\n      })\n      logCaptureEvent('renderer-skip-delegated', {\n        messageId\n      })\n      return\n    }\n  }\n\n  window.addEventListener('message', handleInspectorMessage)\n  window.addEventListener('message', handleScreenshotMessage)\n  window.addEventListener('message', (event) => {\n    if (!event || !event.data || event.source === window) {\n      return\n    }\n    if (event.data.e !== 'pinokio-start-inspector') {\n      return\n    }\n\n    try {\n      console.log('[Inspector] start-request ' + JSON.stringify({\n        url: event.data.frameUrl || null,\n        name: event.data.frameName || null,\n        nodeId: event.data.frameNodeId || null,\n        active: state.active,\n      }))\n    } catch (_) {}\n\n    const payload = {}\n    if (typeof event.data.frameUrl === 'string' && event.data.frameUrl.trim()) {\n      payload.frameUrl = event.data.frameUrl.trim()\n    }\n    if (typeof event.data.frameName === 'string' && event.data.frameName.trim()) {\n      payload.frameName = event.data.frameName.trim()\n    }\n    if (typeof event.data.frameNodeId === 'string' && event.data.frameNodeId.trim()) {\n      payload.frameNodeId = event.data.frameNodeId.trim()\n    }\n\n    if (!payload.frameUrl && !payload.frameName && !payload.frameNodeId) {\n      return\n    }\n\n    if (state.active) {\n      try {\n        console.log('[Inspector] stopping-current-before-start')\n      } catch (_) {}\n      stopInspector()\n    }\n\n    hideOverlay()\n\n    state.active = true\n    state.button = null\n    state.lastFrameOrdinal = null\n    state.lastRelativeOrdinal = null\n    state.lastUrl = payload.frameUrl || null\n    state.lastDomPath = null\n\n    showOverlay('Inspect mode enabled – hover items and click to capture.', payload.frameUrl || '', null)\n\n    window.electronAPI.startInspector(payload).then(() => {\n      try {\n        console.log('[Inspector] ipc-start-success ' + JSON.stringify(payload))\n      } catch (_) {}\n    }).catch((error) => {\n      const message = error && error.message ? error.message : 'Unable to start inspect mode.'\n      showOverlay(message, payload.frameUrl || '', null)\n      try {\n        console.log('[Inspector] ipc-start-error ' + JSON.stringify({ message }))\n      } catch (_) {}\n      resetState()\n    })\n  })\n  document.addEventListener('click', handleToggleClick, true)\n  window.addEventListener('keydown', (event) => {\n    if (event.key === 'Escape' && state.active) {\n      stopInspector()\n    }\n  })\n})()\n"
  },
  {
    "path": "prompt.html",
    "content": "<html>\n<head>\n<style>body {font-family: sans-serif;} button {float:right; margin-left: 10px;} label { display: block; margin-bottom: 5px; width: 100%; } input {margin-bottom: 10px; padding: 5px; width: 100%; display:block;}</style>\n<script>\ndocument.addEventListener(\"DOMContentLoaded\" () => {\n  document.querySelector(\"#cancel\").addEventListener(\"click\", (e) => {\n    debugger\n    e.preventDefault()\n    e.stopPropagation()\n    window.close()\n  })\n  document.querySelector(\"form\").addEventListener(\"submit\", (e) => {\n    e.preventDefault()\n    e.stopPropagation()\n    debugger\n    window.electronAPI.send('prompt-response', document.querySelector(\"#val\").value)\n    window.close()\n  })\n})\n</script>\n</head>\n<body>\n<form>\n  <label for=\"val\">${arg.title}</label>\n  <input id=\"val\" value=\"${arg.val}\" autofocus />\n  <button id='ok'>OK</button>\n  <button id='cancel'>Cancel</button>\n</form>\n</body>\n</html>\n"
  },
  {
    "path": "script/patch.command",
    "content": "sudo -s xattr -d com.apple.quarantine /Applications/Pinokio.app\n"
  },
  {
    "path": "script/run-update-banner-test.js",
    "content": "const { spawn } = require('child_process')\n\nconst cmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'\nconst child = spawn(cmd, ['run', 'start'], {\n  stdio: 'inherit',\n  env: {\n    ...process.env,\n    PINOKIO_TEST_UPDATE_BANNER: '1'\n  }\n})\n\nchild.on('exit', (code) => {\n  process.exit(code || 0)\n})\n"
  },
  {
    "path": "script/zip.js",
    "content": "const { exec } = require('child_process');\nconst path = require('path')\nconst fs = require('fs')\nconst version = process.env.npm_package_version\n\n// Windows\nlet exePath = path.resolve(__dirname, `../dist/Pinokio Setup ${version}.exe`)\nlet zipPath = path.resolve(__dirname, `../dist/Pinokio-${version}-win32.zip`)\nexec(`zip -j \"${zipPath}\" \"${exePath}\"`, (error, stdout, stderr) => {\n  if (error) {\n    console.error(`Error executing command: ${error}`);\n    return;\n  }\n\n  console.log('Command executed successfully.');\n  console.log('stdout:', stdout);\n  console.log('stderr:', stderr);\n});\n\n// Mac\n\n\n// find dmg files\nconst macPaths = [{\n  dmg: path.resolve(__dirname, `../dist/Pinokio-${version}-arm64.dmg`),\n  //temp: path.resolve(__dirname, `../dist/Pinokio-${version}-darwin-arm64-temp`),\n  temp: `Pinokio-${version}-darwin-arm64`,\n  //zip: path.resolve(__dirname, `../dist/Pinokio-${version}-darwin-arm64.zip`),\n  zip: `Pinokio-${version}-darwin-arm64.zip`\n}, {\n  dmg: path.resolve(__dirname, `../dist/Pinokio-${version}.dmg`),\n  //temp: path.resolve(__dirname, `../dist/Pinokio-${version}-darwin-intel-temp`),\n  temp: `Pinokio-${version}-darwin-intel`,\n  //zip: path.resolve(__dirname, `../dist/Pinokio-${version}-darwin-intel.zip`)\n  zip: `Pinokio-${version}-darwin-intel.zip`\n}]\nlet sentinelPath = path.resolve(__dirname, `../assets/Sentinel.app`)\nfor(let macPath of macPaths) {\n  const zipPath = macPath.zip\n  try {\n    console.log(\"mkdirSync\", path.resolve(__dirname, \"../dist\", macPath.temp))\n    fs.mkdirSync(path.resolve(__dirname, \"../dist\", macPath.temp), { recursive: true })\n  } catch (e) {\n    console.log(\"E1\", e)\n  }\n  try {\n    fs.cpSync(macPath.dmg, path.resolve(__dirname, \"../dist\", macPath.temp, \"install.dmg\"), { force: true, recursive: true })\n  } catch (e) {\n    console.log(\"E2\", e)\n  }\n  try {\n    fs.cpSync(sentinelPath, path.resolve(__dirname, \"../dist\", macPath.temp, \"Sentinel.app\"), { force: true, recursive: true })\n  } catch (e) {\n    console.log(\"E3\", e)\n  }\n  const cmd = `zip -r \"${zipPath}\" \"${macPath.temp}\"`\n  console.log({ cmd })\n  exec(cmd, { cwd: path.resolve(__dirname, \"../dist\") }, (error, stdout, stderr) => {\n    if (error) {\n      console.error(`Error executing command: ${error}`);\n      return;\n    }\n\n    console.log('Command executed successfully.');\n    console.log('stdout:', stdout);\n    console.log('stderr:', stderr);\n  });\n//  try {\n//    fs.rmSync(path.resolve(__dirname, \"../dist\", macPath.temp), { recursive: true })\n//  } catch (e) {\n//  }\n}\nlet rmFiles = [\n  `Pinokio-${version}-arm64-mac.zip`,\n  `Pinokio-${version}-mac.zip`,\n//  `Pinokio-${version}-darwin-arm64`,\n//  `Pinokio-${version}-darwin-intel`,\n]\nfor(let f of rmFiles) {\n  try {\n    fs.rmSync(path.resolve(__dirname, \"../dist\", f), { recursive: true })\n  } catch (e) {\n  }\n}\n"
  },
  {
    "path": "splash.html",
    "content": "<html>\n<head>\n<style>\nhtml, body {\n  width: 100%;\n  height: 100%;\n  margin: 0;\n  font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n  background: #ffffff;\n  color: #111827;\n}\nbody {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  overflow: hidden;\n}\n.card {\n  width: min(360px, 90vw);\n  padding: 32px;\n  text-align: center;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 8px;\n  max-height: calc(100vh - 40px);\n  overflow: hidden;\n}\n.logo {\n  width: 48px;\n  height: 48px;\n  margin: 0 auto 20px;\n}\n.title {\n  margin: 0 0 6px;\n  font-size: 16px;\n  font-weight: 600;\n}\n.subtitle {\n  margin: 0;\n  opacity: 0.85;\n  line-height: 1.4;\n  font-size: 13px;\n}\n.hint {\n  font-size: 12px;\n  opacity: 0.7;\n  margin: 16px 0 0;\n  word-break: break-word;\n}\n.loader {\n  width: 60px;\n  height: 2px;\n  margin: 24px auto;\n  background: rgba(17, 24, 39, 0.08);\n  overflow: hidden;\n  position: relative;\n}\n.loader::after {\n  content: \"\";\n  position: absolute;\n  top: 0;\n  left: -30%;\n  width: 30%;\n  height: 100%;\n  background: #111827;\n  animation: slide 1.2s ease-in-out infinite;\n}\n.detail {\n  padding: 14px 16px;\n  border-radius: 12px;\n  border: 1px solid rgba(17, 24, 39, 0.08);\n  font-size: 12px;\n  text-align: left;\n  white-space: pre-wrap;\n  margin-top: 16px;\n  max-height: 160px;\n  overflow-y: auto;\n  background: rgba(249, 250, 251, 0.8);\n  width: 100%;\n}\nbody.error .subtitle {\n  color: #b91c1c;\n}\nbody.error .loader {\n  display: none;\n}\n@keyframes slide {\n  0% {\n    left: -30%;\n  }\n  50% {\n    left: 100%;\n  }\n  100% {\n    left: 100%;\n  }\n}\n</style>\n</head>\n<body>\n<div class=\"card\">\n  <img class=\"logo\" id=\"logo\" src=\"assets/icon.png\" alt=\"Pinokio\" onerror=\"this.onerror=null;this.src='assets/icon_small.png'\">\n  <div class=\"text\">\n    <p class=\"title\" id=\"title\">Starting Pinokio…</p>\n    <p class=\"subtitle\" id=\"subtitle\">This should only take a moment.</p>\n  </div>\n  <div class=\"loader\" id=\"loader\"></div>\n  <pre class=\"detail\" id=\"detail\" hidden></pre>\n  <p class=\"hint\" id=\"hint\" hidden></p>\n</div>\n<script>\n  const params = new URLSearchParams(window.location.search)\n  const state = params.get('state') || 'loading'\n  const message = params.get('message') || 'Starting Pinokio…'\n  const detail = params.get('detail')\n  const logPath = params.get('log')\n  const icon = params.get('icon')\n\n  if (icon) {\n    const logo = document.getElementById('logo')\n    if (logo) {\n      logo.src = icon\n    }\n  }\n\n  document.getElementById('title').textContent = message\n  if (state === 'error') {\n    document.body.classList.add('error')\n    const friendly = detail && detail.trim().length > 0\n      ? detail\n      : 'Please review the Pinokio logs for more information.'\n    document.getElementById('loader').hidden = true\n    const detailEl = document.getElementById('detail')\n    detailEl.textContent = friendly\n    detailEl.hidden = false\n    const hintEl = document.getElementById('hint')\n    hintEl.textContent = logPath\n      ? `Logs: ${logPath}`\n      : 'Logs: ~/.pinokio/logs/stdout.txt'\n    hintEl.hidden = false\n    document.getElementById('subtitle').textContent = 'Something stopped Pinokio from starting.'\n  } else {\n    document.getElementById('subtitle').textContent = 'This should only take a moment.'\n  }\n</script>\n</body>\n</html>\n"
  },
  {
    "path": "temp/rebuild.js",
    "content": "\"use strict\";\nvar __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {\n    if (k2 === undefined) k2 = k;\n    var desc = Object.getOwnPropertyDescriptor(m, k);\n    if (!desc || (\"get\" in desc ? !m.__esModule : desc.writable || desc.configurable)) {\n      desc = { enumerable: true, get: function() { return m[k]; } };\n    }\n    Object.defineProperty(o, k2, desc);\n}) : (function(o, m, k, k2) {\n    if (k2 === undefined) k2 = k;\n    o[k2] = m[k];\n}));\nvar __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {\n    Object.defineProperty(o, \"default\", { enumerable: true, value: v });\n}) : function(o, v) {\n    o[\"default\"] = v;\n});\nvar __importStar = (this && this.__importStar) || function (mod) {\n    if (mod && mod.__esModule) return mod;\n    var result = {};\n    if (mod != null) for (var k in mod) if (k !== \"default\" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);\n    __setModuleDefault(result, mod);\n    return result;\n};\nvar __importDefault = (this && this.__importDefault) || function (mod) {\n    return (mod && mod.__esModule) ? mod : { \"default\": mod };\n};\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.rebuild = exports.Rebuilder = void 0;\nconst debug_1 = __importDefault(require(\"debug\"));\nconst events_1 = require(\"events\");\nconst fs = __importStar(require(\"fs-extra\"));\nconst nodeAbi = __importStar(require(\"node-abi\"));\nconst os = __importStar(require(\"os\"));\nconst path = __importStar(require(\"path\"));\nconst cache_1 = require(\"./cache\");\nconst types_1 = require(\"./types\");\nconst module_rebuilder_1 = require(\"./module-rebuilder\");\nconst module_walker_1 = require(\"./module-walker\");\nconst d = (0, debug_1.default)('electron-rebuild');\nconst defaultMode = 'sequential';\nconst defaultTypes = ['prod', 'optional'];\nclass Rebuilder {\n    constructor(options) {\n      console.log(\"options\", options)\n        var _a;\n        this.platform = options.platform || process.platform;\n        this.lifecycle = options.lifecycle;\n        this.buildPath = options.buildPath;\n        this.electronVersion = options.electronVersion;\n        this.arch = options.arch || process.arch;\n        this.force = options.force || false;\n        this.headerURL = options.headerURL || 'https://www.electronjs.org/headers';\n        this.mode = options.mode || defaultMode;\n        this.debug = options.debug || false;\n        this.useCache = options.useCache || false;\n        this.useElectronClang = options.useElectronClang || false;\n        this.cachePath = options.cachePath || path.resolve(os.homedir(), '.electron-rebuild-cache');\n        this.prebuildTagPrefix = options.prebuildTagPrefix || 'v';\n        this.msvsVersion = process.env.GYP_MSVS_VERSION;\n        this.disablePreGypCopy = options.disablePreGypCopy || false;\n        if (this.useCache && this.force) {\n            console.warn('[WARNING]: Electron Rebuild has force enabled and cache enabled, force take precedence and the cache will not be used.');\n            this.useCache = false;\n        }\n        if (typeof this.electronVersion === 'number') {\n            if (`${this.electronVersion}`.split('.').length === 1) {\n                this.electronVersion = `${this.electronVersion}.0.0`;\n            }\n            else {\n                this.electronVersion = `${this.electronVersion}.0`;\n            }\n        }\n        if (typeof this.electronVersion !== 'string') {\n            throw new Error(`Expected a string version for electron version, got a \"${typeof this.electronVersion}\"`);\n        }\n        this.ABIVersion = (_a = options.forceABI) === null || _a === void 0 ? void 0 : _a.toString();\n        const onlyModules = options.onlyModules || null;\n        const extraModules = (options.extraModules || []).reduce((acc, x) => acc.add(x), new Set());\n        const types = options.types || defaultTypes;\n        this.moduleWalker = new module_walker_1.ModuleWalker(this.buildPath, options.projectRootPath, types, extraModules, onlyModules);\n        this.rebuilds = [];\n        d('rebuilding with args:', this.buildPath, this.electronVersion, this.arch, extraModules, this.force, this.headerURL, types, this.debug);\n        console.log(\"THIS>PLATFORM\", this.platform)\n    }\n    get ABI() {\n        if (this.ABIVersion === undefined) {\n            this.ABIVersion = nodeAbi.getAbi(this.electronVersion, 'electron');\n        }\n        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n        return this.ABIVersion;\n    }\n    get buildType() {\n        return this.debug ? types_1.BuildType.Debug : types_1.BuildType.Release;\n    }\n    async rebuild() {\n        if (!path.isAbsolute(this.buildPath)) {\n            throw new Error('Expected buildPath to be an absolute path');\n        }\n        this.lifecycle.emit('start');\n        await this.moduleWalker.walkModules();\n        for (const nodeModulesPath of await this.moduleWalker.nodeModulesPaths) {\n            await this.moduleWalker.findAllModulesIn(nodeModulesPath);\n        }\n        for (const modulePath of this.moduleWalker.modulesToRebuild) {\n            this.rebuilds.push(() => this.rebuildModuleAt(modulePath));\n        }\n        this.rebuilds.push(() => this.rebuildModuleAt(this.buildPath));\n        if (this.mode !== 'sequential') {\n            await Promise.all(this.rebuilds.map(fn => fn()));\n        }\n        else {\n            for (const rebuildFn of this.rebuilds) {\n                await rebuildFn();\n            }\n        }\n    }\n    async rebuildModuleAt(modulePath) {\n        if (!(await fs.pathExists(path.resolve(modulePath, 'binding.gyp')))) {\n            return;\n        }\n        const moduleRebuilder = new module_rebuilder_1.ModuleRebuilder(this, modulePath);\n        this.lifecycle.emit('module-found', path.basename(modulePath));\n        if (!this.force && await moduleRebuilder.alreadyBuiltByRebuild()) {\n            d(`skipping: ${path.basename(modulePath)} as it is already built`);\n            this.lifecycle.emit('module-done');\n            this.lifecycle.emit('module-skip');\n            return;\n        }\n        if (await moduleRebuilder.prebuildInstallNativeModuleExists()) {\n            d(`skipping: ${path.basename(modulePath)} as it was prebuilt`);\n            return;\n        }\n        let cacheKey;\n        if (this.useCache) {\n            cacheKey = await (0, cache_1.generateCacheKey)({\n                ABI: this.ABI,\n                arch: this.arch,\n                debug: this.debug,\n                electronVersion: this.electronVersion,\n                headerURL: this.headerURL,\n                modulePath,\n            });\n            const applyDiffFn = await (0, cache_1.lookupModuleState)(this.cachePath, cacheKey);\n            if (typeof applyDiffFn === 'function') {\n                await applyDiffFn(modulePath);\n                this.lifecycle.emit('module-done');\n                return;\n            }\n        }\n        if (await moduleRebuilder.rebuild(cacheKey)) {\n            this.lifecycle.emit('module-done');\n        }\n    }\n}\nexports.Rebuilder = Rebuilder;\nfunction rebuild(options) {\n  console.log(\"(rebuild)\", options)\n    // eslint-disable-next-line prefer-rest-params\n    d('rebuilding with args:', arguments);\n    const lifecycle = new events_1.EventEmitter();\n    const rebuilderOptions = { ...options, lifecycle };\n    const rebuilder = new Rebuilder(rebuilderOptions);\n    const ret = rebuilder.rebuild();\n    ret.lifecycle = lifecycle;\n    return ret;\n}\nexports.rebuild = rebuild;\n//# sourceMappingURL=rebuild.js.map\n"
  },
  {
    "path": "temp/yarn.js",
    "content": "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.rebuild = exports.nodeGypRebuild = exports.getGypEnv = exports.installOrRebuild = void 0;\nconst builder_util_1 = require(\"builder-util\");\nconst fs_extra_1 = require(\"fs-extra\");\nconst os_1 = require(\"os\");\nconst path = require(\"path\");\nconst electronVersion_1 = require(\"../electron/electronVersion\");\nconst electronRebuild = require(\"@electron/rebuild\");\nconst searchModule = require(\"@electron/rebuild/lib/src/search-module\");\nasync function installOrRebuild(config, appDir, options, forceInstall = false) {\nconsole.log(\"install or rebuild\", { config, options })\n    let isDependenciesInstalled = false;\n    for (const fileOrDir of [\"node_modules\", \".pnp.js\"]) {\n        if (await (0, fs_extra_1.pathExists)(path.join(appDir, fileOrDir))) {\n            isDependenciesInstalled = true;\n            break;\n        }\n    }\n    if (forceInstall || !isDependenciesInstalled) {\n        const effectiveOptions = {\n            buildFromSource: config.buildDependenciesFromSource === true,\n            additionalArgs: (0, builder_util_1.asArray)(config.npmArgs),\n            ...options,\n        };\n        await installDependencies(appDir, effectiveOptions);\n    }\n    else {\n        await rebuild(appDir, config.buildDependenciesFromSource === true, options);\n    }\n}\nexports.installOrRebuild = installOrRebuild;\nfunction getElectronGypCacheDir() {\n    return path.join((0, os_1.homedir)(), \".electron-gyp\");\n}\nfunction getGypEnv(frameworkInfo, platform, arch, buildFromSource) {\n    const npmConfigArch = arch === \"armv7l\" ? \"arm\" : arch;\n    const common = {\n        ...process.env,\n        npm_config_arch: npmConfigArch,\n        npm_config_target_arch: npmConfigArch,\n        npm_config_platform: platform,\n        npm_config_build_from_source: buildFromSource,\n        // required for node-pre-gyp\n        npm_config_target_platform: platform,\n        npm_config_update_binary: true,\n        npm_config_fallback_to_build: true,\n    };\n    if (platform !== process.platform) {\n        common.npm_config_force = \"true\";\n    }\n    if (platform === \"win32\" || platform === \"darwin\") {\n        common.npm_config_target_libc = \"unknown\";\n    }\n    if (!frameworkInfo.useCustomDist) {\n        return common;\n    }\n    // https://github.com/nodejs/node-gyp/issues/21\n    return {\n        ...common,\n        npm_config_disturl: \"https://electronjs.org/headers\",\n        npm_config_target: frameworkInfo.version,\n        npm_config_runtime: \"electron\",\n        npm_config_devdir: getElectronGypCacheDir(),\n    };\n}\nexports.getGypEnv = getGypEnv;\nfunction checkYarnBerry() {\n    var _a;\n    const npmUserAgent = process.env[\"npm_config_user_agent\"] || \"\";\n    const regex = /yarn\\/(\\d+)\\./gm;\n    const yarnVersionMatch = regex.exec(npmUserAgent);\n    const yarnMajorVersion = Number((_a = yarnVersionMatch === null || yarnVersionMatch === void 0 ? void 0 : yarnVersionMatch[1]) !== null && _a !== void 0 ? _a : 0);\n    return yarnMajorVersion >= 2;\n}\nfunction installDependencies(appDir, options) {\n    const platform = options.platform || process.platform;\n    const arch = options.arch || process.arch;\n    const additionalArgs = options.additionalArgs;\n    builder_util_1.log.info({ platform, arch, appDir }, `installing production dependencies`);\n    let execPath = process.env.npm_execpath || process.env.NPM_CLI_JS;\n    const execArgs = [\"install\"];\n    const isYarnBerry = checkYarnBerry();\n    if (!isYarnBerry) {\n        if (process.env.NPM_NO_BIN_LINKS === \"true\") {\n            execArgs.push(\"--no-bin-links\");\n        }\n        execArgs.push(\"--production\");\n    }\n    if (!isRunningYarn(execPath)) {\n        execArgs.push(\"--prefer-offline\");\n    }\n    if (execPath == null) {\n        execPath = getPackageToolPath();\n    }\n    else if (!isYarnBerry) {\n        execArgs.unshift(execPath);\n        execPath = process.env.npm_node_execpath || process.env.NODE_EXE || \"node\";\n    }\n    if (additionalArgs != null) {\n        execArgs.push(...additionalArgs);\n    }\n    return (0, builder_util_1.spawn)(execPath, execArgs, {\n        cwd: appDir,\n        env: getGypEnv(options.frameworkInfo, platform, arch, options.buildFromSource === true),\n    });\n}\nasync function nodeGypRebuild(arch) {\n    return rebuild(process.cwd(), false, arch);\n}\nexports.nodeGypRebuild = nodeGypRebuild;\nfunction getPackageToolPath() {\n    if (process.env.FORCE_YARN === \"true\") {\n        return process.platform === \"win32\" ? \"yarn.cmd\" : \"yarn\";\n    }\n    else {\n        return process.platform === \"win32\" ? \"npm.cmd\" : \"npm\";\n    }\n}\nfunction isRunningYarn(execPath) {\n    const userAgent = process.env.npm_config_user_agent;\n    return process.env.FORCE_YARN === \"true\" || (execPath != null && path.basename(execPath).startsWith(\"yarn\")) || (userAgent != null && /\\byarn\\b/.test(userAgent));\n}\n/** @internal */\nasync function rebuild(appDir, buildFromSource, options) {\n    builder_util_1.log.info({ appDir, arch: options.arch, platform: options.platform }, \"executing @electron/rebuild\");\n    const effectiveOptions = {\n        buildPath: appDir,\n        electronVersion: await (0, electronVersion_1.getElectronVersion)(appDir),\n        arch: options.arch,\n        platform: options.platform,\n        force: true,\n        debug: builder_util_1.log.isDebugEnabled,\n        projectRootPath: await searchModule.getProjectRootPath(appDir),\n    };\n    if (buildFromSource) {\n        effectiveOptions.prebuildTagPrefix = \"totally-not-a-real-prefix-to-force-rebuild\";\n    }\n    return electronRebuild.rebuild(effectiveOptions);\n}\nexports.rebuild = rebuild;\n//# sourceMappingURL=yarn.js.map\n"
  },
  {
    "path": "update-banner.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Pinokio Update</title>\n    <style>\n      :root {\n        --bg: rgba(24, 24, 30, 0.94);\n        --bg-strong: rgba(30, 30, 38, 0.98);\n        --border: rgba(255, 255, 255, 0.12);\n        --text: #f5f5f7;\n        --muted: rgba(255, 255, 255, 0.68);\n        --accent: #f6c251;\n        --accent-strong: #ffcc66;\n        --danger: #ff7b72;\n        --shadow: 0 12px 26px rgba(0, 0, 0, 0.35);\n      }\n\n      html, body {\n        width: 100%;\n        height: 100%;\n        margin: 0;\n        padding: 0;\n        background: transparent;\n        font-family: \"SF Pro Text\", \"Segoe UI\", \"Helvetica Neue\", Arial, sans-serif;\n      }\n\n      #banner {\n        position: relative;\n        box-sizing: border-box;\n        width: 100%;\n        height: 100%;\n        display: flex;\n        align-items: center;\n        justify-content: space-between;\n        gap: 16px;\n        padding: 10px 16px 12px;\n        background: linear-gradient(90deg, var(--bg), var(--bg-strong));\n        border-bottom: 1px solid var(--border);\n        box-shadow: var(--shadow);\n        border-radius: 0;\n      }\n\n      .left {\n        min-width: 0;\n        display: flex;\n        flex-direction: column;\n        gap: 4px;\n      }\n\n      #title {\n        font-size: 15px;\n        font-weight: 600;\n        color: var(--text);\n        letter-spacing: 0.1px;\n      }\n\n      #details {\n        font-size: 12px;\n        color: var(--muted);\n        white-space: nowrap;\n        overflow: hidden;\n        text-overflow: ellipsis;\n        max-width: 520px;\n      }\n\n      .actions {\n        display: flex;\n        align-items: center;\n        gap: 8px;\n        flex-shrink: 0;\n      }\n\n      button {\n        appearance: none;\n        border: 1px solid transparent;\n        border-radius: 999px;\n        padding: 6px 12px;\n        font-size: 12px;\n        font-weight: 600;\n        cursor: pointer;\n        transition: background 120ms ease, border-color 120ms ease, transform 120ms ease;\n      }\n\n      button:disabled {\n        opacity: 0.6;\n        cursor: default;\n      }\n\n      .primary {\n        color: #1b1b1f;\n        background: var(--accent);\n      }\n\n      .primary:hover:not(:disabled) {\n        background: var(--accent-strong);\n        transform: translateY(-1px);\n      }\n\n      .ghost {\n        color: var(--text);\n        background: transparent;\n        border-color: rgba(255, 255, 255, 0.18);\n      }\n\n      .ghost:hover:not(:disabled) {\n        border-color: rgba(255, 255, 255, 0.35);\n      }\n\n      #progress {\n        position: absolute;\n        left: 0;\n        bottom: 0;\n        width: 100%;\n        height: 3px;\n        background: rgba(255, 255, 255, 0.15);\n      }\n\n      #progress-bar {\n        height: 100%;\n        width: 0%;\n        background: var(--accent);\n        transition: width 150ms ease;\n      }\n\n      .hidden {\n        display: none;\n      }\n\n      .danger {\n        color: var(--danger);\n      }\n    </style>\n  </head>\n  <body>\n    <div id=\"banner\">\n      <div class=\"left\">\n        <div id=\"title\">Update available</div>\n        <div id=\"details\"></div>\n      </div>\n      <div class=\"actions\">\n        <button id=\"update-now\" class=\"primary\">Update now</button>\n        <button id=\"restart-now\" class=\"primary hidden\">Restart now</button>\n        <button id=\"view-release\" class=\"ghost hidden\">Release notes</button>\n        <button id=\"dismiss\" class=\"ghost\">Later</button>\n      </div>\n      <div id=\"progress\" class=\"hidden\">\n        <div id=\"progress-bar\"></div>\n      </div>\n    </div>\n    <script>\n      const { ipcRenderer } = require('electron');\n\n      const title = document.getElementById('title');\n      const details = document.getElementById('details');\n      const updateNow = document.getElementById('update-now');\n      const restartNow = document.getElementById('restart-now');\n      const dismiss = document.getElementById('dismiss');\n      const viewRelease = document.getElementById('view-release');\n      const progress = document.getElementById('progress');\n      const progressBar = document.getElementById('progress-bar');\n\n      let lastPayload = {};\n\n      const setHidden = (el, hidden) => {\n        if (!el) return;\n        el.classList.toggle('hidden', Boolean(hidden));\n      };\n\n      const setText = (el, text) => {\n        if (!el) return;\n        el.textContent = text || '';\n      };\n\n      const render = (payload = {}) => {\n        lastPayload = payload || {};\n        const state = payload.state || 'available';\n        const version = payload.version ? `Version ${payload.version}` : '';\n        const notes = payload.notesPreview || '';\n        const detail = [version, notes].filter(Boolean).join(' - ');\n\n        let titleText = 'Update available';\n        if (state === 'downloading') titleText = 'Downloading update';\n        if (state === 'ready') titleText = 'Update ready';\n        if (state === 'error') titleText = 'Update failed';\n\n        setText(title, titleText);\n        setText(details, detail);\n\n        const isDownloading = state === 'downloading';\n        const isReady = state === 'ready';\n        const isError = state === 'error';\n        const hasReleaseUrl = Boolean(payload.releaseUrl);\n\n        updateNow.textContent = isError ? 'Retry' : 'Update now';\n        updateNow.disabled = isDownloading;\n\n        setHidden(updateNow, isReady);\n        setHidden(restartNow, !isReady);\n        setHidden(viewRelease, !hasReleaseUrl);\n        setHidden(progress, !isDownloading);\n\n        if (isError) {\n          title.classList.add('danger');\n        } else {\n          title.classList.remove('danger');\n        }\n\n        if (isDownloading && typeof payload.progressPercent === 'number') {\n          const percent = Math.max(0, Math.min(100, payload.progressPercent));\n          progressBar.style.width = `${percent}%`;\n        } else {\n          progressBar.style.width = '0%';\n        }\n      };\n\n      ipcRenderer.on('pinokio:update-banner', (_event, payload) => {\n        render(payload);\n      });\n\n      updateNow.addEventListener('click', () => {\n        ipcRenderer.send('pinokio:update-banner-action', { action: 'update' });\n      });\n\n      restartNow.addEventListener('click', () => {\n        ipcRenderer.send('pinokio:update-banner-action', { action: 'restart' });\n      });\n\n      dismiss.addEventListener('click', () => {\n        ipcRenderer.send('pinokio:update-banner-action', { action: 'dismiss' });\n      });\n\n      viewRelease.addEventListener('click', () => {\n        if (lastPayload.releaseUrl) {\n          ipcRenderer.send('pinokio:update-banner-action', {\n            action: 'release-notes',\n            releaseUrl: lastPayload.releaseUrl\n          });\n        }\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "updater.js",
    "content": "const { autoUpdater } = require(\"electron-updater\");\nconst ProgressBar = require('electron-progressbar');\nconst { dialog } = require('electron');\n\nclass Updater {\n  constructor(handlers = {}) {\n    this.handlers = handlers || {};\n    this.mainWindow = null;\n    this.progressBar = null;\n  }\n\n  setHandlers(handlers = {}) {\n    this.handlers = handlers || {};\n  }\n\n  run(mainWindow, handlers = null) {\n    if (handlers) {\n      this.setHandlers(handlers);\n    }\n    this.mainWindow = mainWindow || null;\n    autoUpdater.autoDownload = false;\n\n    autoUpdater.on('checking-for-update', () => {\n      console.log('Checking for update...');\n    });\n\n    autoUpdater.on('update-available', (info) => {\n      console.log('Update available:', info.version);\n      if (this.handlers && typeof this.handlers.onUpdateAvailable === 'function') {\n        this.handlers.onUpdateAvailable(info);\n        return;\n      }\n      this.showDefaultUpdatePrompt(info);\n    });\n\n    autoUpdater.on('update-not-available', () => {\n      console.log('No update available.');\n      if (this.handlers && typeof this.handlers.onUpdateNotAvailable === 'function') {\n        this.handlers.onUpdateNotAvailable();\n      }\n    });\n\n    autoUpdater.on(\"download-progress\", (progress) => {\n      console.log(`Downloaded ${Math.round(progress.percent)}%`);\n      if (this.handlers && typeof this.handlers.onDownloadProgress === 'function') {\n        this.handlers.onDownloadProgress(progress);\n        return;\n      }\n      this.updateDefaultProgress(progress);\n    });\n\n    autoUpdater.on(\"update-downloaded\", (info) => {\n      console.log(\"Update downloaded:\", info.version);\n      if (this.handlers && typeof this.handlers.onUpdateDownloaded === 'function') {\n        this.handlers.onUpdateDownloaded(info);\n        return;\n      }\n      this.showDefaultRestartPrompt(info);\n    });\n\n    autoUpdater.on(\"error\", (err) => {\n      console.error(\"Update error:\", err);\n      if (this.handlers && typeof this.handlers.onError === 'function') {\n        this.handlers.onError(err);\n        return;\n      }\n      this.closeProgressBar();\n    });\n\n    autoUpdater.checkForUpdates().catch((err) => {\n      // The updater promise rejects on recoverable network errors; log and continue.\n      console.error('Failed to check for updates:', err)\n    });\n  }\n\n  downloadUpdate() {\n    return autoUpdater.downloadUpdate();\n  }\n\n  quitAndInstall() {\n    autoUpdater.quitAndInstall();\n  }\n\n  showDefaultUpdatePrompt(info) {\n    const targetWindow = this.mainWindow;\n    dialog.showMessageBox(targetWindow, {\n      type: 'question',\n      buttons: ['Yes', 'No'],\n      defaultId: 0,\n      cancelId: 1,\n      title: 'Update Available',\n      message: `Version ${info.version} is available. Do you want to download it now?`\n    }).then(result => {\n      if (result.response === 0) {\n        this.startDefaultDownload();\n      }\n    });\n  }\n\n  startDefaultDownload() {\n    if (this.progressBar) {\n      this.progressBar.close();\n      this.progressBar = null;\n    }\n    this.progressBar = new ProgressBar({\n      indeterminate: false,\n      text: \"Downloading update...\",\n      detail: \"Please wait...\",\n      browserWindow: {\n        parent: this.mainWindow,\n        modal: true,\n        closable: false,\n        minimizable: false,\n        maximizable: false,\n        width: 400,\n        height: 120\n      }\n    });\n    autoUpdater.downloadUpdate();\n  }\n\n  updateDefaultProgress(progress) {\n    if (this.progressBar && !this.progressBar.isCompleted()) {\n      this.progressBar.value = Math.floor(progress.percent);\n      this.progressBar.detail = `Downloaded ${Math.round(progress.percent)}% (${(progress.transferred / 1024 / 1024).toFixed(2)} MB of ${(progress.total / 1024 / 1024).toFixed(2)} MB)`;\n    }\n  }\n\n  showDefaultRestartPrompt(info) {\n    if (this.progressBar && !this.progressBar.isCompleted()) {\n      this.progressBar.setCompleted();\n      this.progressBar = null;\n    }\n    dialog.showMessageBox(this.mainWindow, {\n      type: \"info\",\n      buttons: [\"Restart Now\", \"Later\"],\n      title: \"Update Ready\",\n      message: \"A new version has been downloaded. Restart the application to apply the updates?\"\n    }).then((result) => {\n      if (result.response === 0) {\n        autoUpdater.quitAndInstall();\n      }\n    });\n  }\n\n  closeProgressBar() {\n    if (this.progressBar && !this.progressBar.isCompleted()) {\n      this.progressBar.close();\n      this.progressBar = null;\n    }\n  }\n}\n\nmodule.exports = Updater;\n"
  },
  {
    "path": "wrap-linux-launcher.js",
    "content": "const fs = require('fs')\nconst path = require('path')\nmodule.exports = async (context) => {\n  const { appOutDir, electronPlatformName, packager } = context\n\n  if (electronPlatformName !== 'linux') {\n    return\n  }\n\n  const exeName = packager.executableName || packager.appInfo.productFilename\n  const exePath = path.join(appOutDir, exeName)\n  const wrappedExePath = path.join(appOutDir, `${exeName}-bin`)\n\n  if (!fs.existsSync(exePath)) {\n    console.warn(`[wrap-linux-launcher] Executable not found at ${exePath}, skipping wrapper`)\n    return\n  }\n\n  const originalStat = fs.statSync(exePath)\n\n  fs.renameSync(exePath, wrappedExePath)\n\n  const wrapperScript = `#!/usr/bin/env sh\nexport ELECTRON_OZONE_PLATFORM_HINT=x11\nexport ELECTRON_DISABLE_GPU=1\nSCRIPT_PATH=\"$0\"\nRESOLVED_PATH=\"\"\n\nif command -v readlink >/dev/null 2>&1; then\n  RESOLVED_PATH=\"$(readlink -f \"$SCRIPT_PATH\" 2>/dev/null || true)\"\nfi\n\nif [ -z \"$RESOLVED_PATH\" ] && command -v realpath >/dev/null 2>&1; then\n  RESOLVED_PATH=\"$(realpath \"$SCRIPT_PATH\" 2>/dev/null || true)\"\nfi\n\nif [ -n \"$RESOLVED_PATH\" ]; then\n  SCRIPT_DIR=\"$(dirname \"$RESOLVED_PATH\")\"\nelse\n  SCRIPT_DIR=\"$(dirname \"$SCRIPT_PATH\")\"\nfi\n\nOPT_BIN=\"/opt/Pinokio/${exeName}-bin\"\nLOCAL_BIN=\"$SCRIPT_DIR/${exeName}-bin\"\n\nif [ -x \"$OPT_BIN\" ]; then\n  TARGET_BIN=\"$OPT_BIN\"\nelse\n  TARGET_BIN=\"$LOCAL_BIN\"\nfi\n\nexec \"$TARGET_BIN\" --ozone-platform=x11 --disable-gpu --disable-gpu-sandbox \"$@\"\n`\n\n  fs.writeFileSync(exePath, wrapperScript, { mode: originalStat.mode || 0o755 })\n}\n"
  }
]