Skip to content

Commit 1b4b9fb

Browse files
authored
Merge pull request #148 from open-webui/main
0.0.10
2 parents 531bca9 + eb3c569 commit 1b4b9fb

6 files changed

Lines changed: 209 additions & 75 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.0.10] - 2026-04-24
9+
10+
### Added
11+
12+
- **Concurrent Model Downloads.** Multiple Hugging Face models can now be downloaded simultaneously, each with independent progress tracking and per-file cancel buttons.
13+
14+
### Changed
15+
16+
- **Models Settings UI.** Cleaner layout with inline progress bars, hover-reveal download buttons, and breadcrumb-style repo navigation.
17+
18+
### Fixed
19+
20+
- **GPU Process Crash Recovery.** The app now automatically detects GPU process crashes (common with certain NVIDIA/Intel drivers on Windows) and relaunches with the GPU sandbox disabled, instead of closing immediately. No manual shortcut edits required.
21+
822
## [0.0.9] - 2026-04-20
923

1024
### Fixed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "open-webui",
3-
"version": "0.0.9",
3+
"version": "0.0.10",
44
"license": "AGPL-3.0",
55
"description": "Open WebUI Desktop",
66
"main": "./out/main/index.js",

src/main/index.ts

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,31 @@ log.transports.file.resolvePathFn = () => getLogFilePath('main')
8989

9090
import icon from '../../resources/icon.png?asset'
9191

92+
import { existsSync, writeFileSync, unlinkSync } from 'fs'
93+
9294
if (process.platform === 'linux') {
9395
app.commandLine.appendSwitch('no-sandbox')
9496
}
9597

98+
// ─── GPU Crash Recovery ─────────────────────────────────
99+
// When the GPU process crashes fatally (common on certain NVIDIA/Intel
100+
// driver + Windows combos), we write a marker file and relaunch with
101+
// --disable-gpu-sandbox so the user doesn't have to manually edit
102+
// shortcut properties. On the next launch the marker is detected and
103+
// the switch is applied preemptively.
104+
105+
const gpuCrashMarkerPath = join(app.getPath('userData'), '.gpu-sandbox-disabled')
106+
const gpuSandboxDisabled = existsSync(gpuCrashMarkerPath)
107+
108+
if (gpuSandboxDisabled) {
109+
log.info('GPU sandbox disabled due to previous GPU process crash')
110+
app.commandLine.appendSwitch('disable-gpu-sandbox')
111+
}
112+
113+
// Prevent Chromium from permanently blocking WebGL / 3-D APIs after
114+
// repeated GPU process crashes within the same session.
115+
app.disableDomainBlockingFor3DAPIs()
116+
96117
// ─── State ──────────────────────────────────────────────
97118

98119
let mainWindow: BrowserWindow | null = null
@@ -948,6 +969,15 @@ const resetAppHandler = async () => {
948969
} catch (e) {
949970
log.warn('Failed to uninstall llama.cpp during reset:', e)
950971
}
972+
// Remove GPU crash marker so sandbox is re-tested on next launch
973+
try {
974+
if (existsSync(gpuCrashMarkerPath)) {
975+
unlinkSync(gpuCrashMarkerPath)
976+
log.info('GPU crash marker removed during reset')
977+
}
978+
} catch (e) {
979+
log.warn('Failed to remove GPU crash marker during reset:', e)
980+
}
951981
await new Promise((resolve) => setTimeout(resolve, 1000))
952982
await resetApp()
953983
CONFIG = await getConfig() // reload from defaults since config.json was deleted
@@ -998,6 +1028,43 @@ if (!gotTheLock) {
9981028
}
9991029
electronApp.setAppUserModelId('com.openwebui.desktop')
10001030

1031+
// ─── GPU Process Crash Recovery ──────────────────
1032+
// If the GPU process exits fatally (e.g. sandbox init failure on
1033+
// certain NVIDIA/Intel drivers), write a marker and relaunch with
1034+
// --disable-gpu-sandbox so the user doesn't have to manually edit
1035+
// shortcut targets (see issue #110).
1036+
app.on('child-process-gone', (_event, details) => {
1037+
if (details.type === 'GPU') {
1038+
log.error(
1039+
`GPU process gone: reason=${details.reason}, exitCode=${details.exitCode}`
1040+
)
1041+
1042+
// Only auto-recover from fatal crashes, not normal/clean exits
1043+
if (
1044+
details.reason === 'crashed' ||
1045+
details.reason === 'launch-failed' ||
1046+
details.reason === 'abnormal-exit'
1047+
) {
1048+
if (!gpuSandboxDisabled) {
1049+
log.info('Writing GPU crash marker and relaunching with --disable-gpu-sandbox')
1050+
try {
1051+
writeFileSync(gpuCrashMarkerPath, new Date().toISOString(), 'utf-8')
1052+
} catch (e) {
1053+
log.warn('Failed to write GPU crash marker:', e)
1054+
}
1055+
app.relaunch({ args: [...process.argv.slice(1), '--disable-gpu-sandbox'] })
1056+
app.exit(0)
1057+
}
1058+
}
1059+
}
1060+
})
1061+
1062+
// If we previously set the GPU sandbox marker and this session
1063+
// started successfully, log it so it's visible in diagnostics.
1064+
if (gpuSandboxDisabled) {
1065+
log.info('Running with GPU sandbox disabled (marker file present)')
1066+
}
1067+
10011068
app.on('browser-window-created', (_, window) => {
10021069
optimizer.watchWindowShortcuts(window)
10031070
})
@@ -1010,7 +1077,8 @@ if (!gotTheLock) {
10101077
version: app.getVersion(),
10111078
platform: process.platform,
10121079
arch: process.arch,
1013-
username: require('os').userInfo().username
1080+
username: require('os').userInfo().username,
1081+
gpuSandboxDisabled
10141082
}))
10151083

10161084
ipcMain.handle('app:contentPreloadPath', () => {
@@ -1647,8 +1715,8 @@ if (!gotTheLock) {
16471715
ipcMain.handle('huggingface:models:delete', (_event, repo: string, filename: string) => {
16481716
return deleteModel(repo, filename)
16491717
})
1650-
ipcMain.handle('huggingface:models:cancel', () => {
1651-
cancelDownload()
1718+
ipcMain.handle('huggingface:models:cancel', (_event, repo?: string, filename?: string) => {
1719+
cancelDownload(repo, filename)
16521720
return true
16531721
})
16541722
ipcMain.handle('huggingface:search', async (_event, query: string, token?: string) => {

src/main/utils/huggingface.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,28 @@ const writeManifest = (models: HfModel[]): void => {
6262

6363
// ─── Public API ─────────────────────────────────────────
6464

65-
let activeDownloadAbort: AbortController | null = null
65+
const activeDownloads = new Map<string, AbortController>()
66+
67+
const downloadKey = (repo: string, filename: string): string => `${repo}/${filename}`
6668

6769
/**
68-
* Cancel the current download in progress.
70+
* Cancel a specific download in progress.
71+
* If no repo/filename given, cancels ALL active downloads.
6972
*/
70-
export const cancelDownload = (): void => {
71-
if (activeDownloadAbort) {
72-
activeDownloadAbort.abort()
73-
activeDownloadAbort = null
73+
export const cancelDownload = (repo?: string, filename?: string): void => {
74+
if (repo && filename) {
75+
const key = downloadKey(repo, filename)
76+
const ctrl = activeDownloads.get(key)
77+
if (ctrl) {
78+
ctrl.abort()
79+
activeDownloads.delete(key)
80+
}
81+
} else {
82+
// Cancel all
83+
for (const ctrl of activeDownloads.values()) {
84+
ctrl.abort()
85+
}
86+
activeDownloads.clear()
7487
}
7588
}
7689

@@ -136,8 +149,13 @@ export const downloadModel = async (
136149
headers['Authorization'] = `Bearer ${token}`
137150
}
138151

139-
activeDownloadAbort = new AbortController()
140-
const { signal } = activeDownloadAbort
152+
const key = downloadKey(repo, filename)
153+
// Cancel any existing download for the same file
154+
activeDownloads.get(key)?.abort()
155+
156+
const abortController = new AbortController()
157+
activeDownloads.set(key, abortController)
158+
const { signal } = abortController
141159

142160
// Use fetch for streaming download with progress
143161
const response = await fetch(downloadUrl, {
@@ -183,7 +201,7 @@ export const downloadModel = async (
183201
writeStream.end()
184202
// Clean up partial download
185203
try { fs.unlinkSync(tmpPath) } catch {}
186-
activeDownloadAbort = null
204+
activeDownloads.delete(downloadKey(repo, filename))
187205
throw err
188206
} finally {
189207
writeStream.end()
@@ -192,7 +210,7 @@ export const downloadModel = async (
192210

193211
// Rename tmp to final
194212
fs.renameSync(tmpPath, destPath)
195-
activeDownloadAbort = null
213+
activeDownloads.delete(downloadKey(repo, filename))
196214

197215
// Update manifest
198216
const manifest = readManifest()

src/preload/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,8 @@ const api = {
155155
ipcRenderer.invoke('huggingface:models:download', repo, filename, token, expectedSize),
156156
deleteHfModel: (repo: string, filename: string) =>
157157
ipcRenderer.invoke('huggingface:models:delete', repo, filename),
158-
cancelHfDownload: () => ipcRenderer.invoke('huggingface:models:cancel'),
158+
cancelHfDownload: (repo?: string, filename?: string) =>
159+
ipcRenderer.invoke('huggingface:models:cancel', repo, filename),
159160
searchHfModels: (query: string, token?: string) =>
160161
ipcRenderer.invoke('huggingface:search', query, token),
161162
getHfRepoFiles: (repo: string, token?: string) =>

0 commit comments

Comments
 (0)