mirror of
https://github.com/open-webui/desktop.git
synced 2026-04-28 01:49:29 +00:00
refac
This commit is contained in:
parent
08616e701d
commit
03f6abae75
10 changed files with 186 additions and 64 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -6,3 +6,4 @@ out
|
|||
.DS_Store
|
||||
.eslintcache
|
||||
*.log*
|
||||
*.tsbuildinfo
|
||||
|
|
|
|||
13
CHANGELOG.md
13
CHANGELOG.md
|
|
@ -5,7 +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).
|
||||
|
||||
## [Unreleased]
|
||||
## [0.0.5] - 2026-04-07
|
||||
|
||||
### Added
|
||||
|
||||
- **Bidirectional Theme Sync** — Theme changes in Open WebUI are now mirrored to the desktop shell and vice versa, using a symmetric `theme:update` event protocol
|
||||
- **Zero-Reload Spotlight Queries** — Spotlight prompts are injected directly into already-open webviews via event bridge instead of triggering a full page reload
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Auto-Default Connection** — Selecting a connection now automatically sets it as the default for spotlight and startup
|
||||
- **Connection Switch Stability** — Switching between already-open connections no longer causes unnecessary URL reloads
|
||||
- **Connection Switch Race Condition** — Clicking a remote connection while the local server is still starting no longer gets overridden when the local connection finishes loading
|
||||
|
||||
## [0.0.3] - 2026-04-06
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "open-webui",
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.5",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Open WebUI Desktop",
|
||||
"main": "./out/main/index.js",
|
||||
|
|
|
|||
|
|
@ -410,7 +410,10 @@ const updateTray = () => {
|
|||
const connectionItems = (CONFIG.connections || []).map((conn) => ({
|
||||
label: `${conn.id === CONFIG.defaultConnectionId ? '★ ' : ''}${conn.name}`,
|
||||
sublabel: conn.url,
|
||||
click: () => connectTo(conn)
|
||||
click: async () => {
|
||||
const result = await connectTo(conn)
|
||||
if (result) sendToRenderer('connection:open', result)
|
||||
}
|
||||
}))
|
||||
|
||||
const trayMenuTemplate = [
|
||||
|
|
@ -490,7 +493,6 @@ const connectTo = async (connection: Connection) => {
|
|||
url = url.replace('http://0.0.0.0', 'http://localhost')
|
||||
}
|
||||
|
||||
sendToRenderer('connection:open', { url, connectionId: connection.id })
|
||||
return { url, connectionId: connection.id }
|
||||
}
|
||||
|
||||
|
|
@ -981,9 +983,10 @@ if (!gotTheLock) {
|
|||
url = url.replace('http://0.0.0.0', 'http://localhost')
|
||||
}
|
||||
|
||||
// Navigate to the connection URL with query
|
||||
const targetUrl = `${url}/?q=${encodeURIComponent(query)}`
|
||||
sendToRenderer('connection:open', { url: targetUrl, connectionId: conn.id })
|
||||
// Send query event — the renderer decides whether to open a new
|
||||
// connection (with ?q= baked into the URL) or inject the prompt
|
||||
// into an already-open webview via postMessage (zero navigation).
|
||||
sendToRenderer('query', { query, connectionId: conn.id, url })
|
||||
|
||||
// Show main window and hide spotlight
|
||||
mainWindow?.show()
|
||||
|
|
@ -1300,7 +1303,8 @@ if (!gotTheLock) {
|
|||
)
|
||||
if (defaultConn) {
|
||||
createMainWindow()
|
||||
await connectTo(defaultConn)
|
||||
const result = await connectTo(defaultConn)
|
||||
if (result) sendToRenderer('connection:open', result)
|
||||
} else {
|
||||
createMainWindow()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,14 @@ ipcRenderer.on('desktop:event', (_event, data) => {
|
|||
eventCallbacks.forEach((cb) => cb(data))
|
||||
})
|
||||
|
||||
// ─── Theme Sync: Open WebUI → Desktop ───────────────────
|
||||
// Open WebUI calls window.applyTheme() after every theme change.
|
||||
// We inject this hook so the desktop shell can mirror the theme.
|
||||
contextBridge.exposeInMainWorld('applyTheme', () => {
|
||||
const theme = localStorage.getItem('theme') ?? 'system'
|
||||
ipcRenderer.sendToHost('webview:event', { type: 'theme:update', data: { theme } })
|
||||
})
|
||||
|
||||
// Expose to the Open WebUI page via contextBridge (secure, unforgeable)
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// Push events: desktop → Open WebUI
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@
|
|||
// Now connect — the server is ready
|
||||
installStatus = ''
|
||||
localInstalled = true
|
||||
await connect('local')
|
||||
connect('local')
|
||||
installPhase = 'idle'
|
||||
} catch (e: any) {
|
||||
installPhase = 'error'
|
||||
|
|
@ -182,33 +182,61 @@
|
|||
}
|
||||
}
|
||||
|
||||
const connect = async (id: string) => {
|
||||
const connect = (id: string) => {
|
||||
showingLogs = false
|
||||
// Toggle: clicking the active connection unselects it
|
||||
if (activeConnectionId === id && view === 'connected') {
|
||||
connectingId = ''
|
||||
activeConnectionId = ''
|
||||
connectedUrl = ''
|
||||
view = 'welcome'
|
||||
return
|
||||
}
|
||||
// Persist as default so spotlight/startup always use the last-selected connection
|
||||
window.electronAPI.setDefaultConnection(id)
|
||||
// Already-open connection — just switch to it
|
||||
if (openConnections.has(id)) {
|
||||
connectingId = ''
|
||||
activeConnectionId = id
|
||||
connectedUrl = openConnections.get(id)!
|
||||
view = 'connected'
|
||||
return
|
||||
}
|
||||
connectingId = id
|
||||
try {
|
||||
const result = await window.electronAPI.connectTo(id)
|
||||
if (result?.url) {
|
||||
openConnections.set(result.connectionId, result.url)
|
||||
openConnections = new Map(openConnections) // trigger reactivity
|
||||
connectedUrl = result.url
|
||||
activeConnectionId = result.connectionId
|
||||
view = 'connected'
|
||||
}
|
||||
} finally {
|
||||
|
||||
const conn = ($connections ?? []).find((c) => c.id === id)
|
||||
if (!conn) return
|
||||
|
||||
activeConnectionId = id
|
||||
|
||||
if (conn.type === 'local') {
|
||||
// Local needs server start — use IPC
|
||||
connectingId = id
|
||||
view = 'welcome'
|
||||
window.electronAPI.connectTo(id).then((result: any) => {
|
||||
if (!result?.url) {
|
||||
if (connectingId === id) connectingId = ''
|
||||
return
|
||||
}
|
||||
if (!openConnections.has(result.connectionId)) {
|
||||
openConnections.set(result.connectionId, result.url)
|
||||
openConnections = new Map(openConnections)
|
||||
}
|
||||
if (connectingId === id) {
|
||||
connectedUrl = result.url
|
||||
activeConnectionId = result.connectionId
|
||||
connectingId = ''
|
||||
if (installPhase !== 'working') {
|
||||
view = 'connected'
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Remote — open immediately, no IPC needed
|
||||
connectingId = ''
|
||||
openConnections.set(id, conn.url)
|
||||
openConnections = new Map(openConnections)
|
||||
connectedUrl = conn.url
|
||||
view = 'connected'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -307,36 +335,72 @@
|
|||
if (data.type === 'connection:open' && data.data?.url) {
|
||||
const connId = data.data.connectionId ?? ''
|
||||
const incomingUrl = data.data.url
|
||||
const alreadyOpen = openConnections.has(connId)
|
||||
|
||||
openConnections.set(connId, incomingUrl)
|
||||
openConnections = new Map(openConnections)
|
||||
connectedUrl = incomingUrl
|
||||
activeConnectionId = connId
|
||||
|
||||
// If the webview for this connection already exists in the DOM,
|
||||
// updating the map value won't cause it to re-navigate (Electron
|
||||
// webview `src` changes on an existing element are ignored). We
|
||||
// need to explicitly call loadURL so that e.g. the spotlight ?q=
|
||||
// parameter actually reaches the Open WebUI instance.
|
||||
if (alreadyOpen) {
|
||||
requestAnimationFrame(() => {
|
||||
const container = document.querySelector('.content-webview-container')
|
||||
if (!container) return
|
||||
const wv = container.querySelector(
|
||||
`webview[partition="persist:connection-${connId}"]`
|
||||
) as any
|
||||
if (wv?.loadURL) {
|
||||
wv.loadURL(incomingUrl)
|
||||
}
|
||||
})
|
||||
if (!openConnections.has(connId)) {
|
||||
openConnections.set(connId, incomingUrl)
|
||||
openConnections = new Map(openConnections)
|
||||
}
|
||||
|
||||
// Don't switch to connected view during active install — the install
|
||||
// flow handles its own transition after confirming reachability.
|
||||
// Only auto-switch if no connection is currently active.
|
||||
// This handles startup auto-connect and tray clicks when on the welcome screen,
|
||||
// without overriding a connection the user is already viewing.
|
||||
if (view !== 'connected') {
|
||||
connectedUrl = openConnections.get(connId) ?? incomingUrl
|
||||
activeConnectionId = connId
|
||||
|
||||
if (installPhase !== 'working') {
|
||||
view = 'connected'
|
||||
}
|
||||
}
|
||||
}
|
||||
// Desktop query — for new connections, bake ?q= into the initial URL.
|
||||
// For already-open connections, the event flows through to the webview
|
||||
// where Open WebUI's Chat.svelte handles it via electronAPI.onEvent.
|
||||
// Fall back to loadURL for old Open WebUI versions without the handler.
|
||||
if (data.type === 'query' && data.data?.query) {
|
||||
const connId = data.data.connectionId ?? ''
|
||||
const query = data.data.query
|
||||
const baseUrl = data.data.url ?? ''
|
||||
|
||||
if (!openConnections.has(connId)) {
|
||||
const initialUrl = `${baseUrl}/?q=${encodeURIComponent(query)}`
|
||||
openConnections.set(connId, initialUrl)
|
||||
openConnections = new Map(openConnections)
|
||||
connectedUrl = initialUrl
|
||||
activeConnectionId = connId
|
||||
if (installPhase !== 'working') {
|
||||
view = 'connected'
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
activeConnectionId = connId
|
||||
connectedUrl = openConnections.get(connId)!
|
||||
if (installPhase !== 'working') {
|
||||
view = 'connected'
|
||||
}
|
||||
|
||||
// Check if Open WebUI supports the query event (version >= 0.6.6).
|
||||
// If not, fall back to full URL navigation.
|
||||
requestAnimationFrame(async () => {
|
||||
const container = document.querySelector('.content-webview-container')
|
||||
if (!container) return
|
||||
const wv = container.querySelector(
|
||||
`webview[partition="persist:connection-${connId}"]`
|
||||
) as any
|
||||
if (!wv) return
|
||||
|
||||
try {
|
||||
const ver = await wv.executeJavaScript('window.WEBUI_VERSION || ""')
|
||||
if (!ver) {
|
||||
// Old Open WebUI — fall back to ?q= navigation
|
||||
wv.loadURL(`${baseUrl}/?q=${encodeURIComponent(query)}`)
|
||||
}
|
||||
// New Open WebUI — event already forwarded by Content.svelte
|
||||
} catch {
|
||||
wv.loadURL(`${baseUrl}/?q=${encodeURIComponent(query)}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
if (data.type === 'status:open-terminal') {
|
||||
openTerminalStatus = data.data
|
||||
|
|
|
|||
|
|
@ -85,8 +85,7 @@
|
|||
|
||||
const isLoading = $derived(
|
||||
connectingId !== '' ||
|
||||
serverStarting ||
|
||||
(view === 'connected' && activeConnectionId && (webviewLoading.get(activeConnectionId) ?? true))
|
||||
(serverStarting && activeConnectionId === localConn?.id)
|
||||
)
|
||||
|
||||
// Attach load event listeners and IPC forwarding to webviews
|
||||
|
|
@ -149,6 +148,35 @@
|
|||
} else if (event.channel === 'webview:load') {
|
||||
const page = event.args?.[0]
|
||||
if (page) onSetView(page === 'home' ? 'welcome' : page)
|
||||
} else if (event.channel === 'webview:event') {
|
||||
const payload = event.args?.[0]
|
||||
if (!payload?.type) return
|
||||
|
||||
if (payload.type === 'theme:update') {
|
||||
const webuiTheme = payload.data?.theme ?? 'system'
|
||||
|
||||
// Map Open WebUI theme names to desktop-compatible values
|
||||
let desktopTheme: string
|
||||
if (webuiTheme === 'system') {
|
||||
desktopTheme = 'system'
|
||||
} else if (webuiTheme.includes('dark')) {
|
||||
desktopTheme = 'dark'
|
||||
} else {
|
||||
desktopTheme = 'light'
|
||||
}
|
||||
|
||||
// Resolve and apply CSS class
|
||||
let resolved = desktopTheme
|
||||
if (desktopTheme === 'system') {
|
||||
resolved = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
}
|
||||
document.documentElement.classList.remove('light', 'dark')
|
||||
document.documentElement.classList.add(resolved)
|
||||
|
||||
// Persist to desktop config
|
||||
await window.electronAPI.setConfig({ theme: desktopTheme })
|
||||
config.set(await window.electronAPI.getConfig())
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -99,19 +99,14 @@
|
|||
connectingId === localConn.id ||
|
||||
serverStatus === 'starting' ||
|
||||
(serverStatus === 'running' && !serverReachable)}
|
||||
{@const isLocalDisabled = !serverReachable && !isServerLoading}
|
||||
<div
|
||||
class="w-full px-2.5 py-1.5 rounded-xl group flex items-center gap-2 transition-colors {isLocalDisabled
|
||||
? 'opacity-40 cursor-default'
|
||||
: 'cursor-pointer'} {activeConnectionId === localConn.id || isServerLoading
|
||||
? 'bg-black/[0.06] dark:bg-white/[0.06]'
|
||||
: isLocalDisabled
|
||||
? ''
|
||||
: 'hover:bg-black/[0.03] dark:bg-white/[0.05]'}"
|
||||
class="w-full px-2.5 py-1.5 rounded-xl group flex items-center gap-2 transition-colors cursor-pointer {activeConnectionId === localConn.id
|
||||
? 'bg-black/[0.08] dark:bg-white/[0.08]'
|
||||
: 'hover:bg-black/[0.04] dark:hover:bg-white/[0.06]'}"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onclick={() => !isLocalDisabled && onConnect(localConn.id)}
|
||||
onkeydown={(e) => e.key === 'Enter' && !isLocalDisabled && onConnect(localConn.id)}
|
||||
onclick={() => onConnect(localConn.id)}
|
||||
onkeydown={(e) => e.key === 'Enter' && onConnect(localConn.id)}
|
||||
>
|
||||
{#if connectingId === localConn.id || serverStatus === 'starting' || (serverStatus === 'running' && !serverReachable)}
|
||||
<div class="w-[14px] h-[14px] shrink-0 flex items-center justify-center">
|
||||
|
|
@ -149,8 +144,8 @@
|
|||
{:else}
|
||||
<span
|
||||
class="text-[12px] {activeConnectionId === localConn.id
|
||||
? 'opacity-90'
|
||||
: 'opacity-100'} transition-opacity truncate flex-1 min-w-0"
|
||||
? 'font-medium opacity-100'
|
||||
: 'opacity-70'} transition-opacity truncate flex-1 min-w-0"
|
||||
>{localConn.name ?? 'Open WebUI'}</span
|
||||
>
|
||||
{/if}
|
||||
|
|
@ -245,8 +240,8 @@
|
|||
<div
|
||||
class="w-full px-2.5 py-1.5 rounded-xl group flex items-center gap-2 transition-colors cursor-pointer {activeConnectionId ===
|
||||
conn.id
|
||||
? 'bg-black/[0.06] dark:bg-white/[0.06]'
|
||||
: 'hover:bg-black/[0.03] dark:bg-white/[0.05]'}"
|
||||
? 'bg-black/[0.08] dark:bg-white/[0.08]'
|
||||
: 'hover:bg-black/[0.04] dark:hover:bg-white/[0.06]'}"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onclick={() => editingId !== conn.id && onConnect(conn.id)}
|
||||
|
|
@ -293,8 +288,8 @@
|
|||
{:else}
|
||||
<span
|
||||
class="text-[12px] {activeConnectionId === conn.id
|
||||
? 'opacity-90'
|
||||
: 'opacity-100'} transition-opacity truncate flex-1 min-w-0">{conn.name}</span
|
||||
? 'font-medium opacity-100'
|
||||
: 'opacity-70'} transition-opacity truncate flex-1 min-w-0">{conn.name}</span
|
||||
>
|
||||
{/if}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,18 @@
|
|||
applyThemeClass(newTheme)
|
||||
await window.electronAPI.setConfig({ theme: newTheme })
|
||||
config.set(await window.electronAPI.getConfig())
|
||||
|
||||
// Push theme to all active Open WebUI webviews
|
||||
const container = document.querySelector('.content-webview-container')
|
||||
if (container) {
|
||||
container.querySelectorAll('webview').forEach((wv: any) => {
|
||||
try {
|
||||
wv.send('desktop:event', { type: 'theme:update', data: { theme: newTheme } })
|
||||
} catch (_) {
|
||||
// webview may not be ready yet
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const setDefault = async (id: string) => {
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue