diff --git a/CHANGELOG.md b/CHANGELOG.md index bbad12e..ada89bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.0.17] - 2026-05-03 + +### Added + +- **Webview Context Menu.** Right-clicking inside the webview now shows a native context menu with Cut, Copy, Paste, Undo/Redo, spell-check suggestions, and "Open Link in Browser" — enabling system autofill and password manager integration on login pages (#161). + +### Changed + +- **Windows OpenSSL Compatibility.** The bundled Python's directory is now prepended to `PATH` on Windows so its own OpenSSL DLLs are loaded before any conflicting system-wide installations (Git for Windows, Anaconda, Strawberry Perl, etc.), preventing the `OPENSSL_Uplink: no OPENSSL_Applink` crash on startup (#167). +- **Links Open in Default Browser on Windows.** Added `allowpopups` to the webview so that `target="_blank"` link clicks correctly propagate to the main process handler and open in the default browser instead of being silently blocked (#165, #170). +- **Linux System Requirements.** Documentation now specifies glibc 2.28+ as a minimum requirement for Linux installations. + ## [0.0.16] - 2026-05-02 ### Fixed diff --git a/README.md b/README.md index eb7e066..9a732fa 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Use both at the same time. |--|-------------|-------------| | **Disk** | 5 GB+ | ~500 MB | | **RAM** | 16 GB+ | 4 GB | -| **OS** | macOS 12+, Windows 10+, modern Linux | Same | +| **OS** | macOS 12+, Windows 10+, modern Linux (glibc 2.28+) | Same | > [!NOTE] > Local models need serious RAM (7B ≈ 8 GB, 13B ≈ 16 GB). Lighter machine? Connect to a remote server instead. diff --git a/package.json b/package.json index 777e33c..53d672f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-webui", - "version": "0.0.16", + "version": "0.0.17", "license": "AGPL-3.0", "description": "Open WebUI Desktop", "main": "./out/main/index.js", diff --git a/src/main/index.ts b/src/main/index.ts index 3640fa6..06b8903 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1272,6 +1272,61 @@ if (!gotTheLock) { // Malformed URL — let it through so Chromium can handle/reject it } }) + + // ── Native right-click context menu (#161) ────────────────── + // Electron guests don't show a context menu by default, + // which blocks right-click → Paste / Autofill / password-manager + // integration on login pages. Build a native menu with standard + // editing actions, spell-check suggestions, and link handling. + contents.on('context-menu', (_event, params) => { + const menuItems: Electron.MenuItemConstructorOptions[] = [] + + // Spell-check suggestions (if any) + if (params.misspelledWord && params.dictionarySuggestions?.length) { + for (const suggestion of params.dictionarySuggestions) { + menuItems.push({ + label: suggestion, + click: () => contents.replaceMisspelling(suggestion) + }) + } + menuItems.push({ type: 'separator' }) + } + + // Link handling + if (params.linkURL) { + menuItems.push({ + label: 'Open Link in Browser', + click: () => openUrl(params.linkURL) + }) + menuItems.push({ + label: 'Copy Link', + click: () => clipboard.writeText(params.linkURL) + }) + menuItems.push({ type: 'separator' }) + } + + // Editable field actions (input, textarea, contenteditable) + if (params.isEditable) { + menuItems.push( + { label: 'Undo', role: 'undo', enabled: params.editFlags.canUndo }, + { label: 'Redo', role: 'redo', enabled: params.editFlags.canRedo }, + { type: 'separator' }, + { label: 'Cut', role: 'cut', enabled: params.editFlags.canCut }, + { label: 'Copy', role: 'copy', enabled: params.editFlags.canCopy }, + { label: 'Paste', role: 'paste', enabled: params.editFlags.canPaste }, + { label: 'Select All', role: 'selectAll', enabled: params.editFlags.canSelectAll } + ) + } else if (params.selectionText) { + // Non-editable text selection + menuItems.push( + { label: 'Copy', role: 'copy', enabled: params.editFlags.canCopy } + ) + } + + if (menuItems.length > 0) { + Menu.buildFromTemplate(menuItems).popup() + } + }) } }) diff --git a/src/main/utils/index.ts b/src/main/utils/index.ts index 93a6efd..ad57292 100644 --- a/src/main/utils/index.ts +++ b/src/main/utils/index.ts @@ -318,10 +318,7 @@ export const installPython = async (installationDir?: string, onStatus?: (status ['-m', 'pip', 'install', 'uv'], { encoding: 'utf-8', - env: { - ...process.env, - ...(process.platform === 'win32' ? { PYTHONIOENCODING: 'utf-8' } : {}) - } + env: pythonEnv() }, (error) => { if (error) reject(error) @@ -350,6 +347,38 @@ export const getPythonPath = (installationDir?: string) => { return path.normalize(getPythonExecutablePath(installationDir || getPythonInstallationDir())) } +/** + * Build a process environment suitable for running the bundled Python. + * + * On Windows the standalone Python distribution ships its own OpenSSL DLLs + * (`libssl-3-x64.dll`, `libcrypto-3-x64.dll`) next to `python.exe`. If a + * different OpenSSL installation (Git for Windows, Anaconda, Strawberry Perl, + * etc.) appears earlier on the system `PATH`, Python picks up those mismatched + * DLLs at load-time, which causes the fatal error: + * + * OPENSSL_Uplink(..., 08): no OPENSSL_Applink + * + * To prevent this we prepend the Python installation directory to `PATH` so + * Windows finds the correct DLLs first. On non-Windows platforms this is a + * harmless no-op. + * + * Any additional env overrides (e.g. `configEnvVars`) can be spread after + * calling this helper. + */ +const pythonEnv = (extra: Record = {}): Record => { + const base: Record = { ...process.env } + + if (process.platform === 'win32') { + // python.exe lives at the root of the installation directory on Windows + const pythonDir = getPythonInstallationDir() + const currentPath = process.env['PATH'] || process.env['Path'] || '' + base['PATH'] = `${pythonDir};${currentPath}` + base['PYTHONIOENCODING'] = 'utf-8' + } + + return { ...base, ...extra } +} + export const isPythonInstalled = (installationDir?: string) => { const pythonPath = getPythonPath(installationDir) if (!fs.existsSync(pythonPath)) { @@ -358,10 +387,7 @@ export const isPythonInstalled = (installationDir?: string) => { try { const pythonVersion = execFileSync(pythonPath, ['--version'], { encoding: 'utf-8', - env: { - ...process.env, - ...(process.platform === 'win32' ? { PYTHONIOENCODING: 'utf-8' } : {}) - } + env: pythonEnv() }) log.info('Installed Python Version:', pythonVersion.trim()) return true @@ -375,10 +401,7 @@ export const isUvInstalled = (installationDir?: string) => { try { const result = execFileSync(pythonPath, ['-m', 'uv', '--version'], { encoding: 'utf-8', - env: { - ...process.env, - ...(process.platform === 'win32' ? { PYTHONIOENCODING: 'utf-8' } : {}) - } + env: pythonEnv() }) log.info('Installed uv Version:', result.trim()) return true @@ -428,10 +451,7 @@ export const installPackage = (packageName: string, version?: string, onStatus?: ...(version ? [`${packageName}==${version}`] : [packageName, '-U']) ], { - env: { - ...process.env, - ...(process.platform === 'win32' ? { PYTHONIOENCODING: 'utf-8' } : {}) - } + env: pythonEnv() } ) @@ -486,10 +506,7 @@ export const isPackageInstalled = (packageName: string): boolean => { try { const info = execFileSync(pythonPath, ['-m', 'uv', 'pip', 'show', packageName], { encoding: 'utf-8', - env: { - ...process.env, - ...(process.platform === 'win32' ? { PYTHONIOENCODING: 'utf-8' } : {}) - } + env: pythonEnv() }) return info.includes(`Name: ${packageName}`) } catch { @@ -503,10 +520,7 @@ export const getPackageVersion = (packageName: string): string | null => { try { const info = execFileSync(pythonPath, ['-m', 'uv', 'pip', 'show', packageName], { encoding: 'utf-8', - env: { - ...process.env, - ...(process.platform === 'win32' ? { PYTHONIOENCODING: 'utf-8' } : {}) - } + env: pythonEnv() }) const match = info.match(/^Version:\s*(.+)$/m) return match ? match[1].trim() : null @@ -521,10 +535,7 @@ export const uninstallPackage = (packageName: string): boolean => { try { execFileSync(pythonPath, ['-m', 'uv', 'pip', 'uninstall', packageName], { encoding: 'utf-8', - env: { - ...process.env, - ...(process.platform === 'win32' ? { PYTHONIOENCODING: 'utf-8' } : {}) - } + env: pythonEnv() }) log.info(`Uninstalled package: ${packageName}`) return true @@ -588,14 +599,12 @@ export const startServer = async ( name: 'xterm-256color', cols: 200, rows: 50, - env: { - ...process.env, + env: pythonEnv({ ...(configEnvVars ?? {}), DATA_DIR: dataDir, WEBUI_SECRET_KEY: secretKey, - PYTHONUNBUFFERED: '1', - ...(process.platform === 'win32' ? { PYTHONIOENCODING: 'utf-8' } : {}) - } + PYTHONUNBUFFERED: '1' + }) }) } catch (error) { throw new Error( diff --git a/src/renderer/src/lib/components/Main/Connections/Content.svelte b/src/renderer/src/lib/components/Main/Connections/Content.svelte index adf3874..7bd8c59 100644 --- a/src/renderer/src/lib/components/Main/Connections/Content.svelte +++ b/src/renderer/src/lib/components/Main/Connections/Content.svelte @@ -270,6 +270,7 @@ style="display: {view === 'connected' && activeConnectionId === connId ? 'flex' : 'none'}" partition="persist:connection-{connId}" preload={contentPreloadPath} + allowpopups > {/each}