Compare commits

...

12 Commits

Author SHA1 Message Date
Timothy Jaeryang Baek 64c399738e fix: store HF models directly under models/ for llama-server discovery
llama-server's --models-dir scanner only checks one level of
subdirectories. Downloaded models were stored two levels deep at
models/huggingface/<repo-slug>/<filename>, making them invisible.

Changed the cache directory from models/huggingface/ to models/ so
models land at models/<repo-slug>/<filename> — exactly one level deep.

Includes automatic migration of existing models from the legacy
huggingface/ subdirectory on first run.

Fixes #177
2026-05-05 00:21:59 +09:00
Timothy Jaeryang Baek 842c1481f6 fix: allow webview popups so links open in default browser on Windows (#165, #170) 2026-05-03 00:42:27 +09:00
Timothy Jaeryang Baek e7eef2da86 chore: bump version to 0.0.17 2026-05-03 00:09:33 +09:00
Timothy Jaeryang Baek aab2a687cc refac 2026-05-03 00:06:03 +09:00
Timothy Jaeryang Baek 9e204e78b7 doc 2026-05-02 22:37:04 +09:00
Timothy Jaeryang Baek ef53d6fb21 fix: open links in default browser instead of within the app (#165) 2026-05-02 19:04:15 +09:00
Timothy Jaeryang Baek 21e8a36e0d chore: bump version to 0.0.15 2026-04-28 14:32:36 +09:00
Timothy Jaeryang Baek fcb32f93ab feat: add ARM64 support for Linux and Windows
- Add ubuntu-24.04-arm and windows-11-arm to CI matrix
- Linux ARM64 builds AppImage + deb (Raspberry Pi, DGX Spark)
- Windows ARM64 builds NSIS installer (Snapdragon devices)
- Add arch to NSIS and AppImage artifact names to prevent collisions
- Add latest-linux.yml and latest.yml merge steps for auto-updater
- Update README with ARM64 download links

Closes #140
2026-04-28 14:31:23 +09:00
Timothy Jaeryang Baek 48be8d0386 fix(linux): disable GPU entirely to prevent shared memory crash and grey screen
Escalate from --disable-gpu-compositing to --disable-gpu on Linux.
The GPU process crashes during shared memory allocation in /tmp even
with --disable-dev-shm-usage, and disabling compositing alone still
leaves the GPU process running. --disable-gpu prevents the process
from spawning entirely, resolving both the crash and grey screen.

Closes #157, Closes #119
2026-04-28 14:23:36 +09:00
Timothy Jaeryang Baek 4cab91de4e fix: spotlight dismiss no longer brings main window to foreground (#158)
Escape, click-away, hotkey toggle, and blur now hide the spotlight
overlay silently.  Only spotlight:submit restores the main window
so the user sees their query delivered.
2026-04-28 14:08:58 +09:00
Timothy Jaeryang Baek 564a89baa8 chore: bump version to 0.0.14 and update changelog 2026-04-28 13:20:20 +09:00
Timothy Jaeryang Baek 25b9e195c2 fix(linux): disable GPU compositing to prevent grey/blank webview screen
On many Linux setups (Intel iGPU, mixed NVIDIA/Intel, Wayland
compositors), the GPU compositor fails silently without crashing
the GPU process — producing a grey rectangle where the webview
content should be.  The existing GPU crash recovery marker never
fires because the process stays alive.

Adding --disable-gpu-compositing on Linux is the standard Electron
workaround (used by VS Code, Brave, etc.) for this class of issue.
It disables only the compositor, not all hardware acceleration,
so video decode and basic GPU ops still work.

Also adds render-process-gone listeners to auto-reload the main
window on renderer crashes and log webview guest crashes for
diagnostics.

Closes #119
2026-04-27 15:15:44 +09:00
9 changed files with 319 additions and 76 deletions
+74 -20
View File
@@ -42,8 +42,12 @@ jobs:
include:
- os: ubuntu-latest
arch: x64
- os: ubuntu-24.04-arm
arch: arm64
- os: windows-latest
arch: x64
- os: windows-11-arm
arch: arm64
- os: macos-latest
arch: x64
- os: macos-latest
@@ -67,9 +71,9 @@ jobs:
name: compiled-output
path: out/
# ── Flatpak setup (Linux only) ──
# ── Flatpak setup (Linux x64 only) ──
- name: Cache Flatpak SDKs
if: matrix.os == 'ubuntu-latest'
if: runner.os == 'Linux' && matrix.arch == 'x64'
uses: actions/cache@v4
with:
path: ~/.local/share/flatpak
@@ -77,7 +81,7 @@ jobs:
- name: Install Flatpak build tools
id: flatpak
if: matrix.os == 'ubuntu-latest'
if: runner.os == 'Linux' && matrix.arch == 'x64'
continue-on-error: true
run: |
sudo apt-get update
@@ -113,12 +117,12 @@ jobs:
# ── Platform packaging ──
- name: Package for Windows
id: win_build
if: matrix.os == 'windows-latest'
if: runner.os == 'Windows'
continue-on-error: true
run: npx electron-builder --win --${{ matrix.arch }} --publish never
- name: Package for Windows (unsigned fallback)
if: matrix.os == 'windows-latest' && steps.win_build.outcome == 'failure'
if: runner.os == 'Windows' && steps.win_build.outcome == 'failure'
env:
WIN_CSC_LINK: ''
CSC_IDENTITY_AUTO_DISCOVERY: 'false'
@@ -147,9 +151,12 @@ jobs:
npx electron-builder --mac --${{ matrix.arch }} --publish never
- name: Package for Linux
if: matrix.os == 'ubuntu-latest'
if: runner.os == 'Linux'
run: |
if [ "${{ steps.flatpak.outcome }}" == "success" ]; then
if [ "${{ matrix.arch }}" == "arm64" ]; then
# ARM64: deb + AppImage only (flatpak BaseApp not available for arm64)
npx electron-builder --linux AppImage deb --${{ matrix.arch }} --publish never
elif [ "${{ steps.flatpak.outcome }}" == "success" ]; then
npx electron-builder --linux --${{ matrix.arch }} --publish never
else
echo "Flatpak not available, building without flatpak"
@@ -158,7 +165,7 @@ jobs:
# ── Windows code signing ──
- name: Azure Trusted Signing (Windows Only)
if: matrix.os == 'windows-latest'
if: runner.os == 'Windows'
continue-on-error: true
uses: azure/trusted-signing-action@v0.5.1
with:
@@ -239,6 +246,9 @@ jobs:
pattern: '*-*'
merge-multiple: false
- name: Install js-yaml for manifest merging
run: npm install --no-save js-yaml
- name: Merge macOS latest-mac.yml (x64 + arm64)
run: |
# Each macOS arch build produces its own latest-mac.yml with only
@@ -248,7 +258,6 @@ jobs:
if [ -f "$X64_YML" ] && [ -f "$ARM_YML" ]; then
echo "Merging latest-mac.yml from both architectures"
npm install --no-save js-yaml
node -e "
const fs = require('fs');
const yaml = require('js-yaml');
@@ -259,13 +268,58 @@ jobs:
fs.writeFileSync('$X64_YML', out);
console.log(out);
"
# Remove duplicate so only one latest-mac.yml is uploaded
rm -f "$ARM_YML"
else
echo "Skipping merge — need both $X64_YML and $ARM_YML"
ls -la macos-latest-*/latest-mac.yml 2>/dev/null || true
fi
- name: Merge Linux latest-linux.yml (x64 + arm64)
run: |
X64_YML="ubuntu-latest-x64/latest-linux.yml"
ARM_YML="ubuntu-24.04-arm-arm64/latest-linux.yml"
if [ -f "$X64_YML" ] && [ -f "$ARM_YML" ]; then
echo "Merging latest-linux.yml from both architectures"
node -e "
const fs = require('fs');
const yaml = require('js-yaml');
const x64 = yaml.load(fs.readFileSync('$X64_YML', 'utf8'));
const arm = yaml.load(fs.readFileSync('$ARM_YML', 'utf8'));
x64.files = [...(x64.files || []), ...(arm.files || [])];
const out = yaml.dump(x64, { lineWidth: -1 });
fs.writeFileSync('$X64_YML', out);
console.log(out);
"
rm -f "$ARM_YML"
else
echo "Skipping merge — need both $X64_YML and $ARM_YML"
ls -la ubuntu-*-*/latest-linux.yml 2>/dev/null || true
fi
- name: Merge Windows latest.yml (x64 + arm64)
run: |
X64_YML="windows-latest-x64/latest.yml"
ARM_YML="windows-11-arm-arm64/latest.yml"
if [ -f "$X64_YML" ] && [ -f "$ARM_YML" ]; then
echo "Merging latest.yml from both architectures"
node -e "
const fs = require('fs');
const yaml = require('js-yaml');
const x64 = yaml.load(fs.readFileSync('$X64_YML', 'utf8'));
const arm = yaml.load(fs.readFileSync('$ARM_YML', 'utf8'));
x64.files = [...(x64.files || []), ...(arm.files || [])];
const out = yaml.dump(x64, { lineWidth: -1 });
fs.writeFileSync('$X64_YML', out);
console.log(out);
"
rm -f "$ARM_YML"
else
echo "Skipping merge — need both $X64_YML and $ARM_YML"
ls -la windows-*-*/latest.yml 2>/dev/null || true
fi
- name: Create Release
uses: softprops/action-gh-release@v2
env:
@@ -277,18 +331,18 @@ jobs:
draft: false
prerelease: false
files: |
windows-latest-*/*.exe
windows-latest-*/*.blockmap
windows-latest-*/latest*.yml
windows-*-*/*.exe
windows-*-*/*.blockmap
windows-*-*/latest*.yml
macos-latest-*/*.dmg
macos-latest-*/*.zip
macos-latest-*/*.pkg
macos-latest-*/*.blockmap
macos-latest-*/latest*.yml
ubuntu-latest-*/*.deb
ubuntu-latest-*/*.rpm
ubuntu-latest-*/*.AppImage
ubuntu-latest-*/*.snap
ubuntu-latest-*/*.flatpak
ubuntu-latest-*/*.tar.gz
ubuntu-latest-*/latest*.yml
ubuntu-*-*/*.deb
ubuntu-*-*/*.rpm
ubuntu-*-*/*.AppImage
ubuntu-*-*/*.snap
ubuntu-*-*/*.flatpak
ubuntu-*-*/*.tar.gz
ubuntu-*-*/latest*.yml
+44
View File
@@ -5,6 +5,50 @@ 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.18] - 2026-05-05
### Fixed
- **Downloaded Models Not Recognized by llama.cpp.** Models downloaded from Hugging Face are now stored directly under the `models/` directory instead of a nested `models/huggingface/` subdirectory, so llama-server's model scanner discovers them without manual symlinks. Existing models in the old location are automatically migrated on startup (#177).
## [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
- **Links Open in Default Browser.** Clicking links in chat responses now opens them in the user's default browser instead of navigating within the app or spawning a new Electron window (#165).
## [0.0.15] - 2026-04-28
### Added
- **ARM64 Support for Linux and Windows.** Native ARM64 builds are now produced for Linux (.deb, AppImage) and Windows (NSIS installer), enabling support for Raspberry Pi, NVIDIA DGX Spark, Snapdragon laptops, and other ARM64 devices (#140).
### Fixed
- **Grey/Blank Screen on Linux.** Disabled GPU compositing entirely on Linux to prevent shared memory allocation crashes that caused a grey or blank screen on systems with restricted `/dev/shm` or `/tmp` permissions.
- **Spotlight Dismiss Behavior.** Pressing Escape or the toggle shortcut to dismiss Spotlight no longer erroneously brings the main application window to the foreground (#158).
## [0.0.14] - 2026-04-28
### Fixed
- **Grey/Blank Webview on Linux.** Disabled GPU compositing on Linux to prevent silent compositor failures that produce a grey rectangle instead of rendered content on systems with problematic Intel/NVIDIA drivers or certain Wayland compositors (#119).
- **Renderer Crash Recovery.** The main window now automatically reloads when the renderer process dies unexpectedly, preventing a permanent blank/grey screen.
- **Webview Crash Diagnostics.** Added logging for guest webview renderer crashes to aid debugging connectivity and rendering issues.
- **macOS Notarization.** Resolved Apple notarization failure caused by an expired Developer Program agreement, restoring signed and notarized macOS builds.
## [0.0.13] - 2026-04-27
### Fixed
+9 -6
View File
@@ -18,11 +18,14 @@ Your AI, right on your desktop. [Open WebUI](https://github.com/open-webui/open-
|----------|-----------|
| macOS (Apple Silicon) | [**Download .dmg**](https://github.com/open-webui/desktop/releases/latest/download/open-webui-arm64.dmg) |
| macOS (Intel) | [**Download .dmg**](https://github.com/open-webui/desktop/releases/latest/download/open-webui-x64.dmg) |
| Windows x64 | [**Download .exe**](https://github.com/open-webui/desktop/releases/latest/download/open-webui-setup.exe) |
| Linux (AppImage) | [**Download .AppImage**](https://github.com/open-webui/desktop/releases/latest/download/open-webui.AppImage) |
| Linux (Debian/Ubuntu) | [**Download .deb**](https://github.com/open-webui/desktop/releases/latest/download/open-webui_amd64.deb) |
| Linux (Snap) | [**Download .snap**](https://github.com/open-webui/desktop/releases/latest/download/open-webui_amd64.snap) |
| Linux (Flatpak) | [**Download .flatpak**](https://github.com/open-webui/desktop/releases/latest/download/open-webui.flatpak) |
| Windows x64 | [**Download .exe**](https://github.com/open-webui/desktop/releases/latest/download/open-webui-x64-setup.exe) |
| Windows ARM64 | [**Download .exe**](https://github.com/open-webui/desktop/releases/latest/download/open-webui-arm64-setup.exe) |
| Linux x64 (AppImage) | [**Download .AppImage**](https://github.com/open-webui/desktop/releases/latest/download/open-webui_x64.AppImage) |
| Linux x64 (Debian/Ubuntu) | [**Download .deb**](https://github.com/open-webui/desktop/releases/latest/download/open-webui_amd64.deb) |
| Linux x64 (Snap) | [**Download .snap**](https://github.com/open-webui/desktop/releases/latest/download/open-webui_amd64.snap) |
| Linux x64 (Flatpak) | [**Download .flatpak**](https://github.com/open-webui/desktop/releases/latest/download/open-webui.flatpak) |
| Linux ARM64 (AppImage) | [**Download .AppImage**](https://github.com/open-webui/desktop/releases/latest/download/open-webui_arm64.AppImage) |
| Linux ARM64 (Debian/Ubuntu) | [**Download .deb**](https://github.com/open-webui/desktop/releases/latest/download/open-webui_arm64.deb) |
Internet required on first launch. After that, everything works offline. [All releases →](https://github.com/open-webui/desktop/releases)
@@ -51,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.
+2 -2
View File
@@ -19,7 +19,7 @@ asarUnpack:
win:
executableName: open-webui
nsis:
artifactName: ${name}-setup.${ext}
artifactName: ${name}-${arch}-setup.${ext}
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always
@@ -63,7 +63,7 @@ deb:
snap:
artifactName: ${name}_${arch}.${ext}
appImage:
artifactName: ${name}.${ext}
artifactName: ${name}_${arch}.${ext}
flatpak:
base: org.electronjs.Electron2.BaseApp
baseVersion: '23.08'
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "open-webui",
"version": "0.0.13",
"version": "0.0.18",
"license": "AGPL-3.0",
"description": "Open WebUI Desktop",
"main": "./out/main/index.js",
+117 -11
View File
@@ -105,6 +105,15 @@ if (process.platform === 'linux') {
// This is required for xdg-desktop-portal features like GlobalShortcuts
// to work (the portal is enabled by default in Chromium 134+ / Electron 33+).
app.commandLine.appendSwitch('ozone-platform-hint', 'auto')
// Disable GPU acceleration entirely on Linux. This prevents the GPU
// process from spawning, which avoids shared-memory allocation failures
// in /dev/shm or /tmp that crash the renderer on Ubuntu 24.04+, certain
// Wayland compositors, and AppArmor-restricted environments. The lighter
// --disable-gpu-compositing flag is insufficient because the GPU process
// still starts and attempts shared-memory IPC. Users confirmed that
// --disable-gpu resolves both the crash and grey/blank screen (#119, #157).
app.commandLine.appendSwitch('disable-gpu')
}
// ─── GPU Crash Recovery ─────────────────────────────────
@@ -311,18 +320,10 @@ function createSpotlightWindow(): BrowserWindow {
spotlightWindow.on('blur', () => {
if (blurArmed) {
spotlightWindow?.hide()
// Restore main window when spotlight dismisses
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.show()
}
}
})
spotlightWindow.on('closed', () => {
// Restore main window if spotlight is closed
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.show()
}
spotlightWindow = null
})
@@ -1220,6 +1221,113 @@ if (!gotTheLock) {
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
// Auto-reload when the renderer process dies so the user doesn't
// see a permanent blank/grey screen.
window.webContents.on('render-process-gone', (_event, details) => {
log.error(
`Renderer process gone: reason=${details.reason}, exitCode=${details.exitCode}`
)
if (details.reason !== 'clean-exit') {
window.webContents.reload()
}
})
})
// Log webview guest renderer crashes for diagnostics — the existing
// 'crashed' listener in Content.svelte surfaces these to the user.
//
// For webview guests we also intercept navigation and popup events
// so that external links open in the user's default browser instead
// of navigating the webview or spawning a new Electron window (#165).
app.on('web-contents-created', (_event, contents) => {
contents.on('render-process-gone', (_e, details) => {
if (details.reason !== 'clean-exit') {
log.error(
`WebContents render-process-gone: type=${contents.getType()}, ` +
`reason=${details.reason}, exitCode=${details.exitCode}`
)
}
})
if (contents.getType() === 'webview') {
// ── Popups (target="_blank" links) → open in default browser ──
contents.setWindowOpenHandler(({ url }) => {
openUrl(url)
return { action: 'deny' }
})
// ── In-page navigation to a different origin → open externally ──
// This catches regular link clicks (no target) that would navigate
// the webview away from the Open WebUI instance.
contents.on('will-navigate', (event, url) => {
try {
const currentOrigin = new URL(contents.getURL()).origin
const targetOrigin = new URL(url).origin
if (targetOrigin !== currentOrigin) {
event.preventDefault()
openUrl(url)
}
} catch {
// Malformed URL — let it through so Chromium can handle/reject it
}
})
// ── Native right-click context menu (#161) ──────────────────
// Electron <webview> 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()
}
})
}
})
// ─── IPC Handlers ─────────────────────────────────
@@ -1454,9 +1562,8 @@ if (!gotTheLock) {
sendToRenderer('query', { query, connectionId: conn.id, url, files })
// Hide spotlight first (blur handler will restore main window)
spotlightWindow?.hide()
// Ensure main window is focused to receive the query
// Show main window so it can receive and display the submitted query
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.show()
mainWindow.focus()
@@ -1464,7 +1571,6 @@ if (!gotTheLock) {
})
ipcMain.handle('spotlight:close', () => {
spotlightWindow?.hide()
// blur handler restores main window
})
// Persist bar offset within the fullscreen spotlight window
+29 -2
View File
@@ -5,7 +5,7 @@
* Downloads files from HF repos, manages a local model cache,
* and provides listing/deletion of cached models.
*
* Cache dir: <userData>/models/huggingface/<repo-slug>/<filename>
* Cache dir: <userData>/models/<repo-slug>/<filename>
*/
import * as fs from 'fs'
@@ -33,10 +33,37 @@ export interface HfDownloadProgress {
// ─── Paths ──────────────────────────────────────────────
const getHfCacheDir = (): string => {
const dir = path.join(getInstallDir(), 'models', 'huggingface')
const dir = path.join(getInstallDir(), 'models')
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true })
}
// Migrate models from legacy models/huggingface/<slug>/ to models/<slug>/
const legacyDir = path.join(dir, 'huggingface')
if (fs.existsSync(legacyDir)) {
try {
const entries = fs.readdirSync(legacyDir, { withFileTypes: true })
for (const entry of entries) {
if (entry.isDirectory()) {
const src = path.join(legacyDir, entry.name)
const dest = path.join(dir, entry.name)
if (!fs.existsSync(dest)) {
fs.renameSync(src, dest)
log.info(`[huggingface] Migrated ${entry.name} from legacy cache`)
}
}
}
// Remove legacy dir if empty (manifest.json may remain)
const remaining = fs.readdirSync(legacyDir)
if (remaining.length === 0) {
fs.rmdirSync(legacyDir)
log.info('[huggingface] Removed empty legacy huggingface/ directory')
}
} catch (e) {
log.warn('[huggingface] Failed to migrate legacy cache:', e)
}
}
return dir
}
+42 -33
View File
@@ -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<string, string> = {}): Record<string, string> => {
const base: Record<string, string> = { ...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(
@@ -268,9 +268,9 @@
src={connUrl}
class="flex-1 min-h-0 border-none"
style="display: {view === 'connected' && activeConnectionId === connId ? 'flex' : 'none'}"
allowpopups
partition="persist:connection-{connId}"
preload={contentPreloadPath}
allowpopups
></webview>
{/each}