Full Code of pinokiocomputer/pinokio for AI

main f5e5073c1f19 cached
33 files
263.5 KB
66.9k tokens
43 symbols
1 requests
Download .txt
Showing preview only (275K chars total). Download the full file or copy to clipboard to get everything.
Repository: pinokiocomputer/pinokio
Branch: main
Commit: f5e5073c1f19
Files: 33
Total size: 263.5 KB

Directory structure:
gitextract_4ttzgoi5/

├── .github/
│   └── workflows/
│       ├── build.yml
│       └── test.yml
├── .gitignore
├── LICENSE
├── README.md
├── RELEASE.md
├── after-pack.js
├── build/
│   ├── entitlements.mac.inherit.plist
│   ├── entitlements.mac.plist
│   ├── icon.icns
│   ├── installer.nsh
│   └── sign.js
├── chmod.js
├── config.js
├── full.js
├── linux_build.sh
├── main.js
├── minimal.js
├── package.json
├── patch-linux-arm64-natives.js
├── popup-shell.js
├── popup-toolbar.html
├── preload.js
├── prompt.html
├── script/
│   ├── patch.command
│   ├── run-update-banner-test.js
│   └── zip.js
├── splash.html
├── temp/
│   ├── rebuild.js
│   └── yarn.js
├── update-banner.html
├── updater.js
└── wrap-linux-launcher.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/build.yml
================================================
name: Build/release

on:
  push:
    branches:
      - main

#on: workflow_dispatch

jobs:

  create-release:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - name: Check out Git repository
        uses: actions/checkout@v1

      - name: Get package.json version
        id: get_version
        shell: bash
        run: |
          PACKAGE_VERSION=$(node -p "require('./package.json').version")
          echo "PACKAGE_VERSION=$PACKAGE_VERSION" >> $GITHUB_ENV
      - name: Create an empty release
        shell: bash
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          echo "Releasing version $PACKAGE_VERSION"
          gh release create "v$PACKAGE_VERSION" --draft \
            --title "v$PACKAGE_VERSION" \
            --notes-file RELEASE.md
#            --notes "Pinokio version $PACKAGE_VERSION."

  windows-unsigned:
    if: false
    needs: "create-release"
    runs-on: windows-latest
    permissions:
      contents: write
    steps:
      - name: Check out Git repository
        uses: actions/checkout@v1

      - name: Install Node.js, NPM and Yarn
        uses: actions/setup-node@v1
        with:
          node-version: 22

      - name: Build/release Electron app
        id: electron-builder
        uses: samuelmeuli/action-electron-builder@v1.6.0
        with:
          github_token: ${{ secrets.github_token }}

          # If the commit is tagged with a version (e.g. "v1.0.0"),
          # release the app after building
          #release: ${{ startsWith(github.ref, 'refs/tags/v') }}
          release: true
          #args: --win --dir  # Build win-unpacked only
          args: --win

  windows:
#    if: false
    needs: "create-release"
    runs-on: windows-latest
    permissions:
      contents: write
    steps:
      - name: Check out Git repository
        uses: actions/checkout@v1

      - name: Install Node.js, NPM and Yarn
        uses: actions/setup-node@v1
        with:
          node-version: 22

      - name: Build/release Electron app
        id: electron-builder
        uses: samuelmeuli/action-electron-builder@v1.6.0
        with:
          github_token: ${{ secrets.github_token }}

          # If the commit is tagged with a version (e.g. "v1.0.0"),
          # release the app after building
          #release: ${{ startsWith(github.ref, 'refs/tags/v') }}
          #release: true
          release: false
          #args: --win --dir  # Build win-unpacked only
          args: --win

#      - name: Check contents
#        run: |
#          dir dist-win32 \
#          dir dist-win32\\win-unpacked
#        shell: cmd

### sign start

      - name: upload-unsigned-artifact
        id: upload-unsigned-artifact
        uses: actions/upload-artifact@v4
        with:
          #path: dist-win32
          #path: dist-win32/win-unpacked/Pinokio.exe
          path: dist-win32/Pinokio.exe
          retention-days: 1

      - id: Sign
        if: ${{ runner.os == 'Windows' }}
        uses: signpath/github-action-submit-signing-request@v1.1
        with:
          api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
          organization-id: 'd2da0df2-dc12-4516-8222-87178d5ebf3d'
          project-slug: 'pinokio'
          #signing-policy-slug: 'test-signing'
          signing-policy-slug: 'release-signing'
          github-artifact-id: '${{ steps.upload-unsigned-artifact.outputs.artifact-id }}'
          wait-for-completion: true
          output-artifact-directory: './signed-windows'
          parameters: |
            version: ${{ toJSON(github.ref_name) }}

      - name: Rebuild blockmap and latest.yml from signed installer
        shell: bash
        run: |
          set -euo pipefail
          shopt -s nullglob

          files=(signed-windows/*.exe)
          if [[ ${#files[@]} -ne 1 ]]; then
            echo "Expected exactly one signed exe, found ${#files[@]}" >&2
            exit 1
          fi

          SIGNED_EXE="${files[0]}"
          EXE_BASENAME=$(basename "$SIGNED_EXE")
          VERSION=$(node -p "require('./package.json').version")
          OUT_DIR=dist-win32
          mkdir -p "$OUT_DIR"

          # Use app-builder bundled with electron-builder to regenerate blockmap for the signed binary
          APP_BUILDER=$(node -p "require('app-builder-bin').appBuilderPath")
          "$APP_BUILDER" blockmap --input "$SIGNED_EXE" --output "$OUT_DIR/${EXE_BASENAME}.blockmap"

          EXE="$SIGNED_EXE" BLOCKMAP="$OUT_DIR/${EXE_BASENAME}.blockmap" VERSION="$VERSION" OUT_DIR="$OUT_DIR" node - <<'EOF'
          const fs = require('fs');
          const crypto = require('crypto');
          const path = require('path');

          const exe = process.env.EXE;
          const blockmap = process.env.BLOCKMAP;
          const version = process.env.VERSION;
          const outDir = process.env.OUT_DIR;

          const exeStats = fs.statSync(exe);
          const blockmapStats = fs.statSync(blockmap);
          const sha512 = crypto.createHash('sha512').update(fs.readFileSync(exe)).digest('base64');

          const lines = [
            `version: ${version}`,
            `files:`,
            `  - url: ${path.basename(exe)}`,
            `    sha512: ${sha512}`,
            `    size: ${exeStats.size}`,
            `    blockMapSize: ${blockmapStats.size}`,
            `path: ${path.basename(exe)}`,
            `sha512: ${sha512}`,
            `releaseDate: "${new Date().toISOString()}"`
          ];

          fs.writeFileSync(path.join(outDir, 'latest.yml'), lines.join('\n'));
          EOF

#      # Replace the unsigned exe with the signed exe
#      - name: Replace with signed exe
#        run: |
#          copy /Y ".\signed-windows\Pinokio.exe" ".\dist-win32\win-unpacked\Pinokio.exe"
#        shell: cmd

### sign end

#      # Build the final installer from the signed exe
#      - name: Build final installer
#        env:
#          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
##          CSC_IDENTITY_AUTO_DISCOVERY: "false"   # disable any auto code-sign discovery
##          DISABLE_CODE_SIGNING: "true"           # electron-builder respects this to skip signing
#        run: |
#          #yarn run electron-builder --win --prepackaged dist-win32/win-unpacked --publish never
#          yarn run electron-builder --win --prepackaged dist-win32/win-unpacked --publish always


      - name: Get package.json version
        id: get_version
        shell: bash
        run: |
          PACKAGE_VERSION=$(node -p "require('./package.json').version")
          echo "PACKAGE_VERSION=$PACKAGE_VERSION" >> $GITHUB_ENV

      - name: Publish GitHub Release with gh
        shell: bash
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          echo "Releasing version $PACKAGE_VERSION"
          #gh release create "v$PACKAGE_VERSION" ./signed-windows/*.exe \

          #gh release upload "v$PACKAGE_VERSION" ./dist-win32/*.exe .dist-win32/latest.yml ./dist-win32/*.exe.blockmap
          gh release upload "v$PACKAGE_VERSION" ./signed-windows/*.exe ./dist-win32/latest.yml ./dist-win32/*.exe.blockmap --clobber
          #gh release create "v$PACKAGE_VERSION" ./dist-win32/*.exe \
          #  --title "Release v$PACKAGE_VERSION" \
          #  --notes "Pinokio version $PACKAGE_VERSION."

  mac:
#    if: false
    needs: "create-release"
    runs-on: macos-latest
    permissions:
      contents: write
    steps:
      - name: Check out Git repository
        uses: actions/checkout@v1

      - name: Install Node.js, NPM and Yarn
        uses: actions/setup-node@v1
        with:
          node-version: 22

#      - name: Prepare for app notarization
#        if: startsWith(matrix.os, 'macos')
#        # Import Apple API key for app notarization on macOS
#        run: |
#          mkdir -p ~/private_keys/
#          echo '${{ secrets.api_key }}' > ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8


      - name: Build/release Electron app
        id: electron-builder
        uses: samuelmeuli/action-electron-builder@v1.6.0
        with:
          # GitHub token, automatically provided to the action
          # (No need to define this secret in the repo settings)
          github_token: ${{ secrets.github_token }}

          # If the commit is tagged with a version (e.g. "v1.0.0"),
          # release the app after building
          #release: ${{ startsWith(github.ref, 'refs/tags/v') }}
          release: true
          mac_certs: ${{ secrets.mac_certs }}
          mac_certs_password: ${{ secrets.mac_certs_password }}
        env:
          # macOS notarization API key
          #API_KEY_ID: ${{ secrets.api_key_id }}
          #API_KEY_ISSUER_ID: ${{ secrets.api_key_issuer_id }}
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}

      - name: Show notarization-error.log
        if: failure()
        run: cat dist-darwin/**/notarization-error.log || echo "No notarization-error.log found"

  linux:
#    if: false
    needs: "create-release"
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        arch: [x64, arm64]
    permissions:
      contents: write
    steps:
      - name: Check out Git repository
        uses: actions/checkout@v1

      - name: Install Node.js, NPM and Yarn
        uses: actions/setup-node@v1
        with:
          node-version: 22

      - name: Install dependencies
        shell: bash
        run: |
          if [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then
            npm ci
          else
            npm install
          fi

      - name: Constrain Linux targets to matrix arch
        shell: bash
        env:
          TARGET_ARCH: ${{ matrix.arch }}
        run: |
          node - <<'EOF'
          const fs = require('fs');
          const packagePath = './package.json';
          const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));

          const targetArch = process.env.TARGET_ARCH;
          if (!targetArch) {
            throw new Error('TARGET_ARCH is not set');
          }

          const linux = packageJson.build && packageJson.build.linux;
          if (!linux || !Array.isArray(linux.target)) {
            throw new Error('build.linux.target is not configured as an array');
          }

          linux.target = linux.target.map((entry) => {
            if (typeof entry === 'string') {
              return { target: entry, arch: [targetArch] };
            }
            if (entry && typeof entry === 'object') {
              return { ...entry, arch: [targetArch] };
            }
            return entry;
          });

          fs.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`);
          EOF

      - name: Install ARM64 parcel watcher native
        if: matrix.arch == 'arm64'
        shell: bash
        run: |
          npm_config_os=linux \
          npm_config_cpu=arm64 \
          npm_config_libc=glibc \
          npm_config_force=true \
          npm install --no-save @parcel/watcher-linux-arm64-glibc@2.5.1

      - name: Build/release Linux app
        shell: bash
        env:
          GITHUB_TOKEN: ${{ secrets.github_token }}
        run: |
          ./node_modules/.bin/electron-builder install-app-deps --platform linux --arch ${{ matrix.arch }}
          ./node_modules/.bin/electron-builder --linux --${{ matrix.arch }} --publish never

      - name: Validate ARM64 Linux packaged natives
        if: matrix.arch == 'arm64'
        shell: bash
        run: |
          set -euo pipefail
          shopt -s nullglob

          pick_file() {
            for candidate in "$@"; do
              if [ -f "$candidate" ]; then
                printf '%s\n' "$candidate"
                return 0
              fi
            done
            return 1
          }

          deb_files=(dist-linux/*arm64*.deb dist-linux/*aarch64*.deb)
          if [ ${#deb_files[@]} -eq 0 ]; then
            echo "No ARM64 .deb artifacts found in dist-linux" >&2
            ls -la dist-linux || true
            exit 1
          fi

          for deb in "${deb_files[@]}"; do
            echo "Validating $deb"
            workdir="$(mktemp -d)"
            dpkg-deb -x "$deb" "$workdir/root"

            if ! grep -R --binary-files=without-match -q '/opt/Pinokio/pinokio-bin' "$workdir/root"; then
              echo "Could not find /opt/Pinokio/pinokio-bin fallback in extracted package" >&2
              exit 1
            fi

            pty_node="$(pick_file \
              "$workdir/root/opt/Pinokio/resources/app.asar.unpacked/node_modules/@homebridge/node-pty-prebuilt-multiarch/build/Release/pty.node" \
              "$workdir/root/opt/Pinokio/resources/app.asar.unpacked/node_modules/pinokiod/node_modules/@homebridge/node-pty-prebuilt-multiarch/build/Release/pty.node" \
            )" || {
              echo "Missing patched pty.node in ARM64 package" >&2
              exit 1
            }

            watcher_node="$(pick_file \
              "$workdir/root/opt/Pinokio/resources/app.asar.unpacked/node_modules/@parcel/watcher/build/Release/watcher.node" \
              "$workdir/root/opt/Pinokio/resources/app.asar.unpacked/node_modules/pinokiod/node_modules/@parcel/watcher/build/Release/watcher.node" \
            )" || {
              echo "Missing patched watcher.node in ARM64 package" >&2
              exit 1
            }

            watcher_platform_node="$(pick_file \
              "$workdir/root/opt/Pinokio/resources/app.asar.unpacked/node_modules/@parcel/watcher-linux-arm64-glibc/watcher.node" \
              "$workdir/root/opt/Pinokio/resources/app.asar.unpacked/node_modules/pinokiod/node_modules/@parcel/watcher-linux-arm64-glibc/watcher.node" \
            )" || {
              echo "Missing @parcel/watcher-linux-arm64-glibc payload in ARM64 package" >&2
              exit 1
            }

            file "$pty_node"
            file "$watcher_node"
            file "$watcher_platform_node"

            file "$pty_node" | grep -qi 'aarch64' || {
              echo "pty.node is not ARM64 in $deb" >&2
              exit 1
            }
            file "$watcher_node" | grep -qi 'aarch64' || {
              echo "watcher.node is not ARM64 in $deb" >&2
              exit 1
            }
            file "$watcher_platform_node" | grep -qi 'aarch64' || {
              echo "watcher-linux-arm64-glibc payload is not ARM64 in $deb" >&2
              exit 1
            }

            rm -rf "$workdir"
          done

      - name: Upload Linux release artifacts
        shell: bash
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          set -euo pipefail

          version="$(node -p "require('./package.json').version")"
          tag="v${version}"

          files=()
          while IFS= read -r -d '' file; do
            files+=("$file")
          done < <(find dist-linux -maxdepth 1 -type f \
            \( -name '*.AppImage' -o -name '*.deb' -o -name '*.rpm' -o -name '*linux*.yml' -o -name 'latest*.yml' \) \
            -print0)

          if [ "${#files[@]}" -eq 0 ]; then
            echo "No Linux artifacts found in dist-linux" >&2
            ls -la dist-linux || true
            exit 1
          fi

          gh release upload "$tag" "${files[@]}" --clobber


================================================
FILE: .github/workflows/test.yml
================================================
name: Test

#on: push
on: workflow_dispatch

jobs:
  print:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Get package.json version
        id: get_version
        shell: bash
#        run: echo "PACKAGE_VERSION=$(node -p 'require("./package.json").version')" >> $GITHUB_ENV
        #run: echo 'PACKAGE_VERSION=$(node -p "require(\"./package.json\").version")' >> $GITHUB_ENV
#        run: echo "PACKAGE_VERSION=$(node -p \"require('./package.json').version\")" >> $GITHUB_ENV
        run: |
          PACKAGE_VERSION=$(node -p "require('./package.json').version")
          echo "PACKAGE_VERSION=$PACKAGE_VERSION" >> $GITHUB_ENV

      - name: Print env
        shell: bash
        run: echo $PACKAGE_VERSION


================================================
FILE: .gitignore
================================================
node_modules
dist
cache
package-lock.json
.claude


================================================
FILE: LICENSE
================================================
Copyright 2023 Pinokio

Permission 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:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE 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.


================================================
FILE: README.md
================================================
# Pinokio

Launch Anything.

# Script Policy

Pinokio 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.

This means:

1. **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.
2. **How scripts can be run:** There are two ways to run scripts on Pinokio:
    1. **Write your own:** Just like writing and executing shell scripts in the terminal, you can create your own scripts and run them locally.
    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.
3. **Verified Scripts:** To be featured on the "Discover" page, scripts must go through the following strict process:
    1. **Publisher Verification:** You must be personally verified to submit scripts for consideration. Contact the Pinokio admin (https://x.com/cocktailpeanut) to request verification.
    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.
    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
    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.
    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.
    6. **Troubleshooting:** If any issues arise after a script is featured, the Pinokio admin may:
        - Delist the script from the "Discover" page
        - 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.

# Security

## Scripts are isolated by design

By 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.

The script verification process checks to make sure this doesn't happen.

Th Pinokio script syntax was designed to make this process simpler, both by human and machines.

## Scripts are open source

All 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.

Here'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.

![install.png](install.png)

## Script Verification

Verified 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:

1. **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)
2. **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)
3. **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)

Here's an example execution script that installs python dependencies:

```json
{
  "method": "shell.run",
  "params": {
    "message": "uv pip install -r requirements.txt",
    "path": "server",
    "venv": "venv"
  }
}
```

1. 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`)
2. 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`)
3. 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`

The script verification check makes sure that all these components are run locally within the constraints of each app.

Of course, there are also additional checks such as:

1. Checking the reputation of the repository and the developer of the original project
2. Trhing out the app personally
3. Making sure that the install and launch instructions actually follow the recommended instructions suggested in the original project's README.

No scripts are approved until rigorously tested.


================================================
FILE: RELEASE.md
================================================
# Pinokio Release

## Code Signing Policy

Free code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/).

## Privacy Policy

This program will not transfer any information to other networked systems unless specifically requested by the user or the person installing or operating it.


================================================
FILE: after-pack.js
================================================
module.exports = async (context) => {
  const chmodHandler = require('./chmod')
  const wrapLinuxLauncher = require('./wrap-linux-launcher')
  const patchLinuxArm64Natives = require('./patch-linux-arm64-natives')

  await chmodHandler(context)
  await wrapLinuxLauncher(context)
  await patchLinuxArm64Natives(context)
}


================================================
FILE: build/entitlements.mac.inherit.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>com.apple.security.cs.allow-jit</key>
    <true/>
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
    <true/>
    <key>com.apple.security.cs.disable-library-validation</key>
    <true/>
  </dict>
</plist>


================================================
FILE: build/entitlements.mac.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>com.apple.security.cs.allow-jit</key>
    <true/>
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
    <true/>
    <key>com.apple.security.cs.disable-library-validation</key>
    <true/>
    <key>com.apple.security.device.audio-input</key>
    <true/>
    <key>com.apple.security.device.camera</key>
    <true/>
  </dict>
</plist>


================================================
FILE: build/installer.nsh
================================================
# https://github.com/electron-userland/electron-builder/issues/6865#issuecomment-1871121350
!macro customInit
  Delete "$INSTDIR\Uninstall*.exe"
!macroend


================================================
FILE: build/sign.js
================================================
module.exports = async function () {
  // no-op: prevents electron-builder from calling signtool.exe
};


================================================
FILE: chmod.js
================================================
const exec = require('child_process').exec;
module.exports = async (context) => {
  const paths = [
    `${context.appOutDir}/resources/app.asar.unpacked/node_modules/go-get-folder-size/dist/go-get-folder-size_linux_386/go-get-folder-size`,
    `${context.appOutDir}/resources/app.asar.unpacked/node_modules/go-get-folder-size/dist/go-get-folder-size_linux_amd64_v1/go-get-folder-size`,
    `${context.appOutDir}/resources/app.asar.unpacked/node_modules/go-get-folder-size/dist/go-get-folder-size_linux_arm64/go-get-folder-size`,
  ]
  for(let p of paths) {
    await exec(`chmod +x "${p}"`);
  }
}


================================================
FILE: config.js
================================================
const Store = require('electron-store');
const packagejson = require("./package.json")
const store = new Store();
module.exports = {
  newsfeed: (gitRemote) => {
    return `https://pinokiocomputer.github.io/home/item?uri=${gitRemote}&display=feed`
  },
  profile: (gitRemote) => {
    return `https://pinokiocomputer.github.io/home/item?uri=${gitRemote}&display=profile`
  },
  site: "https://pinokio.co",
  discover_dark: "https://beta.pinokio.co",
  discover_light: "https://beta.pinokio.co",
  portal: "https://beta.pinokio.co",
  docs: "https://pinokio.co/docs",
  install: "https://pinokiocomputer.github.io/program.pinokio.computer/#/?id=install",
  agent: "electron",
  version: packagejson.version,
  store
}


================================================
FILE: full.js
================================================
const {app, screen, shell, BrowserWindow, BrowserView, ipcMain, dialog, clipboard, session, desktopCapturer, systemPreferences, Menu } = require('electron')
const windowStateKeeper = require('electron-window-state');
const fs = require('fs')
const path = require("path")
const Pinokiod = require("pinokiod")
const os = require('os')
const Updater = require('./updater')
const createPopupShellManager = require('./popup-shell')
const is_mac = process.platform.startsWith("darwin")
const platform = os.platform()
var mainWindow;
var root_url;
var wins = {}
var pinned = {}
var launched
var theme
var colors
var splashWindow
var splashIcon
var updateBannerPayload
var updateBannerDismissed = false
var updateInfo = null
var updateDownloadInFlight = false
const updateTestMode = (() => {
  const value = process.env.PINOKIO_TEST_UPDATE_BANNER
  if (!value) {
    return false
  }
  return ['1', 'true', 'yes', 'on'].includes(String(value).toLowerCase())
})()
let updateTestInterval = null
let updateTestTimeout = null
const UPDATE_RELEASES_URL = 'https://github.com/peanutcocktail/pinokio/releases'
const setWindowTitleBarOverlay = (win, overlay) => {
  if (!win || !win.setTitleBarOverlay) {
    return
  }
  try {
    win.setTitleBarOverlay(overlay)
  } catch (e) {
//    console.log("ERROR", e)
  }
}
const applyTitleBarOverlayToAllWindows = () => {
  if (!colors) {
    return
  }
  const overlay = titleBarOverlay(colors)
  const browserWindows = BrowserWindow.getAllWindows()
  for (const win of browserWindows) {
    setWindowTitleBarOverlay(win, overlay)
  }
}
const updateThemeColors = (payload = {}) => {
  console.log("updateThemeColors", payload)
  const nextTheme = payload.theme
  const nextColors = payload.colors
  if (nextTheme) {
    theme = nextTheme
  }
  if (nextColors) {
    colors = nextColors
  }
  applyTitleBarOverlayToAllWindows()
}
const stripHtmlTags = (value) => {
  if (!value) {
    return ''
  }
  return String(value).replace(/<[^>]*>/g, '')
}
const buildReleaseNotesPreview = (notes) => {
  if (!notes) {
    return ''
  }
  let text = ''
  if (Array.isArray(notes)) {
    text = notes.map((note) => note && (note.note || note.releaseNotes || note.title || '')).join('\n')
  } else if (typeof notes === 'string') {
    text = notes
  } else {
    text = String(notes)
  }
  const cleaned = stripHtmlTags(text).replace(/\r/g, '')
  const lines = cleaned.split('\n').map((line) => line.trim()).filter(Boolean)
  if (!lines.length) {
    return ''
  }
  const firstLine = lines[0]
  if (firstLine.length > 140) {
    return `${firstLine.slice(0, 137)}...`
  }
  return firstLine
}
const buildProgressLabel = (progress) => {
  if (!progress || typeof progress.percent !== 'number') {
    return ''
  }
  const percent = Math.round(progress.percent)
  if (typeof progress.transferred === 'number' && typeof progress.total === 'number' && progress.total > 0) {
    const transferred = (progress.transferred / 1024 / 1024).toFixed(1)
    const total = (progress.total / 1024 / 1024).toFixed(1)
    return `${percent}% (${transferred} MB of ${total} MB)`
  }
  return `${percent}%`
}
const buildUpdateBannerPayload = (state, info, extra = {}) => {
  const resolved = info || {}
  return {
    state,
    version: resolved.version || '',
    notesPreview: buildReleaseNotesPreview(resolved.releaseNotes),
    releaseUrl: UPDATE_RELEASES_URL,
    ...extra
  }
}
const clearUpdateTestTimers = () => {
  if (updateTestInterval) {
    clearInterval(updateTestInterval)
    updateTestInterval = null
  }
  if (updateTestTimeout) {
    clearTimeout(updateTestTimeout)
    updateTestTimeout = null
  }
}
const showUpdateBannerTestAvailable = () => {
  updateInfo = {
    version: '99.9.9-test',
    releaseNotes: 'Simulated update for banner testing.'
  }
  updateDownloadInFlight = false
  updateBannerDismissed = false
  showUpdateBanner(buildUpdateBannerPayload('available', updateInfo))
}
const startUpdateBannerTestDownload = () => {
  if (!updateInfo) {
    showUpdateBannerTestAvailable()
  }
  clearUpdateTestTimers()
  updateDownloadInFlight = true
  let progress = 0
  const tick = () => {
    progress = Math.min(100, progress + 6 + Math.random() * 12)
    showUpdateBanner(buildUpdateBannerPayload('downloading', updateInfo, {
      progressPercent: progress,
      notesPreview: `${Math.round(progress)}%`
    }))
    if (progress >= 100) {
      clearUpdateTestTimers()
      updateDownloadInFlight = false
      showUpdateBanner(buildUpdateBannerPayload('ready', updateInfo))
    }
  }
  tick()
  updateTestInterval = setInterval(tick, 320)
}
const simulateUpdateBannerRestart = () => {
  clearUpdateTestTimers()
  hideUpdateBanner()
  updateTestTimeout = setTimeout(() => {
    showUpdateBannerTestAvailable()
  }, 800)
}
const dispatchUpdateBanner = (payload) => {
  updateBannerPayload = payload
  if (!mainWindow || mainWindow.isDestroyed()) {
    return
  }
  if (payload && payload.state === 'available' && updateBannerDismissed) {
    return
  }
  if (mainWindow.webContents && !mainWindow.webContents.isDestroyed()) {
    if (!mainWindow.webContents.isLoading()) {
      mainWindow.webContents.send('pinokio:update-banner', payload)
    }
  }
}
const showUpdateBanner = (payload) => {
  if (payload && payload.state === 'available' && updateBannerDismissed) {
    updateBannerPayload = payload
    return
  }
  dispatchUpdateBanner(payload || updateBannerPayload)
}
const hideUpdateBanner = () => {
  dispatchUpdateBanner({ state: 'hidden' })
}
let PORT
//let PORT = 42000
//let PORT = (platform === 'linux' ? 42000 : 80)

let config = require('./config')

const filter = function (item) {
  return item.browserName === 'Chrome';
};

const updater = new Updater()
const pinokiod = new Pinokiod(config)
const ENABLE_BROWSER_CONSOLE_LOG = process.env.PINOKIO_BROWSER_LOG === '1'
const browserConsoleState = new WeakMap()
const attachedConsoleListeners = new WeakSet()
const consoleLevelLabels = ['log', 'info', 'warn', 'error', 'debug']
let browserLogFilePath
let browserLogFileReady = false
let browserLogBuffer = []
let browserLogWritePromise = Promise.resolve()
let permissionHandlersInstalled = false
let injectorHandlersInstalled = false
const frameInjectorSyncState = new Map()
const frameInjectTargetRegistry = new Map()
const PINOKIO_INJECT_ISOLATED_WORLD_ID = 42000
const permissionPrompted = new Set()
const permissionPromptInFlight = new Set()
const safeParseUrl = (value, base) => {
  if (!value) {
    return null
  }
  try {
    if (base) {
      return new URL(value, base)
    }
    return new URL(value)
  } catch (err) {
    return null
  }
}
const popupNavigationGuards = new Map()
const isRootShellUrl = (value) => {
  const root = safeParseUrl(root_url)
  const target = safeParseUrl(value, root ? root.href : undefined)
  return Boolean(root && target && target.origin === root.origin && (target.pathname || '/') === '/')
}
const getHttpNavigationTarget = (value, base) => {
  const target = safeParseUrl(value, base)
  if (!target || (target.protocol !== 'http:' && target.protocol !== 'https:')) {
    return null
  }
  return target
}
const openNonPinokioNavigationInPopup = ({ event, owner, url, frame } = {}) => {
  const target = getHttpNavigationTarget(url, root_url || undefined)
  if (!target || !owner || owner.isDestroyed?.() || owner.__pinokioPopupShell) {
    return false
  }
  if (popupShellManager.isPinokioWindowUrl(target.href, root_url)) {
    return false
  }
  if (event && typeof event.preventDefault === 'function') {
    event.preventDefault()
  }
  const frameId = frame && (frame.frameToken || frame.frameTreeNodeId || frame.routingId)
  if (frameId) {
    const guardKey = `${owner.id}:${frameId}:${target.href}`
    const now = Date.now()
    const last = popupNavigationGuards.get(guardKey) || 0
    popupNavigationGuards.set(guardKey, now)
    setTimeout(() => {
      if (popupNavigationGuards.get(guardKey) === now) {
        popupNavigationGuards.delete(guardKey)
      }
    }, 1500)
    if (now - last < 1500) {
      return true
    }
  }
  popupShellManager.openExternalWindow({ url: target.href })
  return true
}
const installForceDestroyOnClose = (win) => {
  if (!win || win.__pinokioCloseHandlerInstalled) {
    return
  }
  win.__pinokioCloseHandlerInstalled = true
  win.once('close', (event) => {
    if (win.isDestroyed()) {
      return
    }
    event.preventDefault()
    win.destroy()
  })
}
const popupShellManager = createPopupShellManager({
  installForceDestroyOnClose
})
const installClosePopupOnDownload = (targetSession) => {
  if (!targetSession || targetSession.__pinokioClosePopupOnDownloadInstalled) {
    return
  }
  targetSession.__pinokioClosePopupOnDownloadInstalled = true
  targetSession.on('will-download', (_event, _item, webContents) => {
    if (!webContents || typeof webContents.getOwnerBrowserWindow !== 'function') {
      return
    }
    let owner = null
    try {
      owner = webContents.getOwnerBrowserWindow()
    } catch (_) {
      owner = null
    }
    if (!owner || owner.isDestroyed?.() || !owner.__pinokioCloseOnFirstDownload) {
      return
    }
    owner.__pinokioCloseOnFirstDownload = false
    setTimeout(() => {
      if (!owner.isDestroyed()) {
        owner.close()
      }
    }, 0)
  })
}
const resolveConsoleSourceUrl = (sourceId, pageUrl) => {
  const page = safeParseUrl(pageUrl)
  const source = safeParseUrl(sourceId, page ? page.href : undefined)
  if (source && (source.protocol === 'http:' || source.protocol === 'https:' || source.protocol === 'file:')) {
    return source.href
  }
  if (page) {
    return page.href
  }
  return null
}
const shouldLogUrl = (url) => {
  if (!ENABLE_BROWSER_CONSOLE_LOG) {
    return false
  }
  if (!url) {
    return false
  }
  const rootParsed = safeParseUrl(root_url)
  const target = safeParseUrl(url, rootParsed ? rootParsed.origin : undefined)
  if (!target) {
    return false
  }
  if (rootParsed) {
    if (target.origin !== rootParsed.origin) {
      return false
    }
    const normalizedTargetPath = (target.pathname || '').replace(/\/+$/, '')
    const normalizedRootPath = (rootParsed.pathname || '').replace(/\/+$/, '')
    if (normalizedTargetPath === normalizedRootPath) {
      return false
    }
  } else {
    const normalizedTargetPath = (target.pathname || '').replace(/\/+$/, '')
    if (!normalizedTargetPath) {
      return false
    }
  }
  return true
}
const getBrowserLogFile = () => {
  if (!ENABLE_BROWSER_CONSOLE_LOG) {
    return null
  }
  if (!browserLogFilePath) {
    if (!pinokiod || !pinokiod.kernel || !pinokiod.kernel.homedir) {
      return null
    }
    try {
      browserLogFilePath = pinokiod.kernel.path('logs/browser.log')
    } catch (err) {
      console.error('[BROWSER LOG] Failed to resolve browser log file path', err)
      return null
    }
  }
  return browserLogFilePath
}
const ensureBrowserLogFile = () => {
  if (!ENABLE_BROWSER_CONSOLE_LOG) {
    return null
  }
  const filePath = getBrowserLogFile()
  if (!filePath) {
    return null
  }
  if (browserLogFileReady) {
    return filePath
  }
  try {
    fs.mkdirSync(path.dirname(filePath), { recursive: true })
    if (fs.existsSync(filePath)) {
      try {
        const existingContent = fs.readFileSync(filePath, 'utf8')
        const existingLines = existingContent.split(/\r?\n/).filter((line) => line.length > 0)
        const filteredLines = []
        for (const line of existingLines) {
          const parts = line.split('\t')
          if (parts.length >= 2) {
            const urlPart = parts[1]
            if (!shouldLogUrl(urlPart)) {
              continue
            }
          }
          filteredLines.push(`${line}\n`)
          if (filteredLines.length > 100) {
            filteredLines.shift()
          }
        }
        browserLogBuffer = filteredLines
        fs.writeFileSync(filePath, browserLogBuffer.join(''))
      } catch (err) {
        console.error('[BROWSER LOG] Failed to prime existing browser log', err)
        browserLogBuffer = []
      }
    }
    browserLogFileReady = true
    return filePath
  } catch (err) {
    console.error('[BROWSER LOG] Failed to prepare browser log file', err)
    return null
  }
}
const titleBarOverlay = (colors) => {
  if (is_mac) {
    return false
  } else {
    return colors
  }
}
const getLogFileHint = () => {
  try {
    if (pinokiod && pinokiod.kernel && pinokiod.kernel.homedir) {
      return path.resolve(pinokiod.kernel.homedir, "logs", "stdout.txt")
    }
  } catch (err) {
  }
  return path.resolve(os.homedir(), ".pinokio", "logs", "stdout.txt")
}
const getSplashIcon = () => {
  if (splashIcon) {
    return splashIcon
  }
  const candidates = [
    path.join('assets', 'icon.png'),
    path.join('assets', 'icon_small@2x.png'),
    path.join('assets', 'icon_small.png'),
    'icon2.png'
  ]
  for (const relative of candidates) {
    const absolute = path.join(__dirname, relative)
    if (fs.existsSync(absolute)) {
      splashIcon = relative.split(path.sep).join('/')
      return splashIcon
    }
  }
  splashIcon = path.join('assets', 'icon_small.png').split(path.sep).join('/')
  return splashIcon
}
const ensureSplashWindow = () => {
  if (splashWindow && !splashWindow.isDestroyed()) {
    return splashWindow
  }
  splashWindow = new BrowserWindow({
    width: 420,
    height: 320,
    frame: false,
    resizable: false,
    transparent: true,
    show: false,
    alwaysOnTop: true,
    skipTaskbar: true,
    fullscreenable: false,
    webPreferences: {
      spellcheck: false,
      backgroundThrottling: false
    }
  })
  splashWindow.on('closed', () => {
    splashWindow = null
  })
  return splashWindow
}
const updateSplashWindow = ({ state = 'loading', message, detail, logPath, icon } = {}) => {
  const win = ensureSplashWindow()
  const query = { state }
  if (message) {
    query.message = message
  }
  if (detail) {
    const trimmed = detail.length > 800 ? `${detail.slice(0, 800)}…` : detail
    query.detail = trimmed
  }
  if (logPath) {
    query.log = logPath
  }
  if (icon) {
    query.icon = icon
  }
  win.loadFile(path.join(__dirname, 'splash.html'), { query }).finally(() => {
    if (!win.isDestroyed()) {
      win.show()
    }
  })
}
const closeSplashWindow = () => {
  if (splashWindow && !splashWindow.isDestroyed()) {
    splashWindow.close()
  }
}
const showStartupError = ({ message, detail, error } = {}) => {
  const formatted = detail || formatStartupError(error)
  updateSplashWindow({
    state: 'error',
    message: message || 'Pinokio could not start',
    detail: formatted,
    logPath: getLogFileHint(),
    icon: getSplashIcon()
  })
}
const formatStartupError = (error) => {
  if (!error) {
    return ''
  }
  if (error.stack) {
    return `${error.message || 'Unknown error'}\n\n${error.stack}`
  }
  if (error.message) {
    return error.message
  }
  if (typeof error === 'string') {
    return error
  }
  try {
    return JSON.stringify(error, null, 2)
  } catch (err) {
    return String(error)
  }
}
const SESSION_COOKIE_TTL_DAYS = 90
const SESSION_COOKIE_TTL_SEC = SESSION_COOKIE_TTL_DAYS * 24 * 60 * 60
const SESSION_COOKIE_JAR_FILENAME = 'session-cookies.json'
let sessionCookieSavePromise = null
let isQuitting = false
const getSessionCookieJarPath = () => path.join(app.getPath('userData'), SESSION_COOKIE_JAR_FILENAME)
const buildCookieUrl = (cookie) => {
  if (!cookie || !cookie.domain) {
    return null
  }
  const host = cookie.domain.startsWith('.') ? cookie.domain.slice(1) : cookie.domain
  if (!host) {
    return null
  }
  const scheme = cookie.secure ? 'https://' : 'http://'
  const cookiePath = cookie.path && cookie.path.startsWith('/') ? cookie.path : '/'
  return `${scheme}${host}${cookiePath}`
}
const serializeSessionCookie = (cookie) => {
  const url = buildCookieUrl(cookie)
  if (!url || typeof cookie.name !== 'string') {
    return null
  }
  const entry = {
    url,
    name: cookie.name,
    value: typeof cookie.value === 'string' ? cookie.value : '',
    path: cookie.path && cookie.path.startsWith('/') ? cookie.path : '/',
    secure: !!cookie.secure,
    httpOnly: !!cookie.httpOnly
  }
  if (cookie.hostOnly !== true && cookie.domain) {
    entry.domain = cookie.domain
  }
  if (cookie.sameSite) {
    entry.sameSite = cookie.sameSite
  }
  if (cookie.priority) {
    entry.priority = cookie.priority
  }
  if (cookie.sameParty != null) {
    entry.sameParty = cookie.sameParty
  }
  if (cookie.sourceScheme) {
    entry.sourceScheme = cookie.sourceScheme
  }
  if (Number.isInteger(cookie.sourcePort)) {
    entry.sourcePort = cookie.sourcePort
  }
  return entry
}
const persistSessionCookies = () => {
  if (sessionCookieSavePromise) {
    return sessionCookieSavePromise
  }
  sessionCookieSavePromise = (async () => {
    try {
      const cookies = await session.defaultSession.cookies.get({})
      const sessionCookies = cookies.filter((cookie) => cookie && cookie.session)
      const entries = sessionCookies.map(serializeSessionCookie).filter(Boolean)
      const jarPath = getSessionCookieJarPath()
      if (!entries.length) {
        await fs.promises.unlink(jarPath).catch((err) => {
          if (err && err.code !== 'ENOENT') {
            console.warn('[Session Cookies] Failed to remove jar', err)
          }
        })
        return
      }
      const payload = {
        version: 1,
        savedAt: Date.now(),
        cookies: entries
      }
      await fs.promises.mkdir(path.dirname(jarPath), { recursive: true }).catch(() => {})
      await fs.promises.writeFile(jarPath, JSON.stringify(payload), 'utf8')
    } catch (err) {
      console.warn('[Session Cookies] Failed to persist', err)
    } finally {
      sessionCookieSavePromise = null
    }
  })()
  return sessionCookieSavePromise
}
const restoreSessionCookies = async () => {
  const jarPath = getSessionCookieJarPath()
  let raw
  try {
    raw = await fs.promises.readFile(jarPath, 'utf8')
  } catch (err) {
    if (err && err.code !== 'ENOENT') {
      console.warn('[Session Cookies] Failed to read jar', err)
    }
    return
  }
  let data
  try {
    data = JSON.parse(raw)
  } catch (err) {
    console.warn('[Session Cookies] Failed to parse jar', err)
    return
  }
  const entries = Array.isArray(data.cookies) ? data.cookies : []
  if (!entries.length) {
    return
  }
  const expirationDate = Math.floor(Date.now() / 1000) + SESSION_COOKIE_TTL_SEC
  for (const entry of entries) {
    if (!entry || !entry.url || !entry.name) {
      continue
    }
    const details = {
      url: entry.url,
      name: entry.name,
      value: typeof entry.value === 'string' ? entry.value : '',
      path: entry.path || '/',
      secure: !!entry.secure,
      httpOnly: !!entry.httpOnly,
      expirationDate
    }
    if (entry.domain) {
      details.domain = entry.domain
    }
    if (entry.sameSite) {
      details.sameSite = entry.sameSite
    }
    if (entry.priority) {
      details.priority = entry.priority
    }
    if (entry.sameParty != null) {
      details.sameParty = entry.sameParty
    }
    if (entry.sourceScheme) {
      details.sourceScheme = entry.sourceScheme
    }
    if (Number.isInteger(entry.sourcePort)) {
      details.sourcePort = entry.sourcePort
    }
    try {
      await session.defaultSession.cookies.set(details)
    } catch (err) {
      console.warn('[Session Cookies] Failed to restore cookie', entry.name, err)
    }
  }
}
const clearPersistedSessionCookies = async () => {
  const jarPath = getSessionCookieJarPath()
  try {
    await fs.promises.unlink(jarPath)
  } catch (err) {
    if (err && err.code !== 'ENOENT') {
      console.warn('[Session Cookies] Failed to remove jar', err)
    }
  }
}
const clearSessionCaches = async () => {
  try {
    await session.defaultSession.clearCache()
  } catch (err) {
    console.warn('[Session Cache] Failed to clear http cache', err)
  }
  try {
    await session.defaultSession.clearStorageData({
      storages: ['serviceworkers', 'cachestorage']
    })
  } catch (err) {
    console.warn('[Session Cache] Failed to clear service worker/cache storage', err)
  }
}
function UpsertKeyValue(obj, keyToChange, value) {
  const keyToChangeLower = keyToChange.toLowerCase();
  for (const key of Object.keys(obj)) {
    if (key.toLowerCase() === keyToChangeLower) {
      // Reassign old key
      obj[key] = value;
      // Done
      return;
    }
  }
  // Insert at end instead
  obj[keyToChange] = value;
}

const clearBrowserConsoleState = (webContents) => {
  if (browserConsoleState.has(webContents)) {
    browserConsoleState.delete(webContents)
  }
}

const updateBrowserConsoleTarget = (webContents, url) => {
  if (!ENABLE_BROWSER_CONSOLE_LOG) {
    return
  }
  if (!root_url) {
    clearBrowserConsoleState(webContents)
    return
  }
  let parsed
  try {
    parsed = new URL(url)
  } catch (e) {
    clearBrowserConsoleState(webContents)
    return
  }
  if (parsed.origin !== root_url) {
    clearBrowserConsoleState(webContents)
    return
  }
  const existing = browserConsoleState.get(webContents)
  if (existing && existing.url === parsed.href) {
    return
  }
  browserConsoleState.set(webContents, { url: parsed.href })
}

const inspectorSessions = new Map()
let inspectorHandlersInstalled = false

const inspectorLogFile = path.join(os.tmpdir(), 'pinokio-inspector.log')

const inspectorMainLog = (label, payload) => {
  try {
    const serialized = payload === undefined ? '' : ' ' + JSON.stringify(payload)
    const line = `[InspectorMain] ${label}${serialized}\n`
    try {
      fs.appendFileSync(inspectorLogFile, line)
    } catch (_) {}
    process.stdout.write(line)
  } catch (_) {
    try {
      fs.appendFileSync(inspectorLogFile, `[InspectorMain] ${label}\n`)
    } catch (_) {}
    process.stdout.write(`[InspectorMain] ${label}\n`)
  }
}

const normalizeInspectorUrl = (value) => {
  if (!value) {
    return null
  }
  try {
    return new URL(value).href
  } catch (_) {
    return value
  }
}

const urlsRoughlyMatch = (expected, candidate) => {
  if (!expected) {
    return true
  }
  if (!candidate) {
    return false
  }
  if (candidate === expected) {
    return true
  }
  return candidate.startsWith(expected) || expected.startsWith(candidate)
}

const flattenFrameTree = (frame, acc = [], depth = 0) => {
  if (!frame) {
    return acc
  }
  let frameName = null
  try {
    frameName = typeof frame.name === 'string' && frame.name.length ? frame.name : null
  } catch (_) {
    frameName = null
  }
  acc.push({ frame, depth, url: normalizeInspectorUrl(frame.url || ''), name: frameName })
  const children = Array.isArray(frame.frames) ? frame.frames : []
  for (const child of children) {
    flattenFrameTree(child, acc, depth + 1)
  }
  return acc
}

const findDescendantByUrl = (frame, targetUrl) => {
  if (!frame || !targetUrl) {
    return null
  }
  const normalizedTarget = normalizeInspectorUrl(targetUrl)
  if (!normalizedTarget) {
    return null
  }
  const stack = [frame]
  while (stack.length) {
    const current = stack.pop()
    try {
      const currentUrl = normalizeInspectorUrl(current.url || '')
      if (currentUrl && urlsRoughlyMatch(normalizedTarget, currentUrl)) {
        return current
      }
    } catch (_) {}
    const children = Array.isArray(current.frames) ? current.frames : []
    for (const child of children) {
      if (child) {
        stack.push(child)
      }
    }
  }
  return null
}

const selectTargetFrame = (webContents, payload = {}) => {
  if (!webContents || !webContents.mainFrame) {
    inspectorMainLog('no-webcontents', {})
    return null
  }
  const frames = flattenFrameTree(webContents.mainFrame, [])
  if (!frames.length) {
    inspectorMainLog('no-frames', { webContentsId: webContents.id })
    return null
  }
  inspectorMainLog('incoming', {
    frameUrl: payload.frameUrl || null,
    frameName: payload.frameName || null,
    frameNodeId: payload.frameNodeId || null,
    frameCount: frames.length,
  })

  const canonicalUrl = normalizeInspectorUrl(payload.frameUrl)
  const relativeOrdinal = typeof payload.candidateRelativeOrdinal === 'number' ? payload.candidateRelativeOrdinal : null
  const globalOrdinal = typeof payload.frameIndex === 'number' ? payload.frameIndex : null
  const canonicalFrameName = typeof payload.frameName === 'string' && payload.frameName.trim() ? payload.frameName.trim() : null
  const canonicalFrameNodeId = typeof payload.frameNodeId === 'string' && payload.frameNodeId.trim() ? payload.frameNodeId.trim() : null

  if (canonicalFrameName || canonicalFrameNodeId) {
    inspectorMainLog('identifier-search', {
      frameName: canonicalFrameName || null,
      frameNodeId: canonicalFrameNodeId || null,
      names: frames.map((entry) => entry.name || null).slice(0, 12),
    })

    let identifierMatch = null
    if (canonicalFrameNodeId) {
      identifierMatch = frames.find((entry) => entry && entry.name === canonicalFrameNodeId) || null
      if (identifierMatch) {
        const normalizedUrl = normalizeInspectorUrl(identifierMatch.url || '')
        if (canonicalUrl && (!normalizedUrl || !urlsRoughlyMatch(canonicalUrl, normalizedUrl))) {
          const descendant = findDescendantByUrl(identifierMatch.frame, canonicalUrl)
          if (descendant) {
            inspectorMainLog('identifier-match-node-descendant', {
              index: frames.indexOf(identifierMatch),
              name: identifierMatch.name || null,
              url: identifierMatch.url || null,
              descendantUrl: normalizeInspectorUrl(descendant.url || ''),
            })
            return descendant
          }
        }
        inspectorMainLog('identifier-match-node', {
          index: frames.indexOf(identifierMatch),
          name: identifierMatch.name || null,
          url: identifierMatch.url || null,
        })
        return identifierMatch.frame
      }
    }

    if (canonicalFrameName) {
      identifierMatch = frames.find((entry) => entry && entry.name === canonicalFrameName) || null
      if (identifierMatch) {
        const normalizedUrl = normalizeInspectorUrl(identifierMatch.url || '')
        if (canonicalUrl && (!normalizedUrl || !urlsRoughlyMatch(canonicalUrl, normalizedUrl))) {
          const descendant = findDescendantByUrl(identifierMatch.frame, canonicalUrl)
          if (descendant) {
            inspectorMainLog('identifier-match-name-descendant', {
              index: frames.indexOf(identifierMatch),
              name: identifierMatch.name || null,
              url: identifierMatch.url || null,
              descendantUrl: normalizeInspectorUrl(descendant.url || ''),
            })
            return descendant
          }
        }
        inspectorMainLog('identifier-match-name', {
          index: frames.indexOf(identifierMatch),
          name: identifierMatch.name || null,
          url: identifierMatch.url || null,
        })
        return identifierMatch.frame
      }
    }

    inspectorMainLog('identifier-miss', {})
  }

  let matches = frames
  if (canonicalUrl) {
    matches = frames.filter(({ url }) => urlsRoughlyMatch(canonicalUrl, url))
  }

  if (matches.length) {
    if (relativeOrdinal !== null) {
      const filtered = matches.slice().sort((a, b) => a.depth - b.depth || frames.indexOf(a) - frames.indexOf(b))
      const targetEntry = filtered[Math.min(Math.max(relativeOrdinal, 0), filtered.length - 1)]
      if (targetEntry) {
        inspectorMainLog('relative-ordinal-match', {
          index: frames.indexOf(targetEntry),
          name: targetEntry.name || null,
          url: targetEntry.url || null,
        })
        return targetEntry.frame
      }
    }
    const fallbackEntry = matches[0]
    if (fallbackEntry) {
      inspectorMainLog('fallback-match', {
        index: frames.indexOf(fallbackEntry),
        name: fallbackEntry.name || null,
        url: fallbackEntry.url || null,
      })
      return fallbackEntry.frame
    }
  }

  if (globalOrdinal !== null && frames[globalOrdinal]) {
    inspectorMainLog('global-ordinal-match', {
      index: globalOrdinal,
      name: frames[globalOrdinal].name || null,
      url: frames[globalOrdinal].url || null,
    })
    return frames[globalOrdinal].frame
  }

  inspectorMainLog('default-match', {
    name: frames[0]?.name || null,
    url: frames[0]?.url || null,
  })

  return frames[0]?.frame || null
}

const buildInspectorInjection = () => {
  const source = function () {
    try {
      if (window.__PINOKIO_INSPECTOR__ && typeof window.__PINOKIO_INSPECTOR__.stop === 'function') {
        window.__PINOKIO_INSPECTOR__.stop()
      }

      const overlay = document.createElement('div')
      overlay.style.position = 'fixed'
      overlay.style.pointerEvents = 'none'
      overlay.style.border = '2px solid rgba(77,163,255,0.9)'
      overlay.style.background = 'rgba(77,163,255,0.2)'
      overlay.style.boxShadow = '0 0 0 1px rgba(23,52,92,0.45)'
      overlay.style.zIndex = '2147483647'
      overlay.style.display = 'none'
      document.documentElement.appendChild(overlay)

      let active = true

      const post = (type, payload) => {
        try {
          window.parent.postMessage({ pinokioInspector: { type, frameUrl: window.location.href, ...payload } }, '*')
        } catch (err) {
          // ignore
        }
      }

      const updateBox = (target) => {
        if (!active || !target) {
          overlay.style.display = 'none'
          return
        }
        const rect = target.getBoundingClientRect()
        if (!rect || rect.width <= 0 || rect.height <= 0) {
          overlay.style.display = 'none'
          return
        }
        overlay.style.display = 'block'
        overlay.style.left = `${rect.left}px`
        overlay.style.top = `${rect.top}px`
        overlay.style.width = `${rect.width}px`
        overlay.style.height = `${rect.height}px`
      }

      const buildPathKeys = (node) => {
        if (!node) {
          return []
        }
        const keys = []
        let current = node
        let depth = 0
        while (current && current.nodeType === Node.ELEMENT_NODE && depth < 8) {
          const tag = current.tagName ? current.tagName.toLowerCase() : 'element'
          let descriptor = tag
          if (current.id) {
            descriptor += `#${current.id}`
          } else if (current.classList && current.classList.length) {
            descriptor += `.${Array.from(current.classList).slice(0, 2).join('.')}`
          }
          keys.push(descriptor)
          current = current.parentElement
          depth += 1
        }
        return keys.reverse()
      }

      const handleMove = (event) => {
        if (!active) {
          return
        }
        const target = event.target
        updateBox(target)
        post('update', {
          nodeName: target && target.tagName ? target.tagName.toLowerCase() : '',
          pathKeys: buildPathKeys(target),
        })
      }

      const preventClick = (event) => {
        if (!active) {
          return
        }
        event.preventDefault()
        event.stopPropagation()
      }

      const handleClick = async (event) => {
        if (!active) {
          return
        }
        event.preventDefault()
        event.stopPropagation()
        
        const target = event.target
        const html = target && target.outerHTML ? target.outerHTML : ''
        let screenshot = null
        
        // Hide the overlay before taking screenshot to avoid capturing it
        if (overlay && overlay.style) {
          overlay.style.display = 'none'
        }
        
        // Small delay to ensure overlay is hidden before screenshot
        await new Promise(resolve => setTimeout(resolve, 50))
        
        try {
          // Use html2canvas-like approach to capture actual element rendering
          const rect = target.getBoundingClientRect()
          
          // Send element bounds for screenshot capture
          const screenshotRequest = {
            type: 'screenshot',
            bounds: {
              x: Math.round(rect.left),
              y: Math.round(rect.top),
              width: Math.max(1, Math.round(rect.width)),
              height: Math.max(1, Math.round(rect.height))
            },
            devicePixelRatio: window.devicePixelRatio || 1,
            frameUrl: window.location.href,
            __pinokioRelayStage: 0,
            __pinokioRelayComplete: window === window.top
          }
          
          // Post screenshot request via postMessage to main page
          try {
            console.log('Attempting screenshot capture...')
            console.log('electronAPI available in iframe:', !!window.electronAPI)
            console.log('Screenshot request:', screenshotRequest)
            
            // Send screenshot request to parent page via postMessage
            const response = await new Promise((resolve, reject) => {
              const messageId = 'screenshot_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
              
              const handleResponse = (event) => {
                if (event.data && event.data.pinokioScreenshotResponse && event.data.messageId === messageId) {
                  window.removeEventListener('message', handleResponse)
                  if (event.data.success) {
                    resolve(event.data.screenshot)
                  } else {
                    reject(new Error(event.data.error || 'Screenshot failed'))
                  }
                }
              }
              
              window.addEventListener('message', handleResponse)
              
              // Send request to parent page
              window.parent.postMessage({
                pinokioScreenshotRequest: screenshotRequest,
                messageId: messageId
              }, '*')
              
              // Timeout after 3 seconds
              setTimeout(() => {
                window.removeEventListener('message', handleResponse)
                reject(new Error('Screenshot timeout'))
              }, 3000)
            })
            
            screenshot = response
            console.log('Screenshot captured successfully via parent page')
          } catch (screenshotError) {
            console.error('Screenshot capture failed:', screenshotError)
            screenshot = null
          }
        } catch (error) {
          console.warn('Screenshot capture failed:', error)
          screenshot = null
        }
        
        post('complete', {
          outerHTML: html,
          pathKeys: buildPathKeys(target),
          screenshot: screenshot
        })
        stop()
      }

      const handleKey = (event) => {
        if (!active) {
          return
        }
        if (event.key === 'Escape') {
          post('cancelled', {})
          stop()
        }
      }

      const stop = () => {
        if (!active) {
          return
        }
        active = false
        document.removeEventListener('mousemove', handleMove, true)
        document.removeEventListener('mouseover', handleMove, true)
        document.removeEventListener('mousedown', preventClick, true)
        document.removeEventListener('click', handleClick, true)
        window.removeEventListener('keydown', handleKey, true)
        if (overlay.parentNode) {
          overlay.parentNode.removeChild(overlay)
        }
        window.__PINOKIO_INSPECTOR__ = null
      }

      document.addEventListener('mousemove', handleMove, true)
      document.addEventListener('mouseover', handleMove, true)
      document.addEventListener('mousedown', preventClick, true)
      document.addEventListener('click', handleClick, true)
      window.addEventListener('keydown', handleKey, true)

      window.__PINOKIO_INSPECTOR__ = {
        stop,
      }

      post('started', {})
    } catch (error) {
      try {
        window.parent.postMessage({ pinokioInspector: { type: 'error', frameUrl: window.location.href, message: error && error.message ? error.message : String(error) } }, '*')
      } catch (_) {}
    }
  }
  return `(${source.toString()})();`
}

const buildScreenshotRelayInjection = () => {
  const source = function () {
    try {
      if (window.__PINOKIO_SCREENSHOT_RELAY__) {
        return
      }
      window.__PINOKIO_SCREENSHOT_RELAY__ = true

      const pending = new Map()
      const EXPIRATION_MS = 5000

      const rememberSource = (messageId, sourceWindow) => {
        if (!messageId || !sourceWindow) {
          return
        }
        pending.set(messageId, sourceWindow)
        setTimeout(() => {
          pending.delete(messageId)
        }, EXPIRATION_MS)
      }

      const safeStringify = (value) => {
        try {
          return JSON.stringify(value)
        } catch (_) {
          return '"[unserializable]"'
        }
      }

      const log = (label, payload) => {
        try {
          console.log('[Pinokio Screenshot Relay] ' + label + ' ' + safeStringify(payload))
        } catch (_) {
          // ignore logging failures
        }
      }

      log('relay-installed', { href: window.location.href })

      window.addEventListener('message', (event) => {
        const data = event && event.data
        log('message-event', {
          href: window.location.href,
          hasData: Boolean(data),
          messageId: data && data.messageId ? data.messageId : null,
          hasRequest: Boolean(data && data.pinokioScreenshotRequest),
          hasResponse: Boolean(data && data.pinokioScreenshotResponse)
        })
        if (!data) {
          return
        }

        if (data.pinokioScreenshotRequest) {
          if (!event.source || event.source === window) {
            log('request-ignored-no-source', {
              href: window.location.href,
              messageId: data.messageId || null
            })
            return
          }

          rememberSource(data.messageId, event.source)
          log('request-processing', {
            href: window.location.href,
            messageId: data.messageId || null,
            originalBounds: data.pinokioScreenshotRequest && data.pinokioScreenshotRequest.bounds ? data.pinokioScreenshotRequest.bounds : null,
            originalDevicePixelRatio: data.pinokioScreenshotRequest ? data.pinokioScreenshotRequest.devicePixelRatio : null
          })

          let offsetX = 0
          let offsetY = 0
          let matchedFrame = false
          try {
            for (let index = 0; index < window.frames.length; index += 1) {
              const childWindow = window.frames[index]
              if (childWindow === event.source) {
                log('matching-window-frames', {
                  href: window.location.href,
                  messageId: data.messageId || null,
                  frameIndex: index
                })
                try {
                  const frameElement = childWindow.frameElement
                  if (frameElement) {
                    const rect = frameElement.getBoundingClientRect()
                    offsetX = rect ? rect.left || 0 : 0
                    offsetY = rect ? rect.top || 0 : 0
                    matchedFrame = true
                    log('matched-window-frames', {
                      href: window.location.href,
                      messageId: data.messageId || null,
                      frameIndex: index,
                      rect: rect ? { left: rect.left, top: rect.top, width: rect.width, height: rect.height } : null
                    })
                    break
                  }
                } catch (error) {
                  log('frame-element-access-error', {
                    href: window.location.href,
                    messageId: data.messageId || null,
                    frameIndex: index,
                    error: error && error.message ? error.message : String(error)
                  })
                }
              }
            }

            if (!matchedFrame) {
              const FRAME_SELECTOR = 'iframe, frame'
              const frames = document.querySelectorAll ? document.querySelectorAll(FRAME_SELECTOR) : []
              log('matching-query-selector', {
                href: window.location.href,
                messageId: data.messageId || null,
                selector: FRAME_SELECTOR,
                count: frames ? frames.length : 0
              })
              for (const frameEl of frames) {
                if (!frameEl) {
                  continue
                }
                try {
                  if (frameEl.contentWindow === event.source) {
                    const rect = frameEl.getBoundingClientRect()
                    offsetX = rect ? rect.left || 0 : 0
                    offsetY = rect ? rect.top || 0 : 0
                    matchedFrame = true
                    log('matched-query-selector', {
                      href: window.location.href,
                      messageId: data.messageId || null,
                      selector: FRAME_SELECTOR,
                      rect: rect ? { left: rect.left, top: rect.top, width: rect.width, height: rect.height } : null
                    })
                    break
                  }
                } catch (error) {
                  log('query-selector-access-error', {
                    href: window.location.href,
                    messageId: data.messageId || null,
                    selector: FRAME_SELECTOR,
                    error: error && error.message ? error.message : String(error)
                  })
                }
              }
            }
          } catch (error) {
            log('frame-enumeration-error', {
              href: window.location.href,
              messageId: data.messageId || null,
              error: error && error.message ? error.message : String(error)
            })
          }

          if (!matchedFrame) {
            log('frame-match-failed', {
              href: window.location.href,
              messageId: data.messageId || null,
              offsetX,
              offsetY
            })
          }

          const request = data.pinokioScreenshotRequest || {}
          const originalBounds = request.bounds || {}
          const parentDpr = window.devicePixelRatio || 1
          const currentDpr = request.devicePixelRatio && request.devicePixelRatio > 0 ? request.devicePixelRatio : 1
          const nextStage = (typeof request.__pinokioRelayStage === 'number' ? request.__pinokioRelayStage : 0) + 1
          request.__pinokioRelayStage = nextStage
          request.__pinokioRelayComplete = window.parent === window

          if (matchedFrame) {
            const adjustedBounds = {
              x: (originalBounds.x || 0) + offsetX,
              y: (originalBounds.y || 0) + offsetY,
              width: originalBounds.width || 0,
              height: originalBounds.height || 0,
            }

            request.bounds = adjustedBounds
            request.devicePixelRatio = Math.max(currentDpr, parentDpr)
            request.__pinokioAdjusted = true

            log('request-adjusted', {
              href: window.location.href,
              messageId: data.messageId || null,
              offsetX,
              offsetY,
              parentDpr,
              resultingBounds: adjustedBounds,
              originalBounds,
              resultingDevicePixelRatio: request.devicePixelRatio,
              relayStage: request.__pinokioRelayStage,
              relayComplete: request.__pinokioRelayComplete
            })
          } else {
            log('request-forward-unadjusted', {
              href: window.location.href,
              messageId: data.messageId || null,
              relayStage: request.__pinokioRelayStage,
              relayComplete: request.__pinokioRelayComplete
            })
          }

          data.pinokioScreenshotRequest = request

          log('request-forward', {
            href: window.location.href,
            messageId: data.messageId || null,
            matchedFrame,
            hasParent: Boolean(window.parent && window.parent !== window)
          })

          if (window.parent && window.parent !== window) {
            window.parent.postMessage(data, '*')
            if (event && typeof event.stopImmediatePropagation === 'function') {
              event.stopImmediatePropagation()
            }
            return
          }

          const targetSource = event.source
          const messageId = data.messageId
          const captureRequest = data.pinokioScreenshotRequest
          log('top-level-capture', {
            href: window.location.href,
            messageId,
            relayStage: captureRequest.__pinokioRelayStage,
            relayComplete: captureRequest.__pinokioRelayComplete,
            adjustedFlag: captureRequest.__pinokioAdjusted,
            bounds: captureRequest.bounds || null
          })

          const captureApi = window.electronAPI && typeof window.electronAPI.captureScreenshot === 'function'
            ? window.electronAPI.captureScreenshot
            : null

          if (!captureApi) {
            log('top-level-capture-missing-api', { href: window.location.href })
            return
          }

          Promise.resolve()
            .then(() => captureApi(captureRequest))
            .then((screenshot) => {
              log('top-level-capture-success', { href: window.location.href, messageId })
              try {
                targetSource.postMessage({
                  pinokioScreenshotResponse: true,
                  messageId,
                  success: true,
                  screenshot
                }, '*')
              } catch (error) {
                log('top-level-response-error', {
                  href: window.location.href,
                  messageId,
                  error: error && error.message ? error.message : String(error)
                })
              }
            })
            .catch((error) => {
              log('top-level-capture-error', {
                href: window.location.href,
                messageId,
                error: error && error.message ? error.message : String(error)
              })
              try {
                targetSource.postMessage({
                  pinokioScreenshotResponse: true,
                  messageId,
                  success: false,
                  error: error && error.message ? error.message : String(error)
                }, '*')
              } catch (responseError) {
                log('top-level-response-error', {
                  href: window.location.href,
                  messageId,
                  error: responseError && responseError.message ? responseError.message : String(responseError)
                })
              }
            })
          return
        }

        if (data.pinokioScreenshotResponse && data.messageId) {
          log('response-processing', {
            href: window.location.href,
            messageId: data.messageId
          })
          const target = pending.get(data.messageId)
          if (target && target !== event.source) {
            pending.delete(data.messageId)
            try {
              log('response-forwarding-down', {
                href: window.location.href,
                messageId: data.messageId
              })
              target.postMessage(data, '*')
              return
            } catch (error) {
              log('response-forwarding-error', {
                href: window.location.href,
                messageId: data.messageId,
                error: error && error.message ? error.message : String(error)
              })
            }
          }

          log('response-forwarding-up', {
            href: window.location.href,
            messageId: data.messageId,
            hasParent: Boolean(window.parent && window.parent !== window)
          })
          if (window.parent && window.parent !== window) {
            window.parent.postMessage(data, '*')
          }
        }
      }, true)
    } catch (error) {
      try {
        console.warn('[Pinokio Screenshot Relay] relay-install-error ' + (error && error.message ? error.message : String(error)))
      } catch (_) {
        // ignore logging failures
      }
    }
  }
  return `(${source.toString()})();`
}

const installScreenshotRelays = async (frame) => {
  if (!frame) {
    return
  }

  const topFrame = frame.top || frame
  const frames = flattenFrameTree(topFrame, [])
  for (const entry of frames) {
    const candidate = entry && entry.frame
    if (!candidate || candidate.isDestroyed && candidate.isDestroyed()) {
      continue
    }
    try {
      await candidate.executeJavaScript(buildScreenshotRelayInjection(), true)
    } catch (error) {
      console.warn('Screenshot relay injection failed:', error && error.message ? error.message : error)
    }
  }
}

const startInspectorSession = async (webContents, payload = {}) => {
  const existing = inspectorSessions.get(webContents.id)
  if (existing) {
    await stopInspectorSession(webContents)
  }

  const targetFrame = selectTargetFrame(webContents, payload)
  if (!targetFrame) {
    throw new Error('Unable to locate iframe to inspect.')
  }

  await installScreenshotRelays(targetFrame)
  await targetFrame.executeJavaScript(buildInspectorInjection(), true)


  const navigationHandler = () => {
    const resultPromise = stopInspectorSession(webContents)
    Promise.resolve(resultPromise).then((outcome) => {
      if (!webContents.isDestroyed()) {
        webContents.send('pinokio:inspector-cancelled', { frameUrl: (outcome && outcome.frameUrl) || targetFrame.url || payload.frameUrl || '' })
      }
    })
  }

  if (!webContents.isDestroyed()) {
    webContents.on('did-navigate', navigationHandler)
    webContents.on('did-navigate-in-page', navigationHandler)
  }

  inspectorSessions.set(webContents.id, {
    frame: targetFrame,
    navigationHandler,
  })

  return {
    frameUrl: targetFrame.url || payload.frameUrl || '',
  }
}

const stopInspectorSession = async (webContents) => {
  const session = inspectorSessions.get(webContents.id)
  if (!session) {
    return { frameUrl: '' }
  }
  inspectorSessions.delete(webContents.id)
  if (session.navigationHandler && !webContents.isDestroyed()) {
    webContents.removeListener('did-navigate', session.navigationHandler)
    webContents.removeListener('did-navigate-in-page', session.navigationHandler)
  }
  const frameUrl = session.frame && session.frame.url ? session.frame.url : ''
  try {
    await session.frame.executeJavaScript('window.__PINOKIO_INSPECTOR__ && window.__PINOKIO_INSPECTOR__.stop()', true)
  } catch (_) {}
  return { frameUrl }
}

const safeCaptureStringify = (value) => {
  try {
    return JSON.stringify(value)
  } catch (_) {
    return '"[unserializable]"'
  }
}

const captureLog = (label, payload) => {
  try {
    console.log('[Pinokio Capture] ' + label + ' ' + safeCaptureStringify(payload))
  } catch (_) {
    console.log('[Pinokio Capture] ' + label)
  }
}

const installInspectorHandlers = () => {
  console.log('Installing inspector handlers...')
  if (inspectorHandlersInstalled) {
    console.log('Inspector handlers already installed, skipping')
    return
  }
  inspectorHandlersInstalled = true
  console.log('Installing pinokio:capture-screenshot handler')

  ipcMain.handle('pinokio:start-inspector', async (event, payload = {}) => {
    try {
      const result = await startInspectorSession(event.sender, payload)
      event.sender.send('pinokio:inspector-started', { frameUrl: result.frameUrl })
      return { ok: true }
    } catch (error) {
      const message = error && error.message ? error.message : 'Unable to start inspect mode.'
      event.sender.send('pinokio:inspector-error', { message })
      throw new Error(message)
    }
  })

  ipcMain.handle('pinokio:stop-inspector', async (event) => {
    try {
      const result = await stopInspectorSession(event.sender)
      event.sender.send('pinokio:inspector-cancelled', { frameUrl: result.frameUrl || '' })
      return { ok: true }
    } catch (error) {
      const message = error && error.message ? error.message : 'Unable to stop inspect mode.'
      event.sender.send('pinokio:inspector-error', { message })
      throw new Error(message)
    }
  })

  ipcMain.handle('pinokio:capture-screenshot-debug', async (event, payload) => {
    const { screenshotRequest } = payload

    const emitDebug = (label, data) => {
      captureLog(label, data)
      try {
        event.sender.send('pinokio:capture-debug-log', {
          label,
          payload: data
        })
      } catch (_) {
        // ignore renderer emit errors
      }
    }

    emitDebug('handler-invoked', {
      senderId: event && event.sender ? event.sender.id : null,
      hasRequest: Boolean(screenshotRequest),
      bounds: screenshotRequest && screenshotRequest.bounds ? {
        x: screenshotRequest.bounds.x,
        y: screenshotRequest.bounds.y,
        width: screenshotRequest.bounds.width,
        height: screenshotRequest.bounds.height,
      } : null,
      devicePixelRatio: screenshotRequest ? screenshotRequest.devicePixelRatio : null,
      adjustedFlag: Boolean(screenshotRequest && screenshotRequest.__pinokioAdjusted),
      relayStage: screenshotRequest && typeof screenshotRequest.__pinokioRelayStage !== 'undefined' ? screenshotRequest.__pinokioRelayStage : null,
      relayComplete: screenshotRequest && typeof screenshotRequest.__pinokioRelayComplete !== 'undefined' ? screenshotRequest.__pinokioRelayComplete : null,
      frameOffset: screenshotRequest && screenshotRequest.frameOffset ? {
        x: screenshotRequest.frameOffset.x,
        y: screenshotRequest.frameOffset.y,
      } : null
    })
    if (!screenshotRequest || !screenshotRequest.bounds) {
      throw new Error('Invalid screenshot request')
    }
    
    // Get the inspector session to access the target frame
    const session = inspectorSessions.get(event.sender.id)
    if (!session || !session.frame) {
      throw new Error('No inspector session or frame found')
    }
    
    try {
      const bounds = screenshotRequest.bounds
      const dpr = screenshotRequest.devicePixelRatio || 1
      const alreadyAdjusted = Boolean(screenshotRequest.__pinokioAdjusted)

      emitDebug('incoming-bounds', {
        senderId: event && event.sender ? event.sender.id : null,
        bounds,
        devicePixelRatio: dpr,
        alreadyAdjusted,
        relayStage: screenshotRequest && typeof screenshotRequest.__pinokioRelayStage !== 'undefined' ? screenshotRequest.__pinokioRelayStage : null,
        relayComplete: screenshotRequest && typeof screenshotRequest.__pinokioRelayComplete !== 'undefined' ? screenshotRequest.__pinokioRelayComplete : null
      })

      let framePosition = { x: 0, y: 0 }

      if (!alreadyAdjusted) {
        try {
          framePosition = await session.frame.executeJavaScript(`
            (function() {
              let x = 0, y = 0;
              let currentWindow = window;

              while (currentWindow !== window.top) {
                try {
                  const frameElement = currentWindow.frameElement;
                  if (frameElement) {
                    const rect = frameElement.getBoundingClientRect();
                    x += rect.left;
                    y += rect.top;
                  }
                } catch (error) {
                  return { x, y, crossOriginBlocked: true };
                }
                currentWindow = currentWindow.parent;
              }

              return { x, y };
            })();
          `)
          if (framePosition && framePosition.crossOriginBlocked) {
            framePosition = { x: framePosition.x || 0, y: framePosition.y || 0 }
          }
        } catch (error) {
          console.warn('Unable to determine frame offset via DOM script:', error)
          framePosition = { x: 0, y: 0 }
          emitDebug('frame-position-fallback', {
            senderId: event && event.sender ? event.sender.id : null,
            error: error && error.message ? error.message : String(error)
          })
        }
      }

      emitDebug('frame-position-computed', {
        senderId: event && event.sender ? event.sender.id : null,
        alreadyAdjusted,
        framePosition,
        bounds,
        devicePixelRatio: dpr,
        relayStage: screenshotRequest && typeof screenshotRequest.__pinokioRelayStage !== 'undefined' ? screenshotRequest.__pinokioRelayStage : null,
        relayComplete: screenshotRequest && typeof screenshotRequest.__pinokioRelayComplete !== 'undefined' ? screenshotRequest.__pinokioRelayComplete : null
      })
      
      // Capture full page and crop to element bounds
      const fullImage = await event.sender.capturePage()
      const fullSize = fullImage.getSize()
      emitDebug('capture-page-size', {
        senderId: event && event.sender ? event.sender.id : null,
        fullSize
      })
      
      // Calculate crop bounds with frame position and device pixel ratio
      const cropBounds = {
        x: Math.round((bounds.x + framePosition.x) * dpr),
        y: Math.round((bounds.y + framePosition.y) * dpr),  
        width: Math.round(bounds.width * dpr),
        height: Math.round(bounds.height * dpr)
      }
      
      // Validate crop bounds
      cropBounds.x = Math.max(0, Math.min(cropBounds.x, fullSize.width - 1))
      cropBounds.y = Math.max(0, Math.min(cropBounds.y, fullSize.height - 1))
      cropBounds.width = Math.min(cropBounds.width, fullSize.width - cropBounds.x)
      cropBounds.height = Math.min(cropBounds.height, fullSize.height - cropBounds.y)
      emitDebug('crop-bounds', {
        senderId: event && event.sender ? event.sender.id : null,
        framePosition,
        dpr,
        validatedCropBounds: cropBounds,
        fullSize
      })
      
      const croppedImage = fullImage.crop(cropBounds)
      const buffer = croppedImage.toPNG()
      emitDebug('capture-success', {
        senderId: event && event.sender ? event.sender.id : null,
        cropWidth: cropBounds.width,
        cropHeight: cropBounds.height
      })
      
      return 'data:image/png;base64,' + buffer.toString('base64')
    } catch (error) {
      console.error('Screenshot capture failed:', error)
      emitDebug('capture-error', {
        senderId: event && event.sender ? event.sender.id : null,
        error: error && error.message ? error.message : String(error)
      })
      throw error
    }
  })
}

const getFrameInjectorKey = (frame) => {
  if (!frame) {
    return ''
  }
  if (typeof frame.frameTreeNodeId === 'number') {
    return `frame:${frame.frameTreeNodeId}`
  }
  const processId = typeof frame.processId === 'number' ? frame.processId : 'unknown'
  const token = typeof frame.frameToken === 'string' && frame.frameToken
    ? frame.frameToken
    : String(typeof frame.routingId === 'number' ? frame.routingId : 'unknown')
  return `${processId}:${token}`
}

const getPinokioInjectWebContentsKey = (sender, frame = null) => {
  if (sender && typeof sender.id === 'number') {
    return `wc:${sender.id}`
  }
  if (frame && frame.hostWebContents && typeof frame.hostWebContents.id === 'number') {
    return `wc:${frame.hostWebContents.id}`
  }
  return ''
}

const serializeForJavaScript = (value) => JSON.stringify(value)
  .replace(/\u2028/g, '\\u2028')
  .replace(/\u2029/g, '\\u2029')

const normalizePinokioInjectDescriptor = (descriptor) => {
  if (!descriptor || typeof descriptor !== 'object' || Array.isArray(descriptor)) {
    return null
  }
  const src = typeof descriptor.src === 'string' ? descriptor.src.trim() : ''
  if (!src) {
    return null
  }
  const match = Array.isArray(descriptor.match) && descriptor.match.length
    ? descriptor.match.filter((item) => typeof item === 'string' && item.trim())
    : ['*']
  const world = typeof descriptor.world === 'string' && descriptor.world.trim().toLowerCase() === 'isolated'
    ? 'isolated'
    : 'main'
  const whenValue = typeof descriptor.when === 'string' ? descriptor.when.trim().toLowerCase() : ''
  const when = (whenValue === 'start' || whenValue === 'end') ? whenValue : 'idle'
  const frameValue = typeof descriptor.frame === 'string' ? descriptor.frame.trim().toLowerCase() : ''
  const frame = frameValue === 'all' ? 'all' : 'self'
  return {
    src,
    match,
    world,
    when,
    frame
  }
}

const normalizePinokioInjectTargetRegistrations = (targets) => {
  const values = Array.isArray(targets) ? targets : []
  const normalized = []
  for (const target of values) {
    if (!target || typeof target !== 'object' || Array.isArray(target)) {
      continue
    }
    const name = typeof target.name === 'string' ? target.name.trim() : ''
    const src = normalizeInspectorUrl(typeof target.src === 'string' ? target.src.trim() : '')
    if (!name && !src) {
      continue
    }
    normalized.push({
      name,
      src,
      inject: Array.isArray(target.inject)
        ? target.inject.map((entry) => normalizePinokioInjectDescriptor(entry)).filter(Boolean)
        : []
    })
  }
  return normalized
}

const findFramePath = (frame, target, trail = []) => {
  if (!frame || !target) {
    return null
  }
  const nextTrail = trail.concat(frame)
  if (frame === target) {
    return nextTrail
  }
  const children = Array.isArray(frame.frames) ? frame.frames : []
  for (const child of children) {
    const result = findFramePath(child, target, nextTrail)
    if (result) {
      return result
    }
  }
  return null
}

const resolvePinokioRelativeMatchTarget = (href) => {
  try {
    const parsed = new URL(href)
    return `${parsed.pathname}${parsed.search}${parsed.hash}` || '/'
  } catch (_) {
    return href || ''
  }
}

const escapePinokioPattern = (value) => String(value || '').replace(/[|\\{}()[\]^$+?.]/g, '\\$&')
const pinokioPatternToExpression = (value) => {
  const input = String(value || '')
  let expression = ''
  for (let index = 0; index < input.length; index += 1) {
    const char = input[index]
    if (char === '*') {
      while (input[index + 1] === '*') {
        index += 1
      }
      expression += '.*'
      continue
    }
    expression += escapePinokioPattern(char)
  }
  return `^${expression}$`
}

const matchesPinokioInjectPattern = (pattern, currentUrl) => {
  if (typeof pattern !== 'string') {
    return false
  }
  const normalizedPattern = pattern.trim()
  if (!normalizedPattern) {
    return false
  }
  const sourceValue = /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(normalizedPattern)
    ? currentUrl
    : resolvePinokioRelativeMatchTarget(currentUrl)
  const expression = pinokioPatternToExpression(normalizedPattern)
  try {
    return new RegExp(expression).test(sourceValue)
  } catch (_) {
    return false
  }
}

const matchPinokioInjectTargetToFrame = (targets, frame, hints = {}) => {
  if (!Array.isArray(targets) || !targets.length) {
    return null
  }
  const frameName = (frame && typeof frame.name === 'string' ? frame.name.trim() : '')
    || (typeof hints.frameName === 'string' ? hints.frameName.trim() : '')
  const frameUrl = normalizeInspectorUrl((frame && frame.url) || '')
    || normalizeInspectorUrl(typeof hints.frameUrl === 'string' ? hints.frameUrl.trim() : '')

  let matched = null
  if (frameName) {
    matched = targets.find((entry) => entry.name && entry.name === frameName && (!entry.src || urlsRoughlyMatch(entry.src, frameUrl)))
      || targets.find((entry) => entry.name && entry.name === frameName)
  }
  if (!matched && frameUrl) {
    matched = targets.find((entry) => entry.src && urlsRoughlyMatch(entry.src, frameUrl)) || null
  }
  return matched
}

const resolvePinokioInjectTargetMatch = ({ registry, frame, currentUrl, targetHints, descendantDepth = 0 }) => {
  if (!registry || !Array.isArray(registry.targets) || registry.targets.length === 0) {
    return null
  }
  const target = matchPinokioInjectTargetToFrame(registry.targets, frame, targetHints)
  if (!target) {
    return null
  }
  const inject = target.inject.filter((descriptor) => {
    if (descriptor && descriptor.frame !== 'all' && descendantDepth !== 0) {
      return false
    }
    const matches = Array.isArray(descriptor.match) && descriptor.match.length
      ? descriptor.match
      : ['*']
    return matches.some((pattern) => matchesPinokioInjectPattern(pattern, currentUrl))
  })
  return {
    target,
    inject
  }
}

const resolvePinokioInjectorsForFrame = (frame, payload = {}, sender = null) => {
  if (!frame) {
    return {
      inject: [],
      context: null
    }
  }
  const requestedContext = payload && payload.context && typeof payload.context === 'object'
    ? payload.context
    : {}
  const currentUrl = typeof requestedContext.currentUrl === 'string' && requestedContext.currentUrl.trim()
    ? requestedContext.currentUrl.trim()
    : (normalizeInspectorUrl(frame.url || '') || '')
  let ownerFrame = frame.parent || null
  let directChildFrame = frame
  let descendantDepth = 0
  const targetHints = {
    frameName: typeof requestedContext.frameName === 'string' ? requestedContext.frameName.trim() : '',
    frameUrl: currentUrl
  }

  while (ownerFrame) {
    const ownerKey = getFrameInjectorKey(ownerFrame)
    const registry = frameInjectTargetRegistry.get(ownerKey)
    if (!registry || !Array.isArray(registry.targets) || registry.targets.length === 0) {
      directChildFrame = ownerFrame
      ownerFrame = ownerFrame.parent || null
      descendantDepth += 1
      continue
    }
    const match = resolvePinokioInjectTargetMatch({
      registry,
      frame: directChildFrame,
      currentUrl,
      targetHints,
      descendantDepth
    })
    if (!match) {
      directChildFrame = ownerFrame
      ownerFrame = ownerFrame.parent || null
      descendantDepth += 1
      continue
    }
    return {
      inject: match.inject,
      context: {
        frameUrl: normalizeInspectorUrl(ownerFrame.url || '') || '',
        rootFrameUrl: normalizeInspectorUrl(directChildFrame.url || '') || '',
        currentUrl,
        pageUrl: normalizeInspectorUrl(frame.url || '') || currentUrl
      }
    }
  }

  const webContentsKey = getPinokioInjectWebContentsKey(sender, frame)
  if (webContentsKey) {
    const registries = Array.from(frameInjectTargetRegistry.entries())
      .map(([ownerKey, registry]) => ({ ownerKey, registry }))
      .filter(({ registry }) => registry && registry.webContentsKey === webContentsKey && Array.isArray(registry.targets) && registry.targets.length > 0)
      .sort((left, right) => (right.registry.updatedAt || 0) - (left.registry.updatedAt || 0))
    for (const entry of registries) {
      const match = resolvePinokioInjectTargetMatch({
        registry: entry.registry,
        frame,
        currentUrl,
        targetHints,
        descendantDepth: 0
      })
      if (!match) {
        continue
      }
      return {
        inject: match.inject,
        context: {
          frameUrl: entry.registry.pageUrl || '',
          rootFrameUrl: normalizeInspectorUrl(frame.url || '') || currentUrl,
          currentUrl,
          pageUrl: entry.registry.pageUrl || currentUrl
        }
      }
    }
  }

  return {
    inject: [],
    context: null
  }
}

const PINOKIO_ABSOLUTE_URL_PATTERN = /^[a-zA-Z][a-zA-Z\d+\-.]*:/

const resolvePinokioInjectSourceUrl = (value) => {
  if (typeof value !== 'string') {
    return ''
  }
  const trimmed = value.trim()
  if (!trimmed) {
    return ''
  }
  if (!PINOKIO_ABSOLUTE_URL_PATTERN.test(trimmed) && !trimmed.startsWith('/')) {
    return ''
  }
  const baseUrl = root_url || 'http://localhost'
  const parsed = safeParseUrl(trimmed, baseUrl)
  if (!parsed) {
    return ''
  }
  if (!['http:', 'https:', 'file:'].includes(parsed.protocol)) {
    return ''
  }
  return parsed.href
}

const buildPinokioInjectRuntimeBootstrap = () => {
  const source = function() {
    const resolveTargetWindow = () => {
      try {
        if (window.parent && window.parent !== window) {
          return window.parent
        }
      } catch (_) {
      }
      try {
        if (window.top && window.top !== window) {
          return window.top
        }
      } catch (_) {
      }
      return window
    }

    const ensureApi = () => {
      if (!window.$pinokio || typeof window.$pinokio !== 'object') {
        window.$pinokio = {}
      }
      if (typeof window.$pinokio.trigger !== 'function') {
        window.$pinokio.trigger = function(eventName, payload = {}, context = {}) {
          if (typeof eventName !== 'string' || !eventName.trim()) {
            return { ok: false, handled: false, reason: 'invalid_event_name' }
          }
          const nextContext = (context && typeof context === 'object') ? { ...context } : {}
          if (!nextContext.frameUrl) {
            nextContext.frameUrl = window.location.href
          }
          resolveTargetWindow().postMessage({
            e: 'pinokio:event',
            event: eventName.trim(),
            payload: (payload && typeof payload === 'object') ? payload : {},
            context: nextContext
          }, '*')
          return { ok: true, handled: true, event: eventName.trim() }
        }
      }
      window.$pinokio.inject = function(definition) {
        return window.__PINOKIO_INJECT_RUNTIME__.register(definition)
      }
      return window.$pinokio
    }

    const buildMountContext = (descriptor, sourceContext) => {
      const currentUrl = (window && window.location && window.location.href) ? window.location.href : ''
      const baseContext = (sourceContext && typeof sourceContext === 'object') ? { ...sourceContext } : {}
      if (!baseContext.frameUrl) {
        baseContext.frameUrl = currentUrl
      }
      if (!baseContext.currentUrl) {
        baseContext.currentUrl = currentUrl
      }
      if (!baseContext.rootFrameUrl) {
        baseContext.rootFrameUrl = currentUrl
      }
      return {
        ...baseContext,
        descriptor,
        trigger(eventName, payload = {}, context = {}) {
          const nextContext = (context && typeof context === 'object')
            ? { ...baseContext, ...context }
            : { ...baseContext }
          return window.$pinokio.trigger(eventName, payload, nextContext)
        }
      }
    }

    if (!window.__PINOKIO_INJECT_RUNTIME__) {
      const state = {
        current: null,
        cleanups: new Map()
      }
      window.__PINOKIO_INJECT_RUNTIME__ = {
        register(definition) {
          const current = state.current
          if (!current) {
            throw new Error('window.$pinokio.inject() must be called while an injector is loading.')
          }
          if (!definition || typeof definition !== 'object' || typeof definition.mount !== 'function') {
            throw new Error('Pinokio injectors must provide a mount(ctx) function.')
          }
          if (current.registered) {
            throw new Error('Injector registered more than once during a single mount.')
          }
          const cleanup = definition.mount(buildMountContext(current.descriptor, current.context))
          current.registered = true
          if (typeof cleanup === 'function') {
            state.cleanups.set(current.descriptor.runtimeId, cleanup)
          } else {
            state.cleanups.delete(current.descriptor.runtimeId)
          }
          return { ok: true, id: current.descriptor.runtimeId || '' }
        },
        run(descriptor, context, runSource) {
          ensureApi()
          state.current = {
            descriptor: descriptor || {},
            context: (context && typeof context === 'object') ? context : {},
            registered: false
          }
          try {
            if (typeof runSource === 'function') {
              runSource()
            }
            if (!state.current.registered) {
              throw new Error('Injector did not call window.$pinokio.inject(...).')
            }
          } finally {
            state.current = null
          }
        },
        unmountAll() {
          for (const cleanup of state.cleanups.values()) {
            if (typeof cleanup !== 'function') {
              continue
            }
            try {
              cleanup()
            } catch (error) {
              try {
                console.warn('[pinokio][inject] cleanup failed', error && error.message ? error.message : String(error))
              } catch (_) {
              }
            }
          }
          state.cleanups.clear()
        }
      }
    }

    ensureApi()
  }
  return `(${source.toString()})();`
}

const buildPinokioInjectUnmountScript = () => `(() => {
  const runtime = window.__PINOKIO_INJECT_RUNTIME__
  if (runtime && typeof runtime.unmountAll === 'function') {
    runtime.unmountAll()
  }
})();`

const buildPinokioInjectExecution = ({ descriptor, context, source }) => {
  const bootstrap = buildPinokioInjectRuntimeBootstrap()
  return `(() => {
${bootstrap}
window.__PINOKIO_INJECT_RUNTIME__.run(${serializeForJavaScript(descriptor)}, ${serializeForJavaScript(context || {})}, () => {
${source}
})
})();
//# sourceURL=${descriptor.src}`
}

const resetPinokioInjectorsInFrame = async (frame) => {
  if (!frame || (typeof frame.isDestroyed === 'function' && frame.isDestroyed())) {
    return
  }
  const code = buildPinokioInjectUnmountScript()
  const tasks = []
  if (typeof frame.executeJavaScript === 'function') {
    tasks.push(frame.executeJavaScript(code, false))
  }
  if (typeof frame.executeJavaScriptInIsolatedWorld === 'function') {
    tasks.push(frame.executeJavaScriptInIsolatedWorld(
      PINOKIO_INJECT_ISOLATED_WORLD_ID,
      [{ code }],
      false
    ))
  }
  await Promise.allSettled(tasks)
}

const executePinokioInjectDescriptor = async (frame, descriptor, context) => {
  if (!frame || (typeof frame.isDestroyed === 'function' && frame.isDestroyed())) {
    throw new Error('Target frame is not available.')
  }
  const sourceDescriptor = descriptor
  const sourceUrl = resolvePinokioInjectSourceUrl(descriptor.src)
  if (!sourceUrl) {
    throw new Error(`Invalid injector source URL: ${descriptor.src}`)
  }
  const response = await fetch(sourceUrl, { cache: 'no-store' })
  if (!response || !response.ok) {
    const status = response ? response.status : 'unknown'
    throw new Error(`Unable to load injector source: ${status}`)
  }
  const source = await response.text()
  const resolvedDescriptor = {
    ...sourceDescriptor,
    src: sourceUrl
  }
  const code = buildPinokioInjectExecution({ descriptor: resolvedDescriptor, context, source })
  if (descriptor.world === 'isolated') {
    if (typeof frame.executeJavaScriptInIsolatedWorld !== 'function') {
      throw new Error('Isolated-world frame injection is not supported by this Electron frame API.')
    }
    return frame.executeJavaScriptInIsolatedWorld(
      PINOKIO_INJECT_ISOLATED_WORLD_ID,
      [{ code, url: sourceUrl }],
      false
    )
  }
  return frame.executeJavaScript(code, false)
}

const installInjectorHandlers = () => {
  if (injectorHandlersInstalled) {
    return
  }
  injectorHandlersInstalled = true

  const updatePinokioInjectTargets = (ownerFrame, sender, payload = {}) => {
    if (!ownerFrame || (typeof ownerFrame.isDestroyed === 'function' && ownerFrame.isDestroyed())) {
      return { ok: false, reason: 'missing_frame', targets: [] }
    }
    const ownerKey = getFrameInjectorKey(ownerFrame)
    const webContentsKey = getPinokioInjectWebContentsKey(sender, ownerFrame)
    const targets = normalizePinokioInjectTargetRegistrations(payload && payload.targets)
    frameInjectTargetRegistry.set(ownerKey, {
      targets,
      pageUrl: payload && payload.pageUrl ? payload.pageUrl : '',
      webContentsKey,
      updatedAt: Date.now()
    })
    return { ok: true, targets }
  }

  ipcMain.on('pinokio:update-inject-targets', (event, payload = {}) => {
    updatePinokioInjectTargets(event.senderFrame, event.sender, payload)
  })

  ipcMain.on('pinokio:update-inject-targets-sync', (event, payload = {}) => {
    event.returnValue = updatePinokioInjectTargets(event.senderFrame, event.sender, payload)
  })

  ipcMain.handle('pinokio:resolve-injectors', async (event, payload = {}) => {
    const frame = event.senderFrame
    if (!frame || (typeof frame.isDestroyed === 'function' && frame.isDestroyed())) {
      return { ok: false, reason: 'missing_frame', inject: [], context: null }
    }
    const resolved = resolvePinokioInjectorsForFrame(frame, payload, event.sender)
    return {
      ok: true,
      inject: resolved.inject,
      context: resolved.context
    }
  })

  ipcMain.handle('pinokio:reset-injectors', async (event, payload = {}) => {
    const frame = event.senderFrame
    if (!frame || (typeof frame.isDestroyed === 'function' && frame.isDestroyed())) {
      return { ok: false, reason: 'missing_frame' }
    }
    const frameKey = getFrameInjectorKey(frame)
    const syncId = typeof payload.syncId === 'number' ? payload.syncId : 0
    frameInjectorSyncState.set(frameKey, syncId)
    await resetPinokioInjectorsInFrame(frame)
    return { ok: true, syncId }
  })

  ipcMain.handle('pinokio:mount-injectors', async (event, payload = {}) => {
    const frame = event.senderFrame
    if (!frame || (typeof frame.isDestroyed === 'function' && frame.isDestroyed())) {
      return { ok: false, reason: 'missing_frame', applied: [], failed: [] }
    }
    const frameKey = getFrameInjectorKey(frame)
    const syncId = typeof payload.syncId === 'number' ? payload.syncId : 0
    if (syncId && frameInjectorSyncState.get(frameKey) !== syncId) {
      return { ok: true, skipped: true, reason: 'stale_sync', applied: [], failed: [], syncId }
    }
    const baseContext = payload && payload.context && typeof payload.context === 'object'
      ? { ...payload.context }
      : {}
    const injectList = Array.isArray(payload.inject) ? payload.inject : []
    const applied = []
    const failed = []

    for (let index = 0; index < injectList.length; index += 1) {
      if (syncId && frameInjectorSyncState.get(frameKey) !== syncId) {
        return { ok: true, skipped: true, reason: 'stale_sync', applied, failed, syncId }
      }
      const normalizedDescriptor = normalizePinokioInjectDescriptor(injectList[index])
      if (!normalizedDescriptor) {
        continue
      }
      const descriptor = {
        ...normalizedDescriptor,
        runtimeId: `${frameKey}:${syncId}:${index}:${normalizedDescriptor.src}`
      }
      try {
        await executePinokioInjectDescriptor(frame, descriptor, baseContext)
        applied.push({
          src: descriptor.src,
          world: descriptor.world,
          runtimeId: descriptor.runtimeId
        })
      } catch (error) {
        const message = error && error.message ? error.message : String(error)
        failed.push({
          src: descriptor.src,
          world: descriptor.world,
          error: message
        })
        console.warn('[pinokio][main] injector mount failed', {
          src: descriptor.src,
          world: descriptor.world,
          error: message
        })
      }
    }

    return {
      ok: failed.length === 0,
      applied,
      failed,
      syncId
    }
  })
}

const normalizePermissionList = (value) => {
  if (!value) return []
  const list = Array.isArray(value) ? value : [value]
  return list.map((item) => typeof item === 'string' ? item.trim() : '').filter(Boolean)
}

const permissionLabels = {
  microphone: 'Microphone',
  camera: 'Camera',
  screen: 'Screen Recording',
  screen_capture: 'Screen Recording'
}

const logPermission = (...args) => {
  console.log('[PERMISSION]', ...args)
}

const permissionHints = {
  darwin: {
    microphone: 'System Settings → Privacy & Security → Microphone',
    camera: 'System Settings → Privacy & Security → Camera',
    screen: 'System Settings → Privacy & Security → Screen Recording',
    screen_capture: 'System Settings → Privacy & Security → Screen Recording'
  },
  win32: {
    microphone: 'Settings → Privacy & security → Microphone (allow desktop apps)',
    camera: 'Settings → Privacy & security → Camera (allow desktop apps)',
    screen: 'Settings → Privacy & security → Screen recording',
    screen_capture: 'Settings → Privacy & security → Screen recording'
  },
  linux: {
    microphone: 'Check your sound settings (PipeWire/PulseAudio) and app permissions.',
    camera: 'Check your video device permissions in system settings.',
    screen: 'Check your desktop portal or compositor screen capture permissions.',
    screen_capture: 'Check your desktop portal or compositor screen capture permissions.'
  }
}

const getMediaAccessStatusSafe = (mediaType) => {
  if (!systemPreferences || typeof systemPreferences.getMediaAccessStatus !== 'function') {
    return 'unsupported'
  }
  try {
    return systemPreferences.getMediaAccessStatus(mediaType)
  } catch (_) {
    return 'unknown'
  }
}

const requestMediaPermission = async (permission) => {
  const platform = process.platform
  if (permission === 'microphone' || permission === 'camera') {
    const preStatus = getMediaAccessStatusSafe(permission)
    const canAsk = platform === 'darwin' && systemPreferences && typeof systemPreferences.askForMediaAccess === 'function'
    logPermission('requestMediaPermission', permission, { platform, preStatus, canAsk })
    let granted = false
    if (platform === 'darwin' && systemPreferences && typeof systemPreferences.askForMediaAccess === 'function') {
      granted = await systemPreferences.askForMediaAccess(permission)
    }
    const status = getMediaAccessStatusSafe(permission)
    if (status === 'granted') {
      granted = true
    }
    logPermission('requestMediaPermission result', permission, { status, granted })
    return { status, granted }
  }
  if (permission === 'screen' || permission === 'screen_capture') {
    const status = getMediaAccessStatusSafe('screen')
    logPermission('requestMediaPermission screen', permission, { status })
    return { status, granted: status === 'granted' }
  }
  logPermission('requestMediaPermission unsupported', permission)
  return { status: 'unsupported', granted: false }
}

const buildPermissionMessage = (platform, denied) => {
  if (!denied.length) return ''
  const items = denied.map((permission) => permissionLabels[permission] || permission)
  const label = items.length === 1 ? items[0] : items.join(', ')
  const hints = permissionHints[platform] || permissionHints.linux
  const hint = denied.length === 1
    ? (hints[denied[0]] || '')
    : ''
  if (hint) {
    return `Pinokio needs ${label} access. Enable it in ${hint}.`
  }
  return `Pinokio needs ${label} access. Please enable it in your OS privacy settings.`
}

const installPermissionHandlers = () => {
  if (permissionHandlersInstalled) {
    return
  }
  permissionHandlersInstalled = true
  ipcMain.handle('pinokio:request-permissions', async (event, payload = {}) => {
    const permissions = normalizePermissionList(payload.permissions)
    if (permissions.length === 0) {
      return { ok: true, permissions: [], results: {}, denied: [] }
    }
    const results = {}
    const denied = []
    for (const permission of permissions) {
      const result = await requestMediaPermission(permission)
      results[permission] = result
      if (!result.granted) {
        denied.push(permission)
      }
    }
    return {
      ok: denied.length === 0,
      permissions,
      denied,
      results,
      platform: process.platform,
      message: denied.length ? buildPermissionMessage(process.platform, denied) : ''
    }
  })
}

const canRequestPermission = (permission) => {
  if (process.platform !== 'darwin') {
    return false
  }
  return permission === 'microphone' || permission === 'camera'
}

const promptForProjectPermissions = async (webContents, project, permissions) => {
  if (!permissions.length) {
    return
  }
  const promptKey = `${project}:${permissions.join(',')}`
  if (permissionPromptInFlight.has(promptKey) || permissionPrompted.has(promptKey)) {
    logPermission('prompt skipped (already prompted)', { project, permissions })
    return
  }
  logPermission('prompt start', { project, permissions })
  const pending = []
  const blocked = []
  const statusInfo = []
  for (const permission of permissions) {
    const statusTarget = (permission === 'screen' || permission === 'screen_capture') ? 'screen' : permission
    const status = getMediaAccessStatusSafe(statusTarget)
    if (status === 'granted') {
      statusInfo.push({ permission, status, action: 'skip' })
      continue
    }
    if (status === 'denied') {
      blocked.push(permission)
      statusInfo.push({ permission, status, action: 'blocked' })
    } else if (canRequestPermission(permission)) {
      pending.push(permission)
      statusInfo.push({ permission, status, action: 'pending' })
    } else {
      blocked.push(permission)
      statusInfo.push({ permission, status, action: 'blocked' })
    }
  }
  logPermission('prompt status', statusInfo)
  logPermission('prompt lists', { pending, blocked })
  if (pending.length === 0 && blocked.length === 0) {
    return
  }
  permissionPromptInFlight.add(promptKey)
  try {
    const owner = webContents && !webContents.isDestroyed()
      ? BrowserWindow.fromWebContents(webContents)
      : null
    const denied = blocked.slice()
    if (pending.length > 0) {
      const label = pending.map((permission) => permissionLabels[permission] || permission).join(', ')
      const { response } = await dialog.showMessageBox(owner, {
        type: 'info',
        buttons: ['Allow', 'Not now'],
        defaultId: 0,
        cancelId: 1,
        title: 'Permission required',
        message: `Allow ${label} access?`,
        detail: `This app requests ${label} access. Click "Allow" to show the OS permission prompt.`,
        noLink: true
      })
      logPermission('prompt response', { project, permissions: pending, response })
      if (response === 0) {
        for (const permission of pending) {
          const result = await requestMediaPermission(permission)
          if (!result.granted) {
            denied.push(permission)
          }
        }
      }
    }
    if (denied.length > 0) {
      logPermission('prompt denied', { project, denied })
      const message = buildPermissionMessage(process.platform, denied)
      if (message) {
        await dialog.showMessageBox(owner, {
          type: 'warning',
          buttons: ['OK'],
          defaultId: 0,
          message,
          noLink: true
        })
      }
    }
  } finally {
    permissionPromptInFlight.delete(promptKey)
    permissionPrompted.add(promptKey)
  }
}

// Screenshot capture function for inspect mode
const captureScreenshotRegion = async (bounds) => {
  try {
    const { nativeImage } = require('electron')
    
    // Get all displays to find the correct one
    const displays = screen.getAllDisplays()
    const primaryDisplay = screen.getPrimaryDisplay()
    
    // Get desktop capturer sources with full resolution
    const sources = await desktopCapturer.getSources({ 
      types: ['screen'],
      thumbnailSize: {
        width: primaryDisplay.bounds.width * primaryDisplay.scaleFactor,
        height: primaryDisplay.bounds.height * primaryDisplay.scaleFactor
      }
    })
    
    if (sources.length === 0) {
      throw new Error('No screen sources available')
    }
    
    // Find the screen source that matches our primary display
    let screenSource = sources[0] // fallback to first source
    
    // Try to find the exact screen source by name or use the first one
    for (const source of sources) {
      if (source.name.includes('Entire Screen') || source.name.includes('Screen 1')) {
        screenSource = source
        break
      }
    }
    
    // Get the full resolution screenshot from thumbnail
    const thumbnailImage = screenSource.thumbnail
    const fullScreenshotBuffer = thumbnailImage.toPNG()
    const fullScreenshot = nativeImage.createFromBuffer(fullScreenshotBuffer)
    
    // Calculate the actual pixel bounds accounting for device pixel ratio
    const scaleFactor = primaryDisplay.scaleFactor
    const actualBounds = {
      x: Math.max(0, Math.round(bounds.x * scaleFactor)),
      y: Math.max(0, Math.round(bounds.y * scaleFactor)),
      width: Math.min(
        Math.round(bounds.width * scaleFactor),
        fullScreenshot.getSize().width - Math.round(bounds.x * scaleFactor)
      ),
      height: Math.min(
        Math.round(bounds.height * scaleFactor),
        fullScreenshot.getSize().height - Math.round(bounds.y * scaleFactor)
      )
    }
    
    // Ensure minimum size
    actualBounds.width = Math.max(1, actualBounds.width)
    actualBounds.height = Math.max(1, actualBounds.height)
    
    // Crop the screenshot to the element bounds
    const croppedImage = fullScreenshot.crop(actualBounds)
    
    // Convert to PNG buffer and then to data URL
    const croppedBuffer = croppedImage.toPNG()
    const dataUrl = 'data:image/png;base64,' + croppedBuffer.toString('base64')
    
    console.log(`Screenshot captured: ${actualBounds.width}x${actualBounds.height} at (${actualBounds.x},${actualBounds.y})`)
    
    return dataUrl
  } catch (error) {
    console.warn('Screenshot capture failed:', error)
    throw error
  }
}


//function enable_cors(win) {
//  win.webContents.session.webRequest.onBeforeSendHeaders((details, callback) => {
//    details.requestHeaders['Origin'] = null;
//    details.headers['Origin'] = null;
//    callback({ requestHeaders: details.requestHeaders })
//  });
////  win.webContents.session.webRequest.onBeforeSendHeaders(
////    (details, callback) => {
////      const { requestHeaders } = details;
////      UpsertKeyValue(requestHeaders, 'Access-Control-Allow-Origin', ['*']);
////      callback({ requestHeaders });
////    },
////  );
////
////  win.webContents.session.webRequest.onHeadersReceived((details, callback) => {
////    const { responseHeaders } = details;
////    UpsertKeyValue(responseHeaders, 'Access-Control-Allow-Origin', ['*']);
////    UpsertKeyValue(responseHeaders, 'Access-Control-Allow-Headers', ['*']);
////    callback({
////      responseHeaders,
////    });
////  });
//}


const pushContextMenuSeparator = (template) => {
  if (!template.length) {
    return
  }
  if (template[template.length - 1].type === 'separator') {
    return
  }
  template.push({ type: 'separator' })
}
const buildBrowserContextMenuTemplate = (webContents, params = {}) => {
  const template = []
  const linkURL = typeof params.linkURL === 'string' ? params.linkURL : ''
  const srcURL = typeof params.srcURL === 'string' ? params.srcURL : ''
  const selectionText = typeof params.selectionText === 'string' ? params.selectionText : ''
  const hasSelection = selectionText.trim().length > 0
  const editFlags = params.editFlags || {}
  const isEditable = Boolean(params.isEditable)
  const hasMediaSource = typeof params.mediaType === 'string' && params.mediaType !== 'none' && srcURL
  const canSuggestSpelling = Array.isArray(params.dictionarySuggestions) && params.dictionarySuggestions.length > 0
  const hasMisspelledWord = typeof params.misspelledWord === 'string' && params.misspelledWord.length > 0
  const owner = webContents && !webContents.isDestroyed() ? webContents.getOwnerBrowserWindow() : null
  const canGoBack = Boolean(webContents && webContents.canGoBack && webContents.canGoBack())
  const canGoForward = Boolean(webContents && webContents.canGoForward && webContents.canGoForward())

  if (linkURL) {
    template.push({
      label: 'Open Link in New Window',
      click: () => {
        try {
          if (typeof loadNewWindow === 'function' && PORT) {
            if (popupShellManager.isPinokioWindowUrl(linkURL, root_url)) {
              loadNewWindow(linkURL, PORT)
            } else {
              popupShellManager.openExternalWindow({ url: linkURL })
            }
            return
          }
        } catch (error) {
        }
        shell.openExternal(linkURL).catch(() => {})
      }
    })
    template.push({
      label: 'Open Link in Browser',
      click: () => {
        shell.openExternal(linkURL).catch(() => {})
      }
    })
    template.push({
      label: 'Copy Link Address',
      click: () => clipboard.writeText(linkURL)
    })
    pushContextMenuSeparator(template)
  }

  if (hasMediaSource) {
    template.push({
      label: 'Open Media in Browser',
      click: () => {
        shell.openExternal(srcURL).catch(() => {})
      }
    })
    template.push({
      label: 'Copy Media Address',
      click: () => clipboard.writeText(srcURL)
    })
    pushContextMenuSeparator(template)
  }

  if (!isEditable) {
    template.push({
      label: 'Back',
      enabled: canGoBack,
      click: () => {
        if (webContents && !webContents.isDestroyed() && webContents.canGoBack()) {
          webContents.goBack()
        }
      }
    })
    template.push({
      label: 'Forward',
      enabled: canGoForward,
      click: () => {
        if (webContents && !webContents.isDestroyed() && webContents.canGoForward()) {
          webContents.goForward()
        }
      }
    })
    template.push({
      label: 'Reload',
      click: () => {
        if (webContents && !webContents.isDestroyed()) {
          webContents.reload()
        }
      }
    })
    pushContextMenuSeparator(template)
  }

  if (isEditable) {
    if (canSuggestSpelling && hasMisspelledWord) {
      for (const suggestion of params.dictionarySuggestions.slice(0, 5)) {
        template.push({
          label: suggestion,
          click: () => {
            if (webContents && !webContents.isDestroyed()) {
              webContents.replaceMisspelling(suggestion)
            }
          }
        })
      }
      template.push({
        label: 'Add to Dictionary',
        click: () => {
          try {
            if (webContents && !webContents.isDestroyed() && webContents.session && typeof webContents.session.addWordToSpellCheckerDictionary === 'function') {
              webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord)
            }
          } catch (error) {
          }
        }
      })
      pushContextMenuSeparator(template)
    }
    template.push({ role: 'undo', enabled: editFlags.canUndo !== false })
    template.push({ role: 'redo', enabled: editFlags.canRedo !== false })
    pushContextMenuSeparator(template)
    template.push({ role: 'cut', enabled: editFlags.canCut !== false })
    template.push({ role: 'copy', enabled: editFlags.canCopy !== false })
    template.push({ role: 'paste', enabled: editFlags.canPaste !== false })
    template.push({ role: 'delete', enabled: editFlags.canDelete !== false })
    pushContextMenuSeparator(template)
    template.push({ role: 'selectAll' })
  } else {
    if (hasSelection) {
      template.push({ role: 'copy' })
    }
    template.push({ role: 'selectAll' })
  }

  pushContextMenuSeparator(template)
  template.push({
    label: 'Inspect Element',
    click: () => {
      if (!webContents || webContents.isDestroyed()) {
        return
      }
      if (!webContents.isDevToolsOpened()) {
        webContents.openDevTools({ mode: 'detach' })
      }
      const x = typeof params.x === 'number' ? params.x : null
      const y = typeof params.y === 'number' ? params.y : null
      if (x !== null && y !== null) {
        webContents.inspectElement(x, y)
      }
    }
  })

  if (template.length && template[template.length - 1].type === 'separator') {
    template.pop()
  }

  if (owner && owner.isDestroyed()) {
    return []
  }
  return template
}
const attach = (event, webContents) => {
  let wc = webContents

  if (ENABLE_BROWSER_CONSOLE_LOG && !attachedConsoleListeners.has(webContents)) {
    attachedConsoleListeners.add(webContents)
    webContents.on('console-message', (event, level, message, line, sourceId) => {
      if (!root_url) {
        return
      }
      const state = browserConsoleState.get(webContents)
      let pageUrl = state && state.url ? state.url : ''
      if (!pageUrl) {
        try {
          pageUrl = webContents.getURL()
        } catch (err) {
          pageUrl = ''
        }
      }
      if (!pageUrl || !pageUrl.startsWith(root_url)) {
        return
      }
      const targetFile = ensureBrowserLogFile()
      if (!targetFile) {
        return
      }
      const logUrl = resolveConsoleSourceUrl(sourceId, pageUrl)
      if (!logUrl || !shouldLogUrl(logUrl)) {
        return
      }
      const timestamp = new Date().toISOString()
      const levelLabel = consoleLevelLabels[level] || 'log'
      let location = ''
      if (sourceId) {
        location = ` (${sourceId}${line ? `:${line}` : ''})`
      } else if (line) {
        location = ` (:${line})`
      }
      const entry = `[${timestamp}]\t${logUrl}\t[${levelLabel}] ${message}${location}\n`
      browserLogBuffer.push(entry)
      if (browserLogBuffer.length > 100) {
        browserLogBuffer.shift()
      }
      browserLogWritePromise = browserLogWritePromise.then(() => fs.promises.writeFile(targetFile, browserLogBuffer.join(''))).catch((err) => {
        console.error('[BROWSER LOG] Failed to persist console output', err)
      })
    })
    webContents.once('destroyed', () => {
      clearBrowserConsoleState(webContents)
    })
  }
  // Enable screen capture permissions for all webContents
  webContents.session.setPermissionRequestHandler((webContents, permission, callback) => {
    callback(true)
    //console.log(`[PERMISSION DEBUG] Permission requested: "${permission}" from webContents`)
    //if (permission === 'media' || permission === 'display-capture' || permission === 'desktopCapture') {
    //  console.log(`[PERMISSION DEBUG] Granting permission: "${permission}"`)
    //  callback(true)
    //} else {
    //  console.log(`[PERMISSION DEBUG] Denying permission: "${permission}"`)
    //  callback(false)
    //}
  })

  webContents.session.setPermissionCheckHandler((webContents, permission) => {
    return true
    //console.log(`[PERMISSION DEBUG] Permission check for: "${permission}"`)
    //return permission === 'media' || permission === 'display-capture' || permission === 'desktopCapture'
  })

  webContents.session.setDisplayMediaRequestHandler((request, callback) => {
    console.log('[DISPLAY MEDIA DEBUG] Display media request received')
    desktopCapturer.getSources({ types: ['screen', 'window'] }).then((sources) => {
      console.log('[DISPLAY MEDIA DEBUG] Available sources:', sources.length)
      if (sources.length > 0) {
        callback({ video: sources[0], audio: 'loopback' })
      } else {
        callback({})
      }
    }).catch(err => {
      console.error('[DISPLAY MEDIA DEBUG] Error getting sources:', err)
      callback({})
    })
  })

  webContents.on('will-prevent-unload', (event) => {
    event.preventDefault()
  })

  webContents.on('will-navigate', (event, url) => {
    if (!webContents.opened) {
      // The first time this view is being used, set the "opened" to true, and don't do anything
      // The next time the view navigates, "the "opened" is already true, so trigger the URL open logic
      //  - if the new URL has the same host as the app's url, open in app
      //  - if it's a remote host, open in external browser
      webContents.opened = true
    } else {
//      console.log("will-navigate", { event, url })
      const owner = webContents.getOwnerBrowserWindow()
      if (openNonPinokioNavigationInPopup({ event, owner, url })) {
        return
      }
      const target = safeParseUrl(url, root_url || undefined)
      if (target && !popupShellManager.isPinokioWindowUrl(target.href, root_url) && target.protocol !== 'http:' && target.protocol !== 'https:') {
        event.preventDefault()
        shell.openExternal(target.href)
      }
    }
  })
  webContents.on('will-frame-navigate', (event) => {
    const owner = webContents.getOwnerBrowserWindow()
    const frame = event && event.frame
    const isDirectChildFrame = Boolean(
      frame &&
      webContents.mainFrame &&
      frame.parent &&
      !frame.parent.parent &&
      frame.parent === webContents.mainFrame
    )
    if (!isDirectChildFrame) {
      return
    }
    const currentUrl = (() => {
      try {
        return webContents.getURL()
      } catch (_) {
        return ''
      }
    })()
    if (!isRootShellUrl(currentUrl)) {
      return
    }
    openNonPinokioNavigationInPopup({
      event,
      owner,
      url: event && event.url,
      frame
    })
  })
//  webContents.session.defaultSession.loadExtension('path/to/unpacked/extension').then(({ id }) => {
//  })


  webContents.session.webRequest.onHeadersReceived((details, callback) => {
//    console.log("details", details)
//    console.log("responseHeaders", JSON.stringify(details.responseHeaders, null, 2))



    // 1. Remove X-Frame-Options
    if (details.responseHeaders["X-Frame-Options"]) {
      delete details.responseHeaders["X-Frame-Options"] 
    } else if (details.responseHeaders["x-frame-options"]) {
      delete details.responseHeaders["x-frame-options"] 
    }

    // 2. Remove Content-Security-Policy "frame-ancestors" attribute
    let csp
    let csp_type;
    if (details.responseHeaders["Content-Security-Policy"]) {
      csp = details.responseHeaders["Content-Security-Policy"]
      csp_type = 0
    } else if (details.responseHeaders['content-security-policy']) {
      csp = details.responseHeaders["content-security-policy"]
      csp_type = 1
    }

    if (details.responseHeaders["cross-origin-opener-policy-report-only"]) {
      delete details.responseHeaders["cross-origin-opener-policy-report-only"]
    } else if (details.responseHeaders["Cross-Origin-Opener-Policy-Report-Only"]) {
      delete details.responseHeaders["Cross-Origin-Opener-Policy-Report-Only"]
    }


    if (csp) {
//      console.log("CSP", csp)
      // find /frame-ancestors ;$/
      let new_csp = csp.map((c) => {
        return c.replaceAll(/frame-ancestors[^;]+;?/gi, "")
      })

//      console.log("new_csp = ", new_csp)

      const r = {
        responseHeaders: details.responseHeaders
      }
      if (csp_type === 0) {
        r.responseHeaders["Content-Security-Policy"] = new_csp
      } else if (csp_type === 1) {
        r.responseHeaders["content-security-policy"] = new_csp
      }
//      console.log("R", JSON.stringify(r, null, 2))

      callback(r)
    } else {
//      console.log("RH", details.responseHeaders)
      callback({
        responseHeaders: details.responseHeaders
      })
    }
  })



  webContents.session.webRequest.onBeforeSendHeaders((details, callback) => {

    let ua = details.requestHeaders['User-Agent']
//    console.log("User Agent Before", ua)
    if (ua) {
      ua = ua.replace(/ pinokio\/[0-9.]+/i, '');
      ua = ua.replace(/Electron\/.+ /i,'');
//      console.log("User Agent After", ua)
      details.requestHeaders['User-Agent'] = ua;
    }


//    console.log("REQ", details)
//    console.log("HEADER BEFORE", details.requestHeaders)
//    // Remove all sec-fetch-* headers
//    for(let key in details.requestHeaders) {
//      if (key.toLowerCase().startsWith("sec-")) {
//        delete details.requestHeaders[key]
//      }
//    }
//    console.log("HEADER AFTER", details.requestHeaders)
    callback({ cancel: false, requestHeaders: details.requestHeaders });
  });


//  webContents.session.webRequest.onBeforeSendHeaders(
//    (details, callback) => {
//      const { requestHeaders } = details;
//      UpsertKeyValue(requestHeaders, 'Access-Control-Allow-Origin', ['*']);
//      callback({ requestHeaders });
//    },
//  );
//
//  webContents.session.webRequest.onHeadersReceived((details, callback) => {
//    const { responseHeaders } = details;
//    UpsertKeyValue(responseHeaders, 'Access-Control-Allow-Origin', ['*']);
//    UpsertKeyValue(responseHeaders, 'Access-Control-Allow-Headers', ['*']);
//    callback({
//      responseHeaders,
//    });
//  });

//  webContents.session.webRequest.onBeforeSendHeaders((details, callback) => {
//    //console.log("Before", { details })
//    if (details.requestHeaders) details.requestHeaders['Origin'] = null;
//    if (details.requestHeaders) details.requestHeaders['Referer'] = null;
//    if (details.requestHeaders) details.requestHeaders['referer'] = null;
//    if (details.headers) details.headers['Origin'] = null;
//    if (details.headers) details.headers['Referer'] = null;
//    if (details.headers) details.headers['referer'] = null;
//
//    if (details.referrer) details.referrer = null
//    //console.log("After", { details })
//    callback({ requestHeaders: details.requestHeaders })
//  });

//  webContents.on("did-create-window", (parentWindow, details) => {
//    const view = new BrowserView();
//    parentWindow.setBrowserView(view);
//    view.setBounds({ x: 0, y: 30, width: parentWindow.getContentBounds().width, height: parentWindow.getContentBounds().height - 30 });
//    view.setAutoResize({ width: true, height: true });
//    view.webContents.loadURL(details.url);
//  })
  webContents.on('did-navigate', (event, url) => {
    let win = webContents.getOwnerBrowserWindow()
    if (win && typeof win.setTitleBarOverlay === "function") {
      const overlay = titleBarOverlay(colors)
      setWindowTitleBarOverlay(win, overlay)
    }
    launched = true

    updateBrowserConsoleTarget(webContents, url)

  })
  webContents.on('did-navigate-in-page', (event, url) => {
    updateBrowserConsoleTarget(webContents, url)
  })
  webContents.on('context-menu', (event, params) => {
    const template = buildBrowserContextMenuTemplate(webContents, params)
    if (!template.length) {
      return
    }
    const menu = Menu.buildFromTemplate(template)
    const win = webContents.getOwnerBrowserWindow()
    if (win && !win.isDestroyed()) {
      menu.popup({ window: win })
      return
    }
    menu.popup()
  })
  webContents.setWindowOpenHandler((config) => {
    let url = config.url
    let features = config.features || ""
    let disposition = config.disposition || ""
    let params = new URLSearchParams(features.split(",").join("&"))
    let win = wc.getOwnerBrowserWindow()
    let [width, height] = win.getSize()
    let [x,y] = win.getPosition()

    // if the origin is the same as the pinokio host,
    // always open in new window

    // if not, check the features
    // if features exists and it's app or self, open in pinokio
    // otherwise if it's file, 

    if (/(^|,)\s*pinokio\s*(,|$)/i.test(features)) {
      const targetUrl = popupShellManager.resolveTargetUrl({
        url,
        openerWebContents: wc,
        rootUrl: root_url
      })
      if (targetUrl) {
        if (popupShellManager.isPinokioWindowUrl(targetUrl, root_url)) {
          loadNewWindow(targetUrl, PORT)
        } else {
          popupShellManager.openExternalWindow({ url: targetUrl })
        }
      }
      return { action: 'deny' };
    }

    if (features === "browser") {
      shell.openExternal(url);
      return { action: 'deny' };
    } else if (disposition === "foreground-tab" || disposition === "background-tab") {
      const targetUrl = popupShellManager.resolveTargetUrl({
        url,
        openerWebContents: wc,
        rootUrl: root_url
      })
      if (targetUrl) {
        popupShellManager.openExternalWindow({ url: targetUrl })
      }
      return { action: 'deny' };
    } else if (popupShellManager.isPinokioWindowUrl(url, root_url)) {
      return {
        action: 'allow',
        outlivesOpener: true,
        overrideBrowserWindowOptions: {
          width: (params.get("width") ? parseInt(params.get("width")) : width),
          height: (params.get("height") ? parseInt(params.get("height")) : height),
          x: x + 30,
          y: y + 30,

          parent: null,
          titleBarStyle : "hidden",
          titleBarOverlay : titleBarOverlay(colors),
          webPreferences: {
            session: session.defaultSession,
            webSecurity: false,
            spellcheck: false,
            nativeWindowOpen: true,
            contextIsolation: false,
            nodeIntegrationInSubFrames: true,
            preload: path.join(__dirname, 'preload.js')
          },
        }
      }
    } else {
      if (features.startsWith("app") || features.startsWith("self")) {
        return popupShellManager.createPopupResponse({ params, width, height, x, y })
      }
      if (features.startsWith("file")) {
        let u = features.replace("file://", "")
        shell.showItemInFolder(u)
        return { action: 'deny' };
      }
      const targetUrl = popupShellManager.resolveTargetUrl({
        url,
        openerWebContents: wc,
        rootUrl: root_url
      })
      if (targetUrl) {
        popupShellManager.openExternalWindow({ url: targetUrl })
      }
      return { action: 'deny' };
    }

//    if (origin === root_url) {
//      // if the origin is the same as pinokio, open in pinokio
//      // otherwise open in external browser
//      if (features) {
//        if (features.startsWith("app") || features.startsWith("self")) {
//          return {
//            action: 'allow',
//            outlivesOpener: true,
//            overrideBrowserWindowOptions: {
//              width: (params.get("width") ? parseInt(params.get("width")) : width),
//              height: (params.get("height") ? parseInt(params.get("height")) : height),
//              x: x + 30,
//              y: y + 30,
//
//              parent: null,
//              titleBarStyle : "hidden",
//              titleBarOverlay : titleBarOverlay("default"),
//            }
//          }
//        } else if (features.startsWith("file")) {
//          let u = features.replace("file://", "")
//          shell.showItemInFolder(u)
//          return { action: 'deny' };
//        } else {
//          return { action: 'deny' };
//        }
//      } else {
//        if (features.startsWith("file")) {
//          let u = features.replace("file://", "")
//          shell.showItemInFolder(u)
//          return { action: 'deny' };
//        } else {
//          shell.openExternal(url);
//          return { action: 'deny' };
//        }
//      }
//    } else {
//      if (features.startsWith("file")) {
//        let u = features.replace("file://", "")
//        shell.showItemInFolder(u)
//        return { action: 'deny' };
//      } else {
//        shell.openExternal(url);
//        return { action: 'deny' };
//      }
//    }
  });
}
const getWinState = (url, options) => {
  let filename
  try {
    let pathname = new URL(url).pathname.slice(1)
    filename = pathname.slice("/").join("-")
  } catch {
    filename = "index.json"
  }
  let state = windowStateKeeper({
    file: filename,
    ...options
  });
  return state
}
const createWindow = (port) => {


  let mainWindowState = windowStateKeeper({
//    file: "index.json",
    defaultWidth: 1000,
    defaultHeight: 800
  });

  mainWindow = new BrowserWindow({
    titleBarStyle : "hidden",
    titleBarOverlay : titleBarOverlay(colors),
    x: mainWindowState.x,
    y: mainWindowState.y,
    width: mainWindowState.width,
    height: mainWindowState.height,
    minWidth: 190,
    webPreferences: {
      session: session.defaultSession,
      webSecurity: false,
      spellcheck: false,
      nativeWindowOpen: true,
      contextIsolation: false,
      nodeIntegrationInSubFrames: true,
      enableRemoteModule: false,
      experimentalFeatures: true,
      preload: path.join(__dirname, 'preload.js')
    },
  })
  mainWindow.on('closed', () => {
    mainWindow = null
  })

  // Debug media device availability
  mainWindow.webContents.once('did-finish-load', () => {
    console.log('[MEDIA DEBUG] Main window loaded, checking media devices availability...')
    mainWindow.webContents.executeJavaScript(`
      console.log('[MEDIA DEBUG] navigator.mediaDevices available:', !!navigator.mediaDevices);
      console.log('[MEDIA DEBUG] getDisplayMedia available:', !!(navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia));
      console.log('[MEDIA DEBUG] getUserMedia available:', !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia));
      if (navigator.mediaDevices && navigator.mediaDevices.getSupportedConstraints) {
        console.log('[MEDIA DEBUG] Supported constraints:', navigator.mediaDevices.getSupportedConstraints());
      }
    `).catch(err => console.error('[MEDIA DEBUG] Error checking media devices:', err))
    if (updateBannerPayload && !(updateBannerPayload.state === 'available' && updateBannerDismissed)) {
      mainWindow.webContents.send('pinokio:update-banner', updateBannerPayload)
    }
  })

  // Enable screen capture permissions
  mainWindow.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => {
    callback(true)
    //console.log(`[PERMISSION DEBUG] MainWindow permission requested: "${permission}"`)
    //if (permission === 'media' || permission === 'display-capture' || permission === 'desktopCapture') {
    //  console.log(`[PERMISSION DEBUG] MainWindow granting permission: "${permission}"`)
    //  callback(true)
    //} else {
    //  console.log(`[PERMISSION DEBUG] MainWindow denying permission: "${permission}"`)
    //  callback(false)
    //}
  })
//  enable_cors(mainWindow)
  if("" + port === "80") {
    root_url = `http://localhost`
  } else {
    root_url = `http://localhost:${port}`
  }
  mainWindow.loadURL(root_url)
//  mainWindow.maximize();
  mainWindowState.manage(mainWindow);

}
const loadNewWindow = (url, port) => {


  let winState = windowStateKeeper({
//    file: "index.json",
    defaultWidth: 1000,
    defaultHeight: 800
  });

  let win = new BrowserWindow({
    titleBarStyle : "hidden",
    titleBarOverlay : titleBarOverlay(colors),
    x: winState.x,
    y: winState.y,
    width: winState.width,
    height: winState.height,
    minWidth: 190,
    webPreferences: {
      session: session.defaultSession,
      webSecurity: false,
      spellcheck: false,
      nativeWindowOpen: true,
      contextIsolation: false,
      nodeIntegrationInSubFrames: true,
      enableRemoteModule: false,
      experimentalFeatures: true,
      preload: path.join(__dirname, 'preload.js')
    },
  })
  installForceDestroyOnClose(win)

  // Enable screen capture permissions
  win.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => {
    callback(true)
    //console.log(`[PERMISSION DEBUG] New window permission requested: "${permission}"`)
    //if (permission === 'media' || permission === 'display-capture' || permission === 'desktopCapture') {
    //  console.log(`[PERMISSION DEBUG] New window granting permission: "${permission}"`)
    //  callback(true)
    //} else {
    //  console.log(`[PERMISSION DEBUG] New window denying permission: "${permission}"`)
    //  callback(false)
    //}
  })

//  enable_cors(win)
  win.focus()
  win.loadURL(url)
  winState.manage(win)

}
popupShellManager.setPinokioHomeWindowOpener(() => {
  if (root_url && PORT) {
    loadNewWindow(root_url, PORT)
  }
})


if (process.defaultApp) {
  if (process.argv.length >= 2) {
    app.setAsDefaultProtocolClient('pinokio', process.execPath, [path.resolve(process.argv[1])])
  }
} else {
  app.setAsDefaultProtocolClient('pinokio')
}

const gotTheLock = app.requestSingleInstanceLock()


if (!gotTheLock) {
  app.quit()
} else {
  app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
    
      // Prevent having error
      event.preventDefault()
      // and continue
      callback(true)

  })

  app.on('second-instance', (event, argv) => {

    if (!mainWindow || mainWindow.isDestroyed()) {
      createWindow(PORT)
    } else {
      if (mainWindow.isMinimized()) mainWindow.restore()
      mainWindow.focus()
    }
    const url = [...argv].reverse().find(arg => typeof arg === 'string' && arg.startsWith('pinokio:'))
    if (!url) {
      return
    }
    //let u = new URL(url).search
    let u = url.replace(/pinokio:[\/]+/, "")
    loadNewWindow(`${root_url}/pinokio/${u}`, PORT)
//    if (BrowserWindow.getAllWindows().length === 0 || !mainWindow) createWindow(PORT)
//    mainWindow.focus()
//    mainWindow.loadURL(`${root_url}/pinokio/${u}`)
  })

  // Create mainWindow, load the rest of the app, etc...
  // Enable desktop capture for getDisplayMedia support (must be before app ready)
  app.commandLine.appendSwitch('disable-features', 'LazyImageLoading')
  app.commandLine.appendSwitch('enable-experimental-web-platform-features');
  app.commandLine.appendSwitch('enable-features', 'GetDisplayMediaSet,GetDisplayMediaSetAutoSelectAllScreens');
  
  app.whenReady().then(async () => {
    console.log('App is ready, about to install inspector handlers...')
    app.userAgentFallback = "Pinokio"

    installInspectorHandlers()
    installInjectorHandlers()
    installPermissionHandlers()
    installClosePopupOnDownload(session.defaultSession)
    installClosePopupOnDownload(popupShellManager.getPopupBrowserSession())

    ipcMain.on('pinokio:update-banner-action', (_event, payload = {}) => {
      const action = payload && payload.action
      if (!action) {
        return
      }
      if (updateTestMode) {
        if (action === 'update') {
          startUpdateBannerTestDownload()
          return
        }
        if (action === 'restart') {
          simulateUpdateBannerRestart()
          return
        }
        if (action === 'dismiss') {
          updateBannerDismissed = true
          hideUpdateBanner()
          return
        }
        if (action === 'release-notes') {
          const target = payload && payload.releaseUrl ? payload.releaseUrl : UPDATE_RELEASES_URL
          shell.openExternal(target)
          return
        }
      }
      if (action === 'update') {
        if (updateDownloadInFlight) {
          return
        }
        updateDownloadInFlight = true
        updateBannerDismissed = false
        showUpdateBanner(buildUpdateBannerPayload('downloading', updateInfo, { progressPercent: 0 }))
        updater.downloadUpdate().catch((err) => {
          updateDownloadInFlight = false
          const message = err && err.message ? err.message : 'Update failed'
          showUpdateBanner(buildUpdateBannerPayload('error', updateInfo, { errorMessage: message }))
        })
        return
      }
      if (action === 'restart') {
        updater.quitAndInstall()
        return
      }
      if (action === 'dismiss') {
        updateBannerDismissed = true
        hideUpdateBanner()
        return
      }
      if (action === 'release-notes') {
        const target = payload && payload.releaseUrl ? payload.releaseUrl : UPDATE_RELEASES_URL
        shell.openExternal(target)
      }
    })

    // PROMPT
    let promptResponse
    ipcMain.on('prompt', function(eventRet, arg) {
      promptResponse = null
      const point = screen.getCursorScreenPoint()
      const display = screen.getDisplayNearestPoint(point)
      const bounds = display.bounds

//      const bounds = focused.getBounds()
      let promptWindow = new BrowserWindow({
        x: bounds.x + bounds.width/2 - 200,
        y: bounds.y + bounds.height/2 - 60,
        width: 400,
        height: 120,
        //width: 1000,
        //height: 500,
        show: false,
        resizable: false,
//        movable: false,
//        alwaysOnTop: true,
        frame: false,
        webPreferences: {
          session: session.defaultSession,
          webSecurity: false,
          spellcheck: false,
          nativeWindowOpen: true,
          contextIsolation: false,
          nodeIntegrationInSubFrames: true,
          preload: path.join(__dirname, 'preload.js')
        },
      })
      arg.val = arg.val || ''
      const promptHtml = `<html><body><form><label for="val">${arg.title}</label>
<input id="val" value="${arg.val}" autofocus />
<button id='ok'>OK</button>
<button id='cancel'>Cancel</button></form>
<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>
<script>
document.querySelector("#cancel").addEventListener("click", (e) => {
  debugger
  e.preventDefault()
  e.stopPropagation()
  window.close()
})
document.querySelector("form").addEventListener("submit", (e) => {
  e.preventDefault()
  e.stopPropagation()
  debugger
  window.electronAPI.send('prompt-response', document.querySelector("#val").value)
  window.close()
})
</script></body></html>`

//      promptWindow.loadFile("prompt.html")
      promptWindow.loadURL('data:text/html,' + encodeURIComponent(promptHtml))
      promptWindow.show()
      promptWindow.on('closed', function() {
        console.log({ promptResponse })
        debugger
        eventRet.returnValue = promptResponse
        promptWindow = null
      })

    })
    ipcMain.on('prompt-response', function(event, arg) {
      if (arg === ''){ arg = null }
      console.log("prompt-response", { arg})
      promptResponse = arg
    })


    updateSplashWindow({
      state: 'loading',
      message: 'Starting Pinokio…',
      icon: getSplashIcon()
    })
    try {
      await restoreSessionCookies()
      await clearSessionCaches()
      try {
        const portInUse = await pinokiod.running(pinokiod.port)
        if (portInUse) {
          showStartupError({
            message: 'Pinokio is already running',
            detail: `Pinokio detected another instance listening on port ${pinokiod.port}. Please close the other instance before launching a new one.`
          })
          return
        }
      } catch (checkError) {
        console.warn('Failed to verify pinokio port availability', checkError)
      }
      await pinokiod.start({
        onquit: () => {
          app.quit()
        },
        onrestart: () => {
          persistSessionCookies().finally(() => {
            app.relaunch()
            app.exit()
          })
        },
        onrefresh: (payload) => {
          try {
            updateThemeColors(payload || { theme: pinokiod.theme, colors: pinokiod.colors })
          } catch (err) {
            console.error('Failed to sync title bar theme', err)
          }
        },
        browser: {
          clearCache: async () => {
            console.log('clear cache from all sessions')
            
            // Clear default session
            await session.defaultSession.clearStorageData()
            
            // Clear all custom sessions from active windows
            const windows = BrowserWindow.getAllWindows()
            for (const window of windows) {
              if (window.webContents && window.webContents.session) {
                await window.webContents.session.clearStorageData()
              }
            }

            await clearPersistedSessionCookies()

            console.log("cleared all sessions")
          },
          requestPermissions: async (payload = {}) => {
            try {
              const project = typeof payload.name === 'string' ? payload.name.trim() : ''
              const permissions = normalizePermissionList(payload.permissions)
              logPermission('callback received', { project, permissions })
              if (!project || permissions.length === 0) {
                logPermission('callback skipped (missing project or permissions)', { project, permissions })
                return { ok: true, skipped: true }
              }
              const owner = BrowserWindow.getFocusedWindow() || mainWindow || BrowserWindow.getAllWindows()[0] || null
              const webContents = owner && owner.webContents ? owner.webContents : null
              if (!webContents || webContents.isDestroyed()) {
                logPermission('callback failed (no webContents)', { project, permissions })
                return { ok: false, error: 'no-webcontents' }
              }
              await promptForProjectPermissions(webContents, project, permissions)
              return { ok: true }
            } catch (err) {
              console.error('[PERMISSION] Failed to prompt via callback', err)
              return { ok: false, error: err && err.message ? err.message : String(err) }
            }
          }
        }
      })
    } catch (error) {
      console.error('Failed to start pinokiod', error)
      showStartupError({ error })
      return
    }
    closeSplashWindow()
    PORT = pinokiod.port
    app.on('web-contents-created', attach)
    app.on('activate', function () {
      if (BrowserWindow.getAllWindows().length === 0) createWindow(PORT)
    })
    app.on('before-quit', function(e) {
      if (pinokiod.kernel.kill) {
        if (isQuitting) {
          return
        }
        e.preventDefault()
        isQuitting = true
        persistSessionCookies().finally(() => {
          console.log('Cleaning up before quit', process.pid)
          pinokiod.kernel.kill()
        })
      }
    });
    app.on('window-all-closed', function () {
      console.log("window-all-closed")
      if (process.platform !== 'darwin') {
        // Reset all shells before quitting
        pinokiod.kernel.shell.reset()
        // wait 1 second before quitting the app
        // otherwise the app.quit() fails because the subprocesses are running
        setTimeout(() => {
          console.log("app.quit()")
          app.quit()
        }, 1000)
      }
    })
    app.on('browser-window-created', (event, win) => {
      const parentWindow = (win && typeof win.getParentWindow === 'function') ? win.getParentWindow() : null
      if (parentWindow && !parentWindow.isDestroyed()) installForceDestroyOnClose(win)
      if (win.type !== "splash") {
        if (win && typeof win.setTitleBarOverlay === 'function') {
          const overlay = titleBarOverlay(colors)
          setWindowTitleBarOverlay(win, overlay)
        }
      }
    })
    app.on('open-url', (event, url) => {
      let u = url.replace(/pinokio:[\/]+/, "")
  //    let u = new URL(url).search
  //    console.log("u", u)
      loadNewWindow(`${root_url}/pinokio/${u}`, PORT)

//      if (BrowserWindow.getAllWindows().length === 0 || !mainWindow) createWindow(PORT)
//      const topWindow = BrowserWindow.getFocusedWindow();
//      console.log("top window", topWindow)
//      //mainWindow.focus()
//      //mainWindow.loadURL(`${root_url}/pinokio/${u}`)
//      topWindow.focus()
//      topWindow.loadURL(`${root_url}/pinokio/${u}`)
    })
//    app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors')

    let all = BrowserWindow.getAllWindows()
    for(win of all) {
      try {
        if (win && typeof win.setTitleBarOverlay === 'function') {
          const overlay = titleBarOverlay(colors)
          setWindowTitleBarOverlay(win, overlay)
        }
      } catch (e) {
  //      console.log("E2", e)
      }
    }
    createWindow(PORT)
    if (updateTestMode) {
      setTimeout(() => {
        showUpdateBannerTestAvailable()
      }, 400)
    } else {
      updater.setHandlers({
        onUpdateAvailable: (info) => {
          updateInfo = info
          updateDownloadInFlight = false
          updateBannerDismissed = false
          showUpdateBanner(buildUpdateBannerPayload('available', info))
        },
        onUpdateNotAvailable: () => {
          updateInfo = null
          updateDownloadInFlight = false
          hideUpdateBanner()
        },
        onDownloadProgress: (progress) => {
          const payload = buildUpdateBannerPayload('downloading', updateInfo, {
            progressPercent: progress && typeof progress.percent === 'number' ? progress.percent : 0,
            notesPreview: buildProgressLabel(progress)
          })
          showUpdateBanner(payload)
        },
        onUpdateDownloaded: (info) => {
          updateInfo = info
          updateDownloadInFlight = false
          showUpdateBanner(buildUpdateBannerPayload('ready', info))
        },
        onError: (err) => {
          const wasDownloading = updateDownloadInFlight
          updateDownloadInFlight = false
          if (!wasDownloading) {
            console.warn('Update check error:', err)
            return
          }
          const message = err && err.message ? err.message : 'Update error'
          showUpdateBanner(buildUpdateBannerPayload('error', updateInfo, { notesPreview: message }))
        }
      })
      updater.run(mainWindow)
    }
  })

}


================================================
FILE: linux_build.sh
================================================
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 dist"


================================================
FILE: main.js
================================================
const { app } = require('electron')
const Pinokiod = require("pinokiod")
const config = require('./config')
const pinokiod = new Pinokiod(config)

if (process.platform === 'linux') {
  console.log('[PINOKIO DEBUG] Linux startup')
  console.log('[PINOKIO DEBUG] ELECTRON_OZONE_PLATFORM_HINT:', process.env.ELECTRON_OZONE_PLATFORM_HINT || '<unset>')
  console.log('[PINOKIO DEBUG] ELECTRON_DISABLE_GPU:', process.env.ELECTRON_DISABLE_GPU || '<unset>')
  console.log('[PINOKIO DEBUG] DISPLAY:', process.env.DISPLAY || '<unset>')
  console.log('[PINOKIO DEBUG] WAYLAND_DISPLAY:', process.env.WAYLAND_DISPLAY || '<unset>')
  console.log('[PINOKIO DEBUG] argv:', process.argv.join(' '))
  app.disableHardwareAcceleration()
}

let mode = pinokiod.kernel.store.get("mode") || "full"
//iprocess.env.PINOKIO_MODE = process.env.PINOKIO_MODE || 'desktop';
if (mode === 'minimal' || mode === 'background') {
  require('./minimal');
} else {
  require('./full');
}


================================================
FILE: minimal.js
================================================
const { app, Tray, Menu, shell, nativeImage, BrowserWindow, session, Notification } = require('electron');
const path = require('path')
const os = require('os')
const fs = require('fs')
const Pinokiod = require("pinokiod")
const config = require('./config')
const Updater = require('./updater')
const pinokiod = new Pinokiod(config)
const updater = new Updater()
let tray
let hiddenWindow
let rootUrl
let splashWindow
let splashIcon

const getLogFileHint = () => {
  try {
    if (pinokiod && pinokiod.kernel && pinokiod.kernel.homedir) {
      return path.resolve(pinokiod.kernel.homedir, "logs", "stdout.txt")
    }
  } catch (err) {
  }
  return path.resolve(os.homedir(), ".pinokio", "logs", "stdout.txt")
}
const ensureSplashWindow = () => {
  if (splashWindow && !splashWindow.isDestroyed()) {
    return splashWindow
  }
  splashWindow = new BrowserWindow({
    width: 420,
    height: 320,
    frame: false,
    resizable: false,
    transparent: true,
    show: false,
    alwaysOnTop: true,
    skipTaskbar: true,
    fullscreenable: false,
    webPreferences: {
      backgroundThrottling: false
    }
  })
  splashWindow.on('closed', () => {
    splashWindow = null
  })
  return splashWindow
}
const getSplashIcon = () => {
  if (splashIcon) {
    return splashIcon
  }
  const candidates = [
    path.join('assets', 'icon.png'),
    path.join('assets', 'icon_small@2x.png'),
    path.join('assets', 'icon_small.png'),
    'icon2.png'
  ]
  for (const relative of candidates) {
    const absolute = path.join(__dirname, relative)
    if (fs.existsSync(absolute)) {
      splashIcon = relative.split(path.sep).join('/')
      return splashIcon
    }
  }
  splashIcon = path.join('assets', 'icon_small.png').split(path.sep).join('/')
  return splashIcon
}
const updateSplashWindow = ({ state = 'loading', message, detail, logPath, icon } = {}) => {
  const win = ensureSplashWindow()
  const query = { state }
  if (message) {
    query.message = message
  }
  if (detail) {
    const trimmed = detail.length > 800 ? `${detail.slice(0, 800)}…` : detail
    query.detail = trimmed
  }
  if (logPath) {
    query.log = logPath
  }
  if (icon) {
    query.icon = icon
  }
  win.loadFile(path.join(__dirname, 'splash.html'), { query }).finally(() => {
    if (!win.isDestroyed()) {
      win.show()
    }
  })
}
const closeSplashWindow = () => {
  if (splashWindow && !splashWindow.isDestroyed()) {
    splashWindow.close()
  }
}
const showStartupError = ({ message, detail, error } = {}) => {
  const formatted = detail || formatStartupError(error)
  updateSplashWindow({
    state: 'error',
    message: message || 'Pinokio could not start',
    detail: formatted,
    logPath: getLogFileHint(),
    icon: getSplashIcon()
  })
}
const formatStartupError = (error) => {
  if (!error) return ''
  if (error.stack) {
    return `${error.message || 'Unknown error'}\n\n${error.stack}`
  }
  if (error.message) return error.message
  if (typeof error === 'string') return error
  try {
    return JSON.stringify(error, null, 2)
  } catch (err) {
    return String(error)
  }
}
const gotTheLock = app.requestSingleInstanceLock()

if (!gotTheLock) {
  app.quit()
}

app.on('second-instance', () => {
  if (rootUrl) {
    shell.openExternal(rootUrl)
  }
})

app.whenReady().then(async () => {
  if (!gotTheLock) {
    return
  }
  updateSplashWindow({
    state: 'loading',
    message: 'Starting Pinokio…',
    icon: getSplashIcon()
  })
  try {
    try {
      const portInUse = await pinokiod.running(pinokiod.port)
      if (portInUse) {
        showStartupError({
          message: 'Pinokio is already running',
          detail: `An existing Pinokio instance is using port ${pinokiod.port}. Please close it before launching another.`
        })
        return
      }
    } catch (checkError) {
      console.warn('Failed to verify pinokio port availability', checkError)
    }
    await pinokiod.start({
      onquit: () => {
        app.quit()
      },
      onrestart: () => {
        app.relaunch();
        app.exit()
      },
      browser: {
        clearCache: async () => {
          console.log('clear cache', session.defaultSession)
          await session.defaultSession.clearStorageData()
          console.log("cleared")
        }
      }
    })
  } catch (error) {
    console.error('Failed to start pinokiod', error)
    showStartupError({ error })
    return
  }
  let quitting = false
  app.on('before-quit', (e) => {
    if (quitting) {
      return
    }
    if (pinokiod && pinokiod.kernel && typeof pinokiod.kernel.kill === 'function') {
      quitting = true
      e.preventDefault()
      try {
        pinokiod.kernel.kill()
      } catch (err) {
        console.warn('Failed to terminate pinokiod on quit', err)
      }
    }
  })
  closeSplashWindow()
  rootUrl = `http://localhost:${pinokiod.port}`
  if (process.platform === 'darwin') app.dock.hide();
  const assetsRoot = app.isPackaged ? process.resourcesPath : __dirname
  const iconPath = path.resolve(assetsRoot, "assets/icon_small.png")
  let icon = nativeImage.createFromPath(iconPath)
  icon = icon.resize({
    height: 24,
    width: 24 
  });
  console.log('Tray icon path:', iconPath, 'isEmpty:', icon.isEmpty()); // if true, image failed to load
  tray = new Tray(icon)
  const contextMenu = Menu.buildFromTemplate([
    { label: 'Open in Browser', click: () => shell.openExternal(rootUrl) },
    { label: 'Restart', click: () => { app.relaunch(); app.exit(); } },
    { label: 'Quit', click: () => app.quit() }
  ]);
  tray.setToolTip('Pinokio');
  tray.setContextMenu(contextMenu);
  const showNotification = (options = {}) => {
    try {
      new Notification({
        title: 'Pinokio',
        body: 'Running in background',
        ...options
      }).show()
    } catch (err) {
      console.warn('Failed to show background notification', err)
    }
  }
  const announceTray = () => {
    const platformHandlers = {
      darwin: () => {
        try {
          tray.setHighlightMode('always')
          tray.setTitle('Pinokio running')
          setTimeout(() => tray.setHighlightMode('selection'), 4000)
          setTimeout(() => tray.popUpContextMenu(contextMenu), 150)
        } catch (err) {
          console.warn('Failed to signal tray/notification on macOS', err)
        }
        showNotification()
      },
      win32: () => {
        try {
          app.setAppUserModelId('Pinokio')
        } catch (err) {
          console.warn('Failed to set AppUserModelID', err)
        }
        showNotification({ icon: iconPath })
      },
      default: () => {
        showNotification()
      }
    }
    const handler = platformHandlers[process.platform] || platformHandlers.default
    handler()
  }
  announceTray()
  tray.on('click', () => {
    tray.popUpContextMenu(contextMenu);
  });
  shell.openExternal(rootUrl);
  hiddenWindow = new BrowserWindow({ show: false });

  updater.run(hiddenWindow)
});


================================================
FILE: package.json
================================================
{
  "name": "Pinokio",
  "private": true,
  "version": "7.0.0",
  "homepage": "https://pinokio.co",
  "description": "pinokio",
  "main": "main.js",
  "email": "cocktailpeanuts@proton.me",
  "author": "https://twitter.com/cocktailpeanut",
  "scripts": {
    "start": "electron .",
    "test:update-banner": "node script/run-update-banner-test.js",
    "pack": "./node_modules/.bin/electron-builder --dir",
    "eject": "hdiutil info | grep '/dev/disk' | awk '{print $1}' | xargs -I {} hdiutil detach {}",
    "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'",
    "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",
    "build2": "npm run l && npm run mw",
    "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",
    "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",
    "zip": "node script/zip",
    "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",
    "postinstall2": "npm run monkeypatch && ./node_modules/.bin/electron-builder install-app-deps",
    "fix": "brew install fpm"
  },
  "build": {
    "appId": "computer.pinokio",
    "afterPack": "after-pack.js",
    "afterSign": "electron-builder-notarize",
    "directories": {
      "output": "dist-${platform}"
    },
    "publish": [
      {
        "provider": "github",
        "owner": "pinokiocomputer",
        "repo": "pinokio"
      }
    ],
    "asarUnpack": [
      "node_modules/go-get-folder-size/**/*",
      "node_modules/7zip-bin/**/*",
      "node_modules/sweetalert2/**/*",
      "node_modules/@homebridge/**/*",
      "node_modules/pinokiod/server/public/**/*",
      "node_modules/pinokiod/server/scripts/**/*",
      "node_modules/toasted-notifier/vendor/mac.noindex/**/*"
    ],
    "nsis": {
      "include": "build/installer.nsh"
    },
    "extraResources": [
      "./script/**",
      {
        "from": "assets/icon_small.png",
        "to": "assets/icon_small.png"
      }
    ],
    "protocols": [
      {
        "name": "pinokio",
        "schemes": [
          "pinokio"
        ]
      }
    ],
    "mac": {
      "category": "utility",
      "target": [
        {
          "target": "default",
          "arch": [
            "x64",
            "arm64"
          ]
        }
      ],
      "hardenedRuntime": true,
      "entitlements": "build/entitlements.mac.plist",
      "entitlementsInherit": "build/entitlements.mac.inherit.plist",
      "extendInfo": {
        "NSMicrophoneUsageDescription": "Pinokio needs microphone access for apps that use audio.",
        "NSCameraUsageDescription": "Pinokio needs camera access for apps that use video."
      }
    },
    "dmg": {
      "backgroundColor": "#ffffff",
      "icon": null,
      "window": {
        "width": 520,
        "height": 320
      },
      "iconSize": 120,
      "contents": [
        {
          "type": "file",
          "x": 160,
          "y": 170
        },
        {
          "type": "link",
          "name": "Applications",
          "path": "/Applications",
          "x": 360,
          "y": 170
        }
      ]
    },
    "linux": {
      "maintainer": "Cocktail Peanut <cocktailpeanuts@proton.me>",
      "target": [
        {
          "target": "deb",
          "arch": [
            "x64",
            "arm64"
          ]
        },
        {
          "target": "rpm",
          "arch": [
            "x64",
            "arm64"
          ]
        },
        {
          "target": "AppImage",
          "arch": [
            "x64",
            "arm64"
          ]
        }
      ]
    },
    "win": {
      "artifactName": "Pinokio.${ext}",
      "signtoolOptions": {
        "sign": "./build/sign.js"
      },
      "target": [
        {
          "target": "nsis",
          "arch": [
            "x64"
          ]
        }
      ]
    }
  },
  "license": "MIT",
  "dependencies": {
    "electron-progressbar": "^2.2.1",
    "electron-store": "^8.1.0",
    "electron-updater": "^6.6.2",
    "electron-window-state": "^5.0.3",
    "pinokiod": "^7.0.0"
  },
  "devDependencies": {
    "@electron/rebuild": "3.2.10",
    "electron": "39.2.3",
    "electron-builder": "26.2.0",
    "electron-builder-notarize": "^1.5.2"
  }
}


================================================
FILE: patch-linux-arm64-natives.js
================================================
const fs = require('fs')
const path = require('path')

const ARCH_BY_ENUM = {
  0: 'ia32',
  1: 'x64',
  2: 'armv7l',
  3: 'arm64',
  4: 'universal'
}

const ELF_MACHINE_AARCH64 = 183

const resolveArch = (context) => {
  if (typeof context.arch === 'string') {
    return context.arch.toLowerCase()
  }

  if (typeof context.arch === 'number') {
    const mapped = ARCH_BY_ENUM[context.arch]
    if (mapped) {
      return mapped
    }
  }

  if (typeof context.appOutDir === 'string' && /arm64/i.test(context.appOutDir)) {
    return 'arm64'
  }

  return ''
}

const ensureAarch64Elf = (filePath, label) => {
  const data = fs.readFileSync(filePath)

  if (data.length < 20) {
    throw new Error(`[linux-arm64-native-fix] ${label} is too small to be an ELF binary: ${filePath}`)
  }

  if (!(data[0] === 0x7f && data[1] === 0x45 && data[2] === 0x4c && data[3] === 0x46)) {
    throw new Error(`[linux-arm64-native-fix] ${label} is not an ELF binary: ${filePath}`)
  }

  const isLittleEndian = data[5] !== 2
  const machine = isLittleEndian ? data.readUInt16LE(18) : data.readUInt16BE(18)

  if (machine !== ELF_MACHINE_AARCH64) {
    throw new Error(`[linux-arm64-native-fix] ${label} is not aarch64 (e_machine=${machine}): ${filePath}`)
  }
}

const copyWithValidation = (source, destination, label) => {
  if (!fs.existsSync(source)) {
    throw new Error(`[linux-arm64-native-fix] Missing ${label} source file: ${source}`)
  }

  ensureAarch64Elf(source, `${label} source`)

  fs.mkdirSync(path.dirname(destination), { recursive: true })
  fs.copyFileSync(source, destination)

  ensureAarch64Elf(destination, `${label} destination`)
  console.log(`[linux-arm64-native-fix] Patched ${label}: ${destination}`)
}

const existingDirectories = (candidates) => candidates.filter((candidate) => fs.existsSync(candidate))

module.exports = async (context) => {
  if (context.electronPlatformName !== 'linux') {
    return
  }

  const arch = resolveArch(context)
  if (arch !== 'arm64') {
    return
  }

  const unpackedRoot = path.join(context.appOutDir, 'resources', 'app.asar.unpacked')

  const ptyBases = existingDirectories([
    path.join(unpackedRoot, 'node_modules', '@homebridge', 'node-pty-prebuilt-multiarch'),
    path.join(unpackedRoot, 'node_modules', 'pinokiod', 'node_modules', '@homebridge', 'node-pty-prebuilt-multiarch')
  ])

  if (ptyBases.length === 0) {
    throw new Error('[linux-arm64-native-fix] Could not find @homebridge/node-pty-prebuilt-multiarch in app.asar.unpacked')
  }

  for (const ptyBase of ptyBases) {
    const source = path.join(ptyBase, 'prebuilds', 'linux-arm64', 'node.abi131.node')
    const destination = path.join(ptyBase, 'build', 'Release', 'pty.node')
    copyWithValidation(source, destination, 'node-pty')
  }

  const watcherBases = existingDirectories([
    path.join(unpackedRoot, 'node_modules', '@parcel', 'watcher'),
    path.join(unpackedRoot, 'node_modules', 'pinokiod', 'node_modules', '@parcel', 'watcher')
  ])

  if (watcherBases.length === 0) {
    throw new Error('[linux-arm64-native-fix] Could not find @parcel/watcher in app.asar.unpacked')
  }

  for (const watcherBase of watcherBases) {
    const source = path.join(path.dirname(watcherBase), 'watcher-linux-arm64-glibc', 'watcher.node')
    const destination = path.join(watcherBase, 'build', 'Release', 'watcher.node')
    copyWithValidation(source, destination, 'parcel-watcher')
  }
}


================================================
FILE: popup-shell.js
================================================
const path = require('path')
const windowStateKeeper = require('electron-window-state')
const { BrowserWindow, WebContentsView, session } = require('electron')

const parseUrl = (value, base) => {
  try {
    return new URL(value, base)
  } catch (_) {
    return null
  }
}

const isHttpUrl = (value) => {
  return Boolean(value && (value.protocol === 'http:' || value.protocol === 'https:'))
}

const getFeatureDimension = (params, key, fallback) => {
  const value = parseInt(params.get(key), 10)
  return Number.isFinite(value) ? value : fallback
}

module.exports = ({
  contentPreloadPath = path.join(__dirname, 'preload.js'),
  toolbarHtmlPath = path.join(__dirname, 'popup-toolbar.html'),
  toolbarHeight = 46,
  installForceDestroyOnClose
} = {}) => {
  let openPinokioHomeWindow = null
  const popupBrowserPartition = 'persist:pinokio-popup-browser'
  const buildBrowserLikeUserAgent = () => {
    const chromeVersion = process.versions.chrome || '140.0.0.0'
    if (process.platform === 'darwin') {
      return `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`
    }
    if (process.platform === 'win32') {
      const arch = process.arch === 'arm64' ? 'ARM64' : 'x64'
      return `Mozilla/5.0 (Windows NT 10.0; Win64; ${arch}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`
    }
    const arch = process.arch === 'arm64' ? 'aarch64' : 'x86_64'
    return `Mozilla/5.0 (X11; Linux ${arch}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`
  }
  const browserLikeUserAgent = buildBrowserLikeUserAgent()
  const getPopupBrowserSession = () => {
    const popupSession = session.fromPartition(popupBrowserPartition)
    if (!popupSession.__pinokioPopupBrowserConfigured) {
      popupSession.__pinokioPopupBrowserConfigured = true
      popupSession.setUserAgent(browserLikeUserAgent, 'en-US,en')
    }
    return popupSession
  }
  const buildAppPopupContentWebPreferences = (overrides = {}) => {
    const next = (overrides && typeof overrides === 'object') ? { ...overrides } : {}
    return {
      ...next,
      session: session.defaultSession,
      webSecurity: false,
      spellcheck: false,
      nativeWindowOpen: true,
      contextIsolation: false,
      nodeIntegrationInSubFrames: true,
      enableRemoteModule: false,
      experimentalFeatures: true,
      preload: contentPreloadPath
    }
  }
  const buildBrowserPopupContentWebPreferences = (overrides = {}) => {
    const next = (overrides && typeof overrides === 'object') ? { ...overrides } : {}
    delete next.session
    delete next.preload
    delete next.partition
    delete next.nodeIntegration
    delete next.nodeIntegrationInSubFrames
    delete next.contextIsolation
    delete next.experimentalFeatures
    delete next.webSecurity
    return {
      ...next,
      partition: popupBrowserPartition,
      sandbox: true,
      webSecurity: true,
      allowRunningInsecureContent: false,
      nativeWindowOpen: true,
      contextIsolation: true,
      nodeIntegration: false,
      nodeIntegrationInSubFrames: false,
      enableRemoteModule: false,
      experimentalFeatures: false
    }
  }

  const unwrapContainerTarget = (target, rootParsed) => {
    let next = target
    while (next && next.pathname === '/container') {
      const innerUrl = next.searchParams.get('url')
      if (!innerUrl) {
        break
      }
      const unwrapped = parseUrl(innerUrl, rootParsed ? rootParsed.origin : undefined)
      if (!isHttpUrl(unwrapped) || unwrapped.href === next.href) {
        break
      }
      next = unwrapped
    }
    return next
  }

  const isPinokioWindowUrl = (value, rootUrl) => {
    const rootParsed = parseUrl(rootUrl)
    const target = unwrapContainerTarget(
      parseUrl(value, rootParsed ? rootParsed.origin : undefined),
      rootParsed
    )
    if (!rootParsed || !isHttpUrl(target)) {
      return false
    }
    return target.origin === rootParsed.origin
  }

  const resolveTargetUrl = ({ url, openerWebContents, rootUrl } = {}) => {
    const openerUrl = (() => {
      try {
        return openerWebContents && !openerWebContents.isDestroyed()
          ? openerWebContents.getURL()
          : (rootUrl || '')
      } catch (_) {
        return rootUrl || ''
      }
    })()
    const target = parseUrl(url, openerUrl || (rootUrl || undefined))
    return isHttpUrl(target) ? target.href : ''
  }

  const buildRegularWindowOptions = ({ x, y, width, height, overlay } = {}) => {
    const options = {
      x,
      y,
      width: width || 1000,
      height: height || 800,
      minWidth: 190,
      parent: null,
      titleBarStyle: 'hidden',
      webPreferences: buildAppPopupContentWebPreferences()
    }
    if (overlay) {
      options.titleBarOverlay = overlay
    }
    return options
  }

  const createRegularWindow = ({ x, y, width, height, overlay } = {}) => {
    const win = new BrowserWindow(buildRegularWindowOptions({ x, y, width, height, overlay }))
    installForceDestroyOnClose(win)
    return win
  }

  const layoutPopupShell = (shellState) => {
    if (!shellState || !shellState.win || shellState.win.isDestroyed()) {
      return
    }
    const bounds = shellState.win.getContentBounds()
    const width = Math.max(bounds.width || 0, 0)
    const height = Math.max(bounds.height || 0, 0)
    shellState.toolbarView.setBounds({
      x: 0,
      y: 0,
      width,
      height: toolbarHeight
    })
    shellState.contentView.setBounds({
      x: 0,
      y: toolbarHeight,
      width,
      height: Math.max(height - toolbarHeight, 0)
    })
  }

  const buildPopupShellState = (shellState) => {
    const target = shellState && shellState.contentView ? shellState.contentView.webContents : null
    let url = ''
    let title = ''
    try {
      if (target && !target.isDestroyed()) {
        url = target.getURL() || ''
        title = target.getTitle() || ''
      }
    } catch (_) {
    }
    return {
      url,
      title: title || url || 'Pinokio',
      canGoBack: Boolean(target && !target.isDestroyed() && target.canGoBack()),
      canGoForward: Boolean(target && !target.isDestroyed() && target.canGoForward())
    }
  }

  const sendPopupShellState = (shellState) => {
    if (!shellState || !shellState.toolbarView || !shellState.contentView) {
      return
    }
    const toolbarContents = shellState.toolbarView.webContents
    if (!toolbarContents || toolbarContents.isDestroyed()) {
      return
    }
    const state = buildPopupShellState(shellState)
    toolbarContents.send('pinokio:popup-shell-state', state)
    if (shellState.win && !shellState.win.isDestroyed()) {
      shellState.win.setTitle(state.title)
    }
  }

  const createPopupShellWindow = ({
    x,
    y,
    width,
    height,
    adoptedWebContents = null,
    contentWebPreferences = {},
    browserLike = false,
    initialUrl = ''
  } = {}) => {
    const win = new BrowserWindow({
      frame: true,
      x,
      y,
      width: width || 1000,
      height: height || 800,
      minWidth: 190,
      backgroundColor: '#ffffff'
    })
    win.__pinokioPopupShell = true
    win.__pinokioCloseOnFirstDownload = Boolean(browserLike && initialUrl)
    installForceDestroyOnClose(win)

    const toolbarView = new WebContentsView({
      webPreferences: {
        nodeIntegration: true,
        contextIsolation: false,
        spellcheck: false,
        backgroundThrottling: false
      }
    })
    const contentView = adoptedWebContents
      ? new WebContentsView({ webContents: adoptedWebContents })
      : new WebContentsView({
          webPreferences: browserLike
            ? buildBrowserPopupContentWebPreferences(contentWebPreferences)
            : buildAppPopupContentWebPreferences(contentWebPreferences)
        })

    const shellState = {
      win,
      toolbarView,
      contentView
    }

    win.contentView.addChildView(contentView)
    win.contentView.addChildView(toolbarView)
    layoutPopupShell(shellState)

    const syncShellState = () => {
      layoutPopupShell(shellState)
      sendPopupShellState(shellState)
    }
    const focusContent = () => {
      if (contentView.webContents && !contentView.webContents.isDestroyed()) {
        contentView.webContents.focus()
      }
    }

    toolbarView.webContents.on('did-finish-load', () => {
      sendPopupShellState(shellState)
    })
    toolbarView.webContents.on('ipc-message', (_event, channel) => {
      const target = contentView.webContents
      if (!target || target.isDestroyed()) {
        return
      }
      if (channel === 'pinokio:popup-shell-back') {
        if (target.canGoBack()) {
          target.goBack()
        }
        return
      }
      if (channel === 'pinokio:popup-shell-forward') {
        if (target.canGoForward()) {
          target.goForward()
        }
        return
      }
      if (channel === 'pinokio:popup-shell-refresh') {
        target.reload()
        return
      }
      if (channel === 'pinokio:popup-shell-open-home') {
        if (typeof openPinokioHomeWindow === 'function') {
          openPinokioHomeWindow()
        }
      }
    })
    if (browserLike && contentView.webContents && !contentView.webContents.isDestroyed()) {
      getPopupBrowserSession()
      contentView.webContents.setUserAgent(browserLikeUserAgent)
    }
    contentView.webContents.on('did-finish-load', () => {
      if (shellState.win && !shellState.win.isDestroyed()) {
        shellState.win.__pinokioCloseOnFirstDownload = false
      }
      syncShellState()
      focusContent()
    })
    contentView.webContents.on('did-navigate', syncShellState)
    contentView.webContents.on('did-navigate-in-page', syncShellState)
    contentView.webContents.on('page-title-updated', (event) => {
      event.preventDefault()
      sendPopupShellState(shellState)
    })
    win.on('focus', focusContent)
    win.on('resize', syncShellState)

    toolbarView.webContents.loadFile(toolbarHtmlPath).catch((error) => {
      console.error('[pinokio][popup-shell] failed to load toolbar', error)
    })
    if (initialUrl) {
      contentView.webContents.loadURL(initialUrl).catch((error) => {
        console.error('[pinokio][popup-shell] failed to load content url', { initialUrl, error })
      })
    }
    return shellState
  }

  const allowPermissions = (targetSession) => {
    if (!targetSession) {
      return
    }
    targetSession.setPermissionRequestHandler((_webContents, _permission, callback) => {
      callback(true)
    })
  }

  const createPopupWindowState = () => {
    if (typeof windowStateKeeper !== 'function') {
      return {
        x: undefined,
        y: undefined,
        width: 1000,
        height: 800,
        manage: () => {}
      }
    }
    return windowStateKeeper({
//    file: "index.json",
      defaultWidth: 1000,
      defaultHeight: 800
    })
  }

  const createPopupResponse = ({ params, width, height, x, y } = {}) => {
    return {
      action: 'allow',
      outlivesOpener: true,
      overrideBrowserWindowOptions: {
        webPreferences: buildBrowserPopupContentWebPreferences()
      },
      createWindow: (options = {}) => {
        const shellState = createPopupShellWindow({
          width: getFeatureDimension(params, 'width', width),
          height: getFeatureDimension(params, 'height', height),
          x: x + 30,
          y: y + 30,
          adoptedWebContents: options.webContents || null,
          contentWebPreferences: options.webPreferences || {},
          browserLike: true
        })
        return shellState.contentView.webContents
      }
    }
  }

  const openExternalWindow = ({ url, windowState } = {}) => {
    const nextWindowState = windowState || createPopupWindowState()
    const shellState = createPopupShellWindow({
      x: nextWindowState.x,
      y: nextWindowState.y,
      width: nextWindowState.width,
      height: nextWindowState.height,
      browserLike: true,
      initialUrl: url
    })
    const win = shellState.win
    allowPermissions(shellState.contentView.webContents.session)
    win.focus()
    nextWindowState.manage(win)
    return win
  }

  return {
    createPopupResponse,
    getPopupBrowserSession,
    isPinokioWindowUrl,
    resolveTargetUrl,
    openExternalWindow,
    setPinokioHomeWindowOpener: (nextOpener) => {
      openPinokioHomeWindow = typeof nextOpener === 'function' ? nextOpener : null
    }
  }
}


================================================
FILE: popup-toolbar.html
================================================
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta
      http-equiv="Content-Security-Policy"
      content="default-src 'self' 'unsafe-inline'; img-src 'self' data:;"
    />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Popup Toolbar</title>
    <style>
      :root {
        color-scheme: light;
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      }
      html, body {
        margin: 0;
        padding: 0;
        background: #f8fafc;
        color: #0f172a;
        overflow: hidden;
      }
      body {
        height: 100vh;
        border-bottom: 1px solid rgba(15, 23, 42, 0.08);
      }
      .popup-toolbar {
        height: 100%;
        display: flex;
        align-items: center;
        gap: 10px;
        padding: 8px 12px;
        box-sizing: border-box;
      }
      .popup-toolbar__group {
        display: flex;
        align-items: center;
        gap: 8px;
        flex: 0 0 auto;
      }
      .popup-toolbar__logo {
        width: 18px;
        height: 18px;
        display: block;
      }
      .popup-toolbar__url {
        flex: 1 1 auto;
        min-width: 0;
        padding: 7px 10px;
        border-radius: 8px;
        background: #ffffff;
        border: 1px solid rgba(15, 23, 42, 0.12);
        font-size: 12px;
        line-height: 1.3;
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
        user-select: text;
      }
      .popup-toolbar__button {
        flex: 0 0 auto;
        border: 0;
        border-radius: 6px;
        padding: 6px 8px;
        background: transparent;
        color: #0f172a;
        cursor: pointer;
        font: inherit;
        font-size: 12px;
        font-weight: 500;
      }
      .popup-toolbar__button:hover {
        background: rgba(15, 23, 42, 0.06);
      }
      .popup-toolbar__button--icon {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-width: 30px;
        padding: 6px;
      }
      .popup-toolbar__button:disabled {
        cursor: default;
        opacity: 0.55;
      }
    </style>
  </head>
  <body>
    <div class="popup-toolbar">
      <div class="popup-toolbar__group">
        <button class="popup-toolbar__button popup-toolbar__button--icon" id="open-home" type="button" title="Open Pinokio Home">
          <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" />
        </button>
        <button class="popup-toolbar__button popup-toolbar__button--icon" id="go-back" type="button" title="Back" disabled>&larr;</button>
        <button class="popup-toolbar__button popup-toolbar__button--icon" id="go-forward" type="button" title="Forward" disabled>&rarr;</button>
        <button class="popup-toolbar__button popup-toolbar__button--icon" id="refresh" type="button" title="Refresh">&#8635;</button>
      </div>
      <div class="popup-toolbar__url" id="popup-url" title="Loading...">Loading...</div>
      <button class="popup-toolbar__button" id="open-browser" type="button" disabled>Open in Browser</button>
    </div>
    <script>
      const { ipcRenderer, shell } = require('electron')

      const state = { url: '', canGoBack: false, canGoForward: false }
      const urlNode = document.getElementById('popup-url')
      const homeButton = document.getElementById('open-home')
      const backButton = document.getElementById('go-back')
      const forwardButton = document.getElementById('go-forward')
      const refreshButton = document.getElementById('refresh')
      const openButton = document.getElementById('open-browser')

      const render = () => {
        const value = state.url || 'Loading...'
        urlNode.textContent = value
        urlNode.title = value
        backButton.disabled = !state.canGoBack
        forwardButton.disabled = !state.canGoForward
        openButton.disabled = !state.url
      }

      ipcRenderer.on('pinokio:popup-shell-state', (_event, payload = {}) => {
        state.url = typeof payload.url === 'string' ? payload.url : ''
        state.canGoBack = Boolean(payload.canGoBack)
        state.canGoForward = Boolean(payload.canGoForward)
        render()
      })

      homeButton.addEventListener('click', () => {
        ipcRenderer.send('pinokio:popup-shell-open-home')
      })
      backButton.addEventListener('click', () => {
        ipcRenderer.send('pinokio:popup-shell-back')
      })
      forwardButton.addEventListener('click', () => {
        ipcRenderer.send('pinokio:popup-shell-forward')
      })
      refreshButton.addEventListener('click', () => {
        ipcRenderer.send('pinokio:popup-shell-refresh')
      })
      openButton.addEventListener('click', () => {
        if (!state.url) {
          return
        }
        shell.openExternal(state.url).catch(() => {})
      })

      render()
    </script>
  </body>
</html>


================================================
FILE: preload.js
================================================
// put this preload for main-window to give it prompt()
const { ipcRenderer, } = require('electron')
window.prompt = function(title, val){
  return ipcRenderer.sendSync('prompt', {title, val})
}
try {
} catch (_) {
}
const sendPinokio = (action) => {
  if (!action) {
    return
  }
  try {
    if (window.parent === window.top) {
      window.parent.postMessage({ action }, "*")
    }
  } catch (_) {
  }
}

// Only apply frame bridge hooks inside embedded pages.
let isEmbeddedFrame = false
let isDirectChildFrame = false
try {
  isEmbeddedFrame = window.parent !== window
  isDirectChildFrame = isEmbeddedFrame && window.parent === window.top
} catch (_) {
  isEmbeddedFrame = false
  isDirectChildFrame = false
}
let previousFrameUrl = isEmbeddedFrame ? document.location.href : ''
const publishFrameLocation = () => {
  if (!isEmbeddedFrame) {
    return
  }
  const currentUrl = document.location.href
  if (currentUrl === previousFrameUrl) {
    return
  }
  previousFrameUrl = currentUrl
  if (isDirectChildFrame) {
    sendPinokio({
      type: 'location',
      url: currentUrl
    })
  }
  syncPinokioInjectors('location').catch(() => {})
}
if (isEmbeddedFrame) {
  if (isDirectChildFrame) {
    sendPinokio({
      type: 'location',
      url: previousFrameUrl
    })
  }
  const originalPushState = history.pushState
  history.pushState = function pushStateWithPinokioLocation(...args) {
    const result = originalPushState.apply(this, args)
    publishFrameLocation()
    return result
  }
  const originalReplaceState = history.replaceState
  history.replaceState = function replaceStateWithPinokioLocation(...args) {
    const result = originalReplaceState.apply(this, args)
    publishFrameLocation()
    return result
  }
  window.addEventListener('popstate', publishFrameLocation)
  window.addEventListener('hashchange', publishFrameLocation)
  window.addEventListener('beforeunload', () => {
    resetPinokioInjectors('unload').catch(() => {})
  }, { once: true })
  window.addEventListener('message', (event) => {
    if (event.data) {
      if (event.data.action === 'back') {
        history.back()
      } else if (event.data.action === 'forward') {
        history.forward()
      } else if (event.data.action === 'refresh') {
        location.reload()
      }
    }
  })
}


//document.addEventListener("DOMContentLoaded", (e) => {
//  if (window.parent === window.top) {
//    window.parent.postMessage({
//      action: {
//        type: "title",
//        text: document.title
//      }
//    }, "*")
//  }
//})
window.electronAPI = {
  send: (type, msg) => {
    ipcRenderer.send(type, msg)
  },
  sendSync: (type, msg) => ipcRenderer.sendSync(type, msg),
  requestPermissions: (payload) => ipcRenderer.invoke('pinokio:request-permissions', payload || {}),
  startInspector: (payload) => ipcRenderer.invoke('pinokio:start-inspector', payload || {}),
  stopInspector: () => ipcRenderer.invoke('pinokio:stop-inspector'),
  captureScreenshot: (screenshotRequest) => {
    return ipcRenderer.invoke('pinokio:capture-screenshot-debug', { screenshotRequest })
  }
}
const resolvePinokioTargetWindow = () => {
  try {
    if (window.parent && window.parent !== window) {
      return window.parent
    }
  } catch (_) {
  }
  try {
    if (window.top && window.top !== window) {
      return window.top
    }
  } catch (_) {
  }
  return window
}
const postPinokioEvent = (eventName, payload = {}, context = {}) => {
  const target = resolvePinokioTargetWindow()
  const nextContext = (context && typeof context === 'object') ? { ...context } : {}
  if (!nextContext.frameUrl) {
    nextContext.frameUrl = window.location.href
  }
  if (!nextContext.workspace) {
    const workspaceHint = resolvePinokioWorkspaceHint()
    if (workspaceHint) {
      nextContext.workspace = workspaceHint
    }
  }
  target.postMessage({
    e: 'pinokio:event',
    event: eventName,
    payload: (payload && typeof payload === 'object') ? payload : {},
    context: nextContext
  }, '*')
}
const ensurePinokioApi = () => {
  const api = (window.$pinokio && typeof window.$pinokio === 'object')
    ? window.$pinokio
    : {}
  api.trigger = (eventName, payload = {}, context = {}) => {
    if (typeof eventName !== 'string' || !eventName.trim()) {
      return { ok: false, handled: false, reason: 'invalid_event_name' }
    }
    const normalizedEvent = eventName.trim()
    postPinokioEvent(
      normalizedEvent,
      (payload && typeof payload === 'object') ? payload : {},
      (context && typeof context === 'object') ? context : {}
    )
    return { ok: true, handled: 
Download .txt
gitextract_4ttzgoi5/

├── .github/
│   └── workflows/
│       ├── build.yml
│       └── test.yml
├── .gitignore
├── LICENSE
├── README.md
├── RELEASE.md
├── after-pack.js
├── build/
│   ├── entitlements.mac.inherit.plist
│   ├── entitlements.mac.plist
│   ├── icon.icns
│   ├── installer.nsh
│   └── sign.js
├── chmod.js
├── config.js
├── full.js
├── linux_build.sh
├── main.js
├── minimal.js
├── package.json
├── patch-linux-arm64-natives.js
├── popup-shell.js
├── popup-toolbar.html
├── preload.js
├── prompt.html
├── script/
│   ├── patch.command
│   ├── run-update-banner-test.js
│   └── zip.js
├── splash.html
├── temp/
│   ├── rebuild.js
│   └── yarn.js
├── update-banner.html
├── updater.js
└── wrap-linux-launcher.js
Download .txt
SYMBOL INDEX (43 symbols across 6 files)

FILE: full.js
  constant UPDATE_RELEASES_URL (line 33) | const UPDATE_RELEASES_URL = 'https://github.com/peanutcocktail/pinokio/r...
  constant PORT (line 189) | let PORT
  constant ENABLE_BROWSER_CONSOLE_LOG (line 201) | const ENABLE_BROWSER_CONSOLE_LOG = process.env.PINOKIO_BROWSER_LOG === '1'
  constant PINOKIO_INJECT_ISOLATED_WORLD_ID (line 213) | const PINOKIO_INJECT_ISOLATED_WORLD_ID = 42000
  constant SESSION_COOKIE_TTL_DAYS (line 531) | const SESSION_COOKIE_TTL_DAYS = 90
  constant SESSION_COOKIE_TTL_SEC (line 532) | const SESSION_COOKIE_TTL_SEC = SESSION_COOKIE_TTL_DAYS * 24 * 60 * 60
  constant SESSION_COOKIE_JAR_FILENAME (line 533) | const SESSION_COOKIE_JAR_FILENAME = 'session-cookies.json'
  function UpsertKeyValue (line 700) | function UpsertKeyValue(obj, keyToChange, value) {
  constant PINOKIO_ABSOLUTE_URL_PATTERN (line 2073) | const PINOKIO_ABSOLUTE_URL_PATTERN = /^[a-zA-Z][a-zA-Z\d+\-.]*:/
  method trigger (line 2158) | trigger(eventName, payload = {}, context = {}) {
  method register (line 2173) | register(definition) {
  method run (line 2193) | run(descriptor, context, runSource) {
  method unmountAll (line 2211) | unmountAll() {

FILE: patch-linux-arm64-natives.js
  constant ARCH_BY_ENUM (line 4) | const ARCH_BY_ENUM = {
  constant ELF_MACHINE_AARCH64 (line 12) | const ELF_MACHINE_AARCH64 = 183

FILE: preload.js
  function syncPinokioInjectors (line 328) | async function syncPinokioInjectors(reason = 'load') {

FILE: temp/rebuild.js
  class Rebuilder (line 43) | class Rebuilder {
    method constructor (line 44) | constructor(options) {
    method ABI (line 86) | get ABI() {
    method buildType (line 93) | get buildType() {
    method rebuild (line 96) | async rebuild() {
    method rebuildModuleAt (line 118) | async rebuildModuleAt(modulePath) {
  function rebuild (line 157) | function rebuild(options) {

FILE: temp/yarn.js
  function installOrRebuild (line 11) | async function installOrRebuild(config, appDir, options, forceInstall = ...
  function getElectronGypCacheDir (line 33) | function getElectronGypCacheDir() {
  function getGypEnv (line 36) | function getGypEnv(frameworkInfo, platform, arch, buildFromSource) {
  function checkYarnBerry (line 68) | function checkYarnBerry() {
  function installDependencies (line 76) | function installDependencies(appDir, options) {
  function nodeGypRebuild (line 108) | async function nodeGypRebuild(arch) {
  function getPackageToolPath (line 112) | function getPackageToolPath() {
  function isRunningYarn (line 120) | function isRunningYarn(execPath) {
  function rebuild (line 125) | async function rebuild(appDir, buildFromSource, options) {

FILE: updater.js
  class Updater (line 5) | class Updater {
    method constructor (line 6) | constructor(handlers = {}) {
    method setHandlers (line 12) | setHandlers(handlers = {}) {
    method run (line 16) | run(mainWindow, handlers = null) {
    method downloadUpdate (line 76) | downloadUpdate() {
    method quitAndInstall (line 80) | quitAndInstall() {
    method showDefaultUpdatePrompt (line 84) | showDefaultUpdatePrompt(info) {
    method startDefaultDownload (line 100) | startDefaultDownload() {
    method updateDefaultProgress (line 122) | updateDefaultProgress(progress) {
    method showDefaultRestartPrompt (line 129) | showDefaultRestartPrompt(info) {
    method closeProgressBar (line 146) | closeProgressBar() {
Condensed preview — 33 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (281K chars).
[
  {
    "path": ".github/workflows/build.yml",
    "chars": 15404,
    "preview": "name: Build/release\n\non:\n  push:\n    branches:\n      - main\n\n#on: workflow_dispatch\n\njobs:\n\n  create-release:\n    runs-o"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 778,
    "preview": "name: Test\n\n#on: push\non: workflow_dispatch\n\njobs:\n  print:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout"
  },
  {
    "path": ".gitignore",
    "chars": 50,
    "preview": "node_modules\ndist\ncache\npackage-lock.json\n.claude\n"
  },
  {
    "path": "LICENSE",
    "chars": 1047,
    "preview": "Copyright 2023 Pinokio\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software an"
  },
  {
    "path": "README.md",
    "chars": 6437,
    "preview": "# Pinokio\n\nLaunch Anything.\n\n# Script Policy\n\nPinokio is a 1-click launcher for any open-source project. Think of it as "
  },
  {
    "path": "RELEASE.md",
    "chars": 349,
    "preview": "# Pinokio Release\n\n## Code Signing Policy\n\nFree code signing provided by [SignPath.io](https://signpath.io/), certificat"
  },
  {
    "path": "after-pack.js",
    "chars": 321,
    "preview": "module.exports = async (context) => {\n  const chmodHandler = require('./chmod')\n  const wrapLinuxLauncher = require('./w"
  },
  {
    "path": "build/entitlements.mac.inherit.plist",
    "chars": 409,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "build/entitlements.mac.plist",
    "chars": 534,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "build/installer.nsh",
    "chars": 155,
    "preview": "# https://github.com/electron-userland/electron-builder/issues/6865#issuecomment-1871121350\n!macro customInit\n  Delete \""
  },
  {
    "path": "build/sign.js",
    "chars": 104,
    "preview": "module.exports = async function () {\n  // no-op: prevents electron-builder from calling signtool.exe\n};\n"
  },
  {
    "path": "chmod.js",
    "chars": 599,
    "preview": "const exec = require('child_process').exec;\nmodule.exports = async (context) => {\n  const paths = [\n    `${context.appOu"
  },
  {
    "path": "config.js",
    "chars": 718,
    "preview": "const Store = require('electron-store');\nconst packagejson = require(\"./package.json\")\nconst store = new Store();\nmodule"
  },
  {
    "path": "full.js",
    "chars": 128509,
    "preview": "const {app, screen, shell, BrowserWindow, BrowserView, ipcMain, dialog, clipboard, session, desktopCapturer, systemPrefe"
  },
  {
    "path": "linux_build.sh",
    "chars": 228,
    "preview": "docker run --rm -ti \\\n  -v \"$PWD:/project\" \\\n  -w /project \\\n  -e SNAPCRAFT_BUILD_ENVIRONMENT=host \\\n  -e SNAP_DESTRUCTI"
  },
  {
    "path": "main.js",
    "chars": 951,
    "preview": "const { app } = require('electron')\nconst Pinokiod = require(\"pinokiod\")\nconst config = require('./config')\nconst pinoki"
  },
  {
    "path": "minimal.js",
    "chars": 6924,
    "preview": "const { app, Tray, Menu, shell, nativeImage, BrowserWindow, session, Notification } = require('electron');\nconst path = "
  },
  {
    "path": "package.json",
    "chars": 4994,
    "preview": "{\n  \"name\": \"Pinokio\",\n  \"private\": true,\n  \"version\": \"7.0.0\",\n  \"homepage\": \"https://pinokio.co\",\n  \"description\": \"pi"
  },
  {
    "path": "patch-linux-arm64-natives.js",
    "chars": 3415,
    "preview": "const fs = require('fs')\nconst path = require('path')\n\nconst ARCH_BY_ENUM = {\n  0: 'ia32',\n  1: 'x64',\n  2: 'armv7l',\n  "
  },
  {
    "path": "popup-shell.js",
    "chars": 12469,
    "preview": "const path = require('path')\nconst windowStateKeeper = require('electron-window-state')\nconst { BrowserWindow, WebConten"
  },
  {
    "path": "popup-toolbar.html",
    "chars": 8330,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta\n      http-equiv=\"Content-Security-Poli"
  },
  {
    "path": "preload.js",
    "chars": 43737,
    "preview": "// put this preload for main-window to give it prompt()\nconst { ipcRenderer, } = require('electron')\nwindow.prompt = fun"
  },
  {
    "path": "prompt.html",
    "chars": 904,
    "preview": "<html>\n<head>\n<style>body {font-family: sans-serif;} button {float:right; margin-left: 10px;} label { display: block; ma"
  },
  {
    "path": "script/patch.command",
    "chars": 64,
    "preview": "sudo -s xattr -d com.apple.quarantine /Applications/Pinokio.app\n"
  },
  {
    "path": "script/run-update-banner-test.js",
    "chars": 301,
    "preview": "const { spawn } = require('child_process')\n\nconst cmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'\nconst child = s"
  },
  {
    "path": "script/zip.js",
    "chars": 2804,
    "preview": "const { exec } = require('child_process');\nconst path = require('path')\nconst fs = require('fs')\nconst version = process"
  },
  {
    "path": "splash.html",
    "chars": 3302,
    "preview": "<html>\n<head>\n<style>\nhtml, body {\n  width: 100%;\n  height: 100%;\n  margin: 0;\n  font-family: -apple-system, BlinkMacSys"
  },
  {
    "path": "temp/rebuild.js",
    "chars": 7572,
    "preview": "\"use strict\";\nvar __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {\n    if ("
  },
  {
    "path": "temp/yarn.js",
    "chars": 5649,
    "preview": "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.rebuild = exports.nodeGypRebuild = "
  },
  {
    "path": "update-banner.html",
    "chars": 6768,
    "preview": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Pinokio Update</title>\n    <style>\n      :root {"
  },
  {
    "path": "updater.js",
    "chars": 4478,
    "preview": "const { autoUpdater } = require(\"electron-updater\");\nconst ProgressBar = require('electron-progressbar');\nconst { dialog"
  },
  {
    "path": "wrap-linux-launcher.js",
    "chars": 1500,
    "preview": "const fs = require('fs')\nconst path = require('path')\nmodule.exports = async (context) => {\n  const { appOutDir, electro"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the pinokiocomputer/pinokio GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 33 files (263.5 KB), approximately 66.9k tokens, and a symbol index with 43 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!