Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,15 @@
"icons:providers": "node script/copy-provider-icons.mjs",
"smoke:shared": "esbuild test/shared.ts --bundle --platform=node --format=cjs --packages=external --outfile=test/.out/shared.cjs && node test/.out/shared.cjs",
"smoke:app": "esbuild test/smoke.ts --bundle --platform=node --format=cjs --packages=external --outfile=test/.out/smoke.cjs && electron test/.out/smoke.cjs",
"smoke": "npm run smoke:shared && npm run smoke:i18n && npm run smoke:store && npm run smoke:multirepo && npm run smoke:live && npm run smoke:cookies && npm run smoke:app",
"smoke": "npm run smoke:shared && npm run smoke:i18n && npm run smoke:store && npm run smoke:multirepo && npm run smoke:live && npm run smoke:cookies && npm run smoke:kernel && npm run smoke:app",
"smoke:multirepo": "esbuild test/multirepo.ts --bundle --platform=node --format=cjs --packages=external --outfile=test/.out/multirepo.cjs && node test/.out/multirepo.cjs",
"smoke:live": "esbuild test/live-multirepo.ts --bundle --platform=node --format=cjs --packages=external --outfile=test/.out/live.cjs && electron test/.out/live.cjs",
"smoke:cliproxy": "esbuild test/cliproxy.ts --bundle --platform=node --format=cjs --packages=external --outfile=test/.out/cliproxy.cjs && electron test/.out/cliproxy.cjs",
"worktree:setup": "npm ci --prefer-offline --no-audit --no-fund && electron-builder install-app-deps",
"smoke:store": "node test/store-guard.mjs",
"smoke:cookies": "esbuild test/cookies.ts --bundle --platform=node --format=cjs --packages=external --outfile=test/.out/cookies.cjs && electron test/.out/cookies.cjs",
"smoke:i18n": "esbuild test/i18n.ts --bundle --platform=node --format=cjs --outfile=test/.out/i18n.cjs && node test/.out/i18n.cjs",
"smoke:kernel": "esbuild test/kernel.ts --bundle --platform=node --format=cjs --outfile=test/.out/kernel.cjs && node test/.out/kernel.cjs",
"i18n:translate": "node script/i18n-translate.mjs",
"canvas": "vite --config test/canvas/vite.config.mjs",
"smoke:canvas": "electron test/canvas/smoke.cjs",
Expand Down
32 changes: 22 additions & 10 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset'
import macDockIcon from '../../resources/icon-mac.png?asset'
import { registerIpc } from './ipc'
import { registerKernelIpc } from './ipc/kernel'
import { getDb } from './db/database'
import { startLoopScheduler } from './services/loops'
import { listModels } from './services/models'
Expand All @@ -30,9 +31,11 @@ import {
import { resolveThemeById } from './services/themes'
import * as repo from './db/repo'

let mainWindow: BrowserWindow | null = null

function createWindow(): BrowserWindow {
const isMac = process.platform === 'darwin'
const mainWindow = new BrowserWindow({
const window = new BrowserWindow({
width: 1100,
height: 720,
minWidth: 760,
Expand All @@ -54,30 +57,39 @@ function createWindow(): BrowserWindow {
}
})

mainWindow.on('ready-to-show', () => {
mainWindow = window
window.on('closed', () => {
if (mainWindow === window) mainWindow = null
})

window.on('ready-to-show', () => {
// Repaint the native window controls from the active theme before the
// window is first shown. The constructor can only reach built-in themes
// synchronously; this covers a user theme, whose file has to be read.
void resolveThemeById(repo.getSettings().activeThemeId, chromePlatform())
.then((theme) => applyWindowChrome(mainWindow, theme))
.then((theme) => applyWindowChrome(window, theme))
.catch(() => undefined)
mainWindow.show()
window.show()
})

// Open external links in the user's browser instead of a new Electron window.
mainWindow.webContents.setWindowOpenHandler((details) => {
window.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
window.webContents.on('will-navigate', (event, url) => {
const currentUrl = window.webContents.getURL()
if (currentUrl && url !== currentUrl) event.preventDefault()
})

// Load the Vite dev server in development, or the built HTML in production.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
window.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
window.loadFile(join(__dirname, '../renderer/index.html'))
}

return mainWindow
return window
}

/**
Expand Down Expand Up @@ -121,6 +133,7 @@ app.whenReady().then(() => {
// Open the database (runs migrations) and wire up IPC before the first window.
getDb()
registerIpc()
registerKernelIpc(() => mainWindow)
// Anonymous usage tracking (opt-out in Settings). Deliberately after the DB
// and IPC are up so nothing here can delay the first window, and it owns its
// own storage - a failure in it can't touch either.
Expand All @@ -133,8 +146,7 @@ app.whenReady().then(() => {
// backfilled rows can be priced (else they'd all cost $0). Best-effort + async.
void warmCatalogThenBackfill()

const mainWindow = createWindow()
initAutoUpdater(mainWindow)
initAutoUpdater(createWindow())

app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
Expand Down
72 changes: 72 additions & 0 deletions src/main/ipc/kernel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { dialog, ipcMain, type BrowserWindow, type IpcMainInvokeEvent } from 'electron'
import { CHANNELS } from '../../shared/ipc'
import {
getKernelStatus,
installKernelTools,
setKernelAgentAccess,
startKernelDriver,
toggleTestSigning,
uninstallKernelTools
} from '../services/kernel'

function requireMainWindow(
event: IpcMainInvokeEvent,
getMainWindow: () => BrowserWindow | null
): BrowserWindow {
const mainWindow = getMainWindow()
if (
!mainWindow ||
mainWindow.isDestroyed() ||
event.sender !== mainWindow.webContents ||
event.senderFrame !== mainWindow.webContents.mainFrame
) {
throw new Error('Kernel Tools requests are only accepted from the main Roxy window.')
}
return mainWindow
}

export function registerKernelIpc(getMainWindow: () => BrowserWindow | null): void {
ipcMain.handle(CHANNELS.kernelStatus, (event) => {
requireMainWindow(event, getMainWindow)
return getKernelStatus()
})
ipcMain.handle(CHANNELS.kernelInstall, (event) => {
requireMainWindow(event, getMainWindow)
return installKernelTools()
})
ipcMain.handle(CHANNELS.kernelStart, (event) => {
requireMainWindow(event, getMainWindow)
return startKernelDriver()
})
ipcMain.handle(CHANNELS.kernelSetAgentAccess, async (event, enable: boolean) => {
const mainWindow = requireMainWindow(event, getMainWindow)
if (typeof enable !== 'boolean') throw new TypeError('enable must be a boolean.')
if (enable) {
const { response } = await dialog.showMessageBox(mainWindow, {
type: 'warning',
title: 'Enable Kernel Tools agent access?',
message: 'This gives agents elevated access to protected system resources.',
detail: 'Only continue in an isolated test environment with no sensitive data.',
buttons: ['Cancel', 'Enable agent access'],
defaultId: 0,
cancelId: 0,
noLink: true
})
if (response !== 1) {
return { ok: false, error: 'Agent access was not enabled.', steps: [] }
}
}
return setKernelAgentAccess(enable)
})
ipcMain.handle(CHANNELS.kernelUninstall, (event, disableSigning: boolean) => {
requireMainWindow(event, getMainWindow)
if (typeof disableSigning !== 'boolean')
throw new TypeError('disableSigning must be a boolean.')
return uninstallKernelTools(disableSigning)
})
ipcMain.handle(CHANNELS.kernelToggleTestSigning, (event, enable: boolean) => {
requireMainWindow(event, getMainWindow)
if (typeof enable !== 'boolean') throw new TypeError('enable must be a boolean.')
return toggleTestSigning(enable)
})
}
Loading
Loading