From 146f75f9794bdf68fe18fc3f4a146f6eaca8a46e Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Wed, 18 Mar 2026 03:56:16 -0500 Subject: [PATCH] refac --- .github/workflows/release.yml | 41 +++ dev-app-update.yml | 5 +- electron-builder.yml | 5 +- src/main/index.ts | 34 ++- src/main/updater.ts | 66 +++++ src/main/utils/index.ts | 4 +- src/main/utils/open-terminal.ts | 5 +- src/preload/index.ts | 9 +- src/renderer/src/lib/components/Main.svelte | 2 +- .../lib/components/Main/Connections.svelte | 12 + .../Main/Connections/Sidebar.svelte | 262 ++++++++++++++---- .../lib/components/Main/Settings/About.svelte | 136 ++++++++- .../Main/Settings/OpenTerminal.svelte | 54 ++++ .../components/Main/Settings/OpenWebUI.svelte | 27 ++ 14 files changed, 591 insertions(+), 71 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 src/main/updater.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..137f519 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,41 @@ +name: Build & Publish Release + +on: + push: + branches: + - release + +permissions: + contents: write + +jobs: + build: + strategy: + matrix: + include: + - os: macos-latest + platform: mac + - os: ubuntu-latest + platform: linux + - os: windows-latest + platform: win + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Build & Publish + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: npm run build && npx electron-builder --${{ matrix.platform }} --publish always diff --git a/dev-app-update.yml b/dev-app-update.yml index 592f420..5676e36 100644 --- a/dev-app-update.yml +++ b/dev-app-update.yml @@ -1,3 +1,4 @@ -provider: generic -url: https://example.com/auto-updates +provider: github +owner: open-webui +repo: desktop updaterCacheDirName: desktop-updater diff --git a/electron-builder.yml b/electron-builder.yml index abc3fc3..0cd5e34 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -50,5 +50,6 @@ appImage: artifactName: ${name}-${version}.${ext} npmRebuild: false publish: - provider: generic - url: https://example.com/auto-updates + provider: github + owner: open-webui + repo: desktop diff --git a/src/main/index.ts b/src/main/index.ts index 0cae849..d3ba550 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -13,7 +13,8 @@ import { Notification, Menu, ipcMain, - Tray + Tray, + dialog } from 'electron' import path, { join } from 'path' import { electronApp, optimizer, is } from '@electron-toolkit/utils' @@ -52,6 +53,8 @@ import { getOpenTerminalLog } from './utils/open-terminal' +import { initUpdater, checkForUpdates, downloadUpdate, installUpdate } from './updater' + import log from 'electron-log' log.transports.file.resolvePathFn = () => getLogFilePath('main') @@ -590,6 +593,18 @@ if (!gotTheLock) { return config.connections }) + ipcMain.handle('connections:update', async (_event, id: string, updates: Partial) => { + const config = await getConfig() + const idx = config.connections.findIndex((c) => c.id === id) + if (idx !== -1) { + config.connections[idx] = { ...config.connections[idx], ...updates } + await setConfig(config) + CONFIG = config + updateTray() + } + return config.connections + }) + ipcMain.handle('connections:setDefault', async (_event, id: string) => { const config = await getConfig() config.defaultConnectionId = id @@ -611,6 +626,11 @@ if (!gotTheLock) { return await validateRemoteUrl(url) }) + // Updater + ipcMain.handle('updater:check', () => checkForUpdates()) + ipcMain.handle('updater:download', () => downloadUpdate()) + ipcMain.handle('updater:install', () => installUpdate()) + // Misc ipcMain.handle('app:reset', () => resetAppHandler()) @@ -655,6 +675,13 @@ if (!gotTheLock) { return uninstallPackage(packageName) }) + ipcMain.handle('dialog:selectFolder', async () => { + const result = await dialog.showOpenDialog(mainWindow!, { + properties: ['openDirectory'] + }) + return result.canceled ? null : result.filePaths[0] ?? null + }) + ipcMain.handle('app:launchAtLogin:get', () => { return app.getLoginItemSettings().openAtLogin }) @@ -746,6 +773,11 @@ if (!gotTheLock) { createMainWindow() } + // Initialize auto-updater + if (mainWindow) { + initUpdater(mainWindow) + } + app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createMainWindow() else { diff --git a/src/main/updater.ts b/src/main/updater.ts new file mode 100644 index 0000000..81d2047 --- /dev/null +++ b/src/main/updater.ts @@ -0,0 +1,66 @@ +import { autoUpdater, type UpdateInfo } from 'electron-updater' +import log from 'electron-log' +import { BrowserWindow } from 'electron' + +let mainWin: BrowserWindow | null = null + +const send = (type: string, data?: any): void => { + mainWin?.webContents.send('main:data', { type, data }) +} + +export function initUpdater(window: BrowserWindow): void { + mainWin = window + + autoUpdater.logger = log + autoUpdater.autoDownload = false + autoUpdater.autoInstallOnAppQuit = true + + autoUpdater.on('checking-for-update', () => { + send('update:checking') + }) + + autoUpdater.on('update-available', (info: UpdateInfo) => { + send('update:available', { + version: info.version, + releaseDate: info.releaseDate + }) + }) + + autoUpdater.on('update-not-available', (_info: UpdateInfo) => { + send('update:not-available') + }) + + autoUpdater.on('download-progress', (progress) => { + send('update:download-progress', { + percent: progress.percent, + bytesPerSecond: progress.bytesPerSecond, + transferred: progress.transferred, + total: progress.total + }) + }) + + autoUpdater.on('update-downloaded', (_info: UpdateInfo) => { + send('update:downloaded') + }) + + autoUpdater.on('error', (error: Error) => { + send('update:error', { message: error?.message ?? 'Update error' }) + }) + + // Auto-check on launch (silently) + autoUpdater.checkForUpdates().catch((err) => { + log.warn('Auto update check failed:', err) + }) +} + +export async function checkForUpdates(): Promise { + await autoUpdater.checkForUpdates() +} + +export async function downloadUpdate(): Promise { + await autoUpdater.downloadUpdate() +} + +export function installUpdate(): void { + autoUpdater.quitAndInstall(false, true) +} diff --git a/src/main/utils/index.ts b/src/main/utils/index.ts index b7fb8ea..b39d3e0 100644 --- a/src/main/utils/index.ts +++ b/src/main/utils/index.ts @@ -738,6 +738,7 @@ export interface AppConfig { openTerminal: { enabled: boolean port: number + cwd: string } envVars: Record } @@ -753,7 +754,8 @@ const DEFAULT_CONFIG: AppConfig = { }, openTerminal: { enabled: false, - port: 8000 + port: 8000, + cwd: '' }, envVars: {} } diff --git a/src/main/utils/open-terminal.ts b/src/main/utils/open-terminal.ts index 2cb7462..fcf7af4 100644 --- a/src/main/utils/open-terminal.ts +++ b/src/main/utils/open-terminal.ts @@ -63,11 +63,14 @@ export const startOpenTerminal = async ( } } + const cwd = config.openTerminal?.cwd || require('os').homedir() + const commandArgs = [ '-m', 'uv', 'run', 'open-terminal', 'run', '--host', host, '--port', availablePort.toString(), - '--api-key', generatedKey + '--api-key', generatedKey, + '--cwd', cwd ] log.info('Starting Open Terminal...', pythonPath, commandArgs.join(' ')) diff --git a/src/preload/index.ts b/src/preload/index.ts index 9bd2333..8752b27 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -138,9 +138,16 @@ const api = { getConnections: () => ipcRenderer.invoke('connections:list'), addConnection: (connection: any) => ipcRenderer.invoke('connections:add', connection), removeConnection: (id: string) => ipcRenderer.invoke('connections:remove', id), + updateConnection: (id: string, updates: any) => ipcRenderer.invoke('connections:update', id, updates), setDefaultConnection: (id: string) => ipcRenderer.invoke('connections:setDefault', id), connectTo: (id: string) => ipcRenderer.invoke('connections:connect', id), - validateUrl: (url: string) => ipcRenderer.invoke('validate:url', url) + validateUrl: (url: string) => ipcRenderer.invoke('validate:url', url), + selectFolder: () => ipcRenderer.invoke('dialog:selectFolder'), + + // Updater + checkForUpdates: () => ipcRenderer.invoke('updater:check'), + downloadUpdate: () => ipcRenderer.invoke('updater:download'), + installUpdate: () => ipcRenderer.invoke('updater:install') } if (process.contextIsolated) { diff --git a/src/renderer/src/lib/components/Main.svelte b/src/renderer/src/lib/components/Main.svelte index c89dfef..d190b5b 100644 --- a/src/renderer/src/lib/components/Main.svelte +++ b/src/renderer/src/lib/components/Main.svelte @@ -116,7 +116,7 @@ >
e.stopPropagation()} > diff --git a/src/renderer/src/lib/components/Main/Connections.svelte b/src/renderer/src/lib/components/Main/Connections.svelte index 384fd17..45d1049 100644 --- a/src/renderer/src/lib/components/Main/Connections.svelte +++ b/src/renderer/src/lib/components/Main/Connections.svelte @@ -148,6 +148,13 @@ const connect = async (id: string) => { destroyTerminal() showingLogs = false + // Toggle: clicking the active connection unselects it + if (activeConnectionId === id && view === 'connected') { + activeConnectionId = '' + connectedUrl = '' + view = 'welcome' + return + } if (openConnections.has(id)) { activeConnectionId = id connectedUrl = openConnections.get(id)! @@ -412,6 +419,11 @@ {onOpenSettings} onToggleOpenTerminal={toggleOpenTerminal} onToggleOtLogs={toggleOtLogs} + onRename={async (id, name) => { + await window.electronAPI.updateConnection(id, { name }) + connections.set(await window.electronAPI.getConnections()) + }} + onRemove={remove} {openGithub} /> {/if} diff --git a/src/renderer/src/lib/components/Main/Connections/Sidebar.svelte b/src/renderer/src/lib/components/Main/Connections/Sidebar.svelte index 946d6c9..ba63356 100644 --- a/src/renderer/src/lib/components/Main/Connections/Sidebar.svelte +++ b/src/renderer/src/lib/components/Main/Connections/Sidebar.svelte @@ -19,6 +19,8 @@ onOpenSettings: () => void onToggleOpenTerminal: () => void onToggleOtLogs: () => void + onRename: (id: string, name: string) => void + onRemove: (id: string) => void openGithub: () => void } @@ -38,8 +40,33 @@ onOpenSettings, onToggleOpenTerminal, onToggleOtLogs, + onRename, + onRemove, openGithub }: Props = $props() + + // Inline rename state + let editingId = $state(null) + let editValue = $state('') + let menuOpenId = $state(null) + + const startRename = (id: string, currentName: string) => { + editingId = id + editValue = currentName + } + + const commitRename = () => { + if (editingId && editValue.trim()) { + onRename(editingId, editValue.trim()) + } + editingId = null + editValue = '' + } + + const cancelRename = () => { + editingId = null + editValue = '' + }
!isLocalDisabled && onConnect(localConn.id)} onkeydown={(e) => e.key === 'Enter' && !isLocalDisabled && onConnect(localConn.id)} > - {#if serverStatus === 'starting'} + {#if serverStatus === 'starting' || (serverStatus === 'running' && !serverReachable)}
-
+ + + +
{:else if serverReachable}
@@ -101,35 +129,82 @@
{/if} - Open WebUI - - + {/if} + +
+ + + {#if menuOpenId === 'local'} +
{ e.stopPropagation(); menuOpenId = null }}>
+
+
+ + +
+
+ {/if} +
{/if} @@ -145,8 +220,8 @@ : 'hover:bg-white/[0.05]'}" role="button" tabindex="0" - onclick={() => onConnect(conn.id)} - onkeydown={(e) => e.key === 'Enter' && onConnect(conn.id)} + onclick={() => editingId !== conn.id && onConnect(conn.id)} + onkeydown={(e) => e.key === 'Enter' && editingId !== conn.id && onConnect(conn.id)} > - {conn.name} - + {/if} + + +
+ + + {#if menuOpenId === conn.id} +
{ e.stopPropagation(); menuOpenId = null }}>
+
+
+ + +
+
+
+ +
+
+ {/if} +
{/each} diff --git a/src/renderer/src/lib/components/Main/Settings/About.svelte b/src/renderer/src/lib/components/Main/Settings/About.svelte index bd881a7..7537742 100644 --- a/src/renderer/src/lib/components/Main/Settings/About.svelte +++ b/src/renderer/src/lib/components/Main/Settings/About.svelte @@ -1,18 +1,87 @@
@@ -40,6 +109,71 @@
{$appInfo?.platform ?? 'Unknown'}
+ +
+
+
+
Software Update
+ {#if updateStatus === 'up-to-date'} +
You're on the latest version
+ {:else if updateStatus === 'available' && updateVersion} +
Version {updateVersion} is available
+ {:else if updateStatus === 'downloading'} +
Downloading… {downloadPercent}%
+ {:else if updateStatus === 'downloaded'} +
Update ready — restart to apply
+ {:else if updateStatus === 'error'} +
{updateError}
+ {/if} +
+ +
+ {#if updateStatus === 'idle' || updateStatus === 'up-to-date' || updateStatus === 'error'} + + {:else if updateStatus === 'checking'} + + {:else if updateStatus === 'available'} + + {:else if updateStatus === 'downloading'} +
+
+
+
+ {downloadPercent}% +
+ {:else if updateStatus === 'downloaded'} + + {/if} +
+
+
+
+ {:else}
+
+
+
Working Directory
+
Starting directory for terminal sessions
+
+
+ + updateOtConfig('cwd', (e.target as HTMLInputElement).value.trim())} + /> + +
+
+ {#if isRunning && otInfo}
Running Instance
diff --git a/src/renderer/src/lib/components/Main/Settings/OpenWebUI.svelte b/src/renderer/src/lib/components/Main/Settings/OpenWebUI.svelte index 9edffb7..1c4bf0d 100644 --- a/src/renderer/src/lib/components/Main/Settings/OpenWebUI.svelte +++ b/src/renderer/src/lib/components/Main/Settings/OpenWebUI.svelte @@ -6,6 +6,7 @@ let updating = $state(false) let stopping = $state(false) let starting = $state(false) + let restarting = $state(false) let version = $state(null) let loaded = $state(false) @@ -54,6 +55,17 @@ starting = false } + const restartServer = async () => { + restarting = true + try { + await window.electronAPI.restartServer() + serverStatus = 'running' + } catch (e) { + console.error('Failed to restart server:', e) + } + restarting = false + } + const updatePackage = async () => { updating = true try { @@ -144,6 +156,21 @@ Stop {/if} + {:else}