feat(desktop): Linux AppImage polish — window controls, updater UX, docs (#2144)
* feat(electron): add Linux AppImage releases * ci: cache Linux OpenCode CLI artifacts * fix(ci): await Linux release inventory check * fix(electron): add frameless window controls on Linux desktop Linux AppImages were created without native WM decorations and without in-app controls, leaving users unable to close the window with a mouse. Treat Linux like Windows: frameless BrowserWindow plus the existing WindowsWindowControls header buttons and app-menu entry. macOS keeps hidden title bar with traffic lights unchanged. Shared usesFramelessElectronChrome() helper drives main window, mini chat, header insets, and titlebar controls. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * feat(desktop): add configurable window controls position by OS Add desktopWindowControlsPosition setting (auto/left/right) with OS-aware defaults: Linux left, Windows right. Wire frameless chrome controls in Header, TitlebarLeftControls, and MiniChatLayout, plus a Sessions settings control for Windows and Linux desktop shells. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * fix(desktop): address Linux AppImage release review findings Propagate updater capability errors to the UI, treat missing latest-linux.yml feeds as no-update, stop installed-apps IPC spam on Linux, document FUSE/AppImage limits, add CHANGELOG entry, migrate remaining btriapitsyn URLs, and run Electron Linux unit tests on PRs. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> --------- Co-authored-by: jibanez-staticduo <staticduo@gmail.com> Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
co-authored by
Serhii Dziupin
jibanez-staticduo
parent
7e248d4e9b
commit
502c96630e
+34
-31
@@ -14,6 +14,9 @@ import { ElectronSshManager } from './ssh-manager.mjs';
|
||||
import { createTrayController } from './tray.mjs';
|
||||
import { resolveManagedOpenCodeCwd } from './opencode-cwd.mjs';
|
||||
import { sanitizeRuntimeRequestHeaders } from './runtime-request-headers.mjs';
|
||||
import { assertUpdaterCapability } from './updater-capability.mjs';
|
||||
import { checkForDesktopUpdate } from './updater-check.mjs';
|
||||
import { resolveUpdaterFeed } from './updater-feed.mjs';
|
||||
import { mintOutsideFileGrant } from '@openchamber/web/server/lib/fs/routes.js';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
@@ -60,6 +63,9 @@ const shouldStartInBackground = (loginItemSettings = readLoginItemSettings()) =>
|
||||
// Set the product name early so electron-log derives its log directory as
|
||||
// ~/Library/Logs/OpenChamber/ (not ~/Library/Logs/@openchamber/electron/).
|
||||
app.setName('OpenChamber');
|
||||
if (process.platform === 'linux') {
|
||||
app.setDesktopName('openchamber.desktop');
|
||||
}
|
||||
if (isDev) {
|
||||
app.setPath('userData', path.join(app.getPath('appData'), 'OpenChamber Dev'));
|
||||
}
|
||||
@@ -2182,12 +2188,11 @@ const readThemeSource = () => {
|
||||
};
|
||||
|
||||
const getWindowIconPath = () => {
|
||||
if (process.platform !== 'win32' && process.platform !== 'linux') {
|
||||
return undefined;
|
||||
}
|
||||
if (process.platform !== 'win32' && process.platform !== 'linux') return undefined;
|
||||
const iconFileName = process.platform === 'linux' ? 'icon.png' : 'icon.ico';
|
||||
const iconPath = isDev
|
||||
? path.join(__dirname, 'resources', 'icons', 'icon.ico')
|
||||
: path.join(process.resourcesPath, 'icons', 'icon.ico');
|
||||
? path.join(__dirname, 'resources', 'icons', iconFileName)
|
||||
: path.join(process.resourcesPath, 'icons', iconFileName);
|
||||
return fs.existsSync(iconPath) ? iconPath : undefined;
|
||||
};
|
||||
|
||||
@@ -2209,7 +2214,8 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} }
|
||||
const desktopRequestHeaders = rendererRuntimeConfig.requestHeaders || {};
|
||||
const desktopHome = os.homedir() || '';
|
||||
const desktopMacosMajor = String(macosMajorVersion());
|
||||
const usesCustomTitleBar = process.platform === 'darwin' || process.platform === 'win32';
|
||||
const usesFramelessChrome = process.platform === 'win32' || process.platform === 'linux';
|
||||
const usesCustomTitleBar = process.platform === 'darwin' || usesFramelessChrome;
|
||||
// macOS vibrancy, on by default; users can disable it (Appearance settings).
|
||||
const useVibrancy = process.platform === 'darwin' && readSettingsRoot().desktopVibrancy !== false;
|
||||
const titleBarOverlayEnabled = false;
|
||||
@@ -2231,7 +2237,7 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} }
|
||||
// here: setting it in the constructor leaves the material uncomposited on a
|
||||
// cold launch until a window event. No `transparent: true` either — vibrancy
|
||||
// alone is enough and composites reliably once applied to a live window.
|
||||
frame: process.platform === 'win32' ? false : undefined,
|
||||
frame: usesFramelessChrome ? false : undefined,
|
||||
autoHideMenuBar: autoHidesNativeMenuBar,
|
||||
// Electron's hiddenInset adds its own extra inset, which leaves the controls
|
||||
// visibly lower than the app header. Use a plain hidden title bar instead.
|
||||
@@ -2604,6 +2610,7 @@ const createMiniChatWindow = async ({ mode, sessionId = '', directory = '', proj
|
||||
const desktopRequestHeaders = effectiveRuntimeConfig.requestHeaders || {};
|
||||
const desktopHome = os.homedir() || '';
|
||||
const desktopMacosMajor = String(macosMajorVersion());
|
||||
const usesFramelessChrome = process.platform === 'win32' || process.platform === 'linux';
|
||||
// macOS vibrancy, on by default; users can disable it (Appearance settings).
|
||||
const useVibrancy = process.platform === 'darwin' && readSettingsRoot().desktopVibrancy !== false;
|
||||
const browserWindow = new BrowserWindow({
|
||||
@@ -2619,9 +2626,9 @@ const createMiniChatWindow = async ({ mode, sessionId = '', directory = '', proj
|
||||
// here: setting it in the constructor leaves the material uncomposited on a
|
||||
// cold launch until a window event. No `transparent: true` either — vibrancy
|
||||
// alone is enough and composites reliably once applied to a live window.
|
||||
frame: process.platform === 'win32' ? false : undefined,
|
||||
frame: usesFramelessChrome ? false : undefined,
|
||||
autoHideMenuBar: process.platform !== 'darwin',
|
||||
titleBarStyle: process.platform === 'darwin' || process.platform === 'win32' ? 'hidden' : 'default',
|
||||
titleBarStyle: process.platform === 'darwin' || usesFramelessChrome ? 'hidden' : 'default',
|
||||
trafficLightPosition: process.platform === 'darwin' ? { x: 16, y: 17 } : undefined,
|
||||
webPreferences: {
|
||||
additionalArguments: [
|
||||
@@ -2819,10 +2826,6 @@ const compareSemver = (left, right) => {
|
||||
return 0;
|
||||
};
|
||||
|
||||
const parseGithubRepo = () => {
|
||||
return { owner: 'openchamber', repo: 'openchamber' };
|
||||
};
|
||||
|
||||
const setupAutoUpdater = () => {
|
||||
if (!app.isPackaged) {
|
||||
return;
|
||||
@@ -2834,11 +2837,13 @@ const setupAutoUpdater = () => {
|
||||
autoUpdater.disableWebInstaller = false;
|
||||
autoUpdater.logger = log;
|
||||
|
||||
const { owner, repo } = parseGithubRepo();
|
||||
autoUpdater.setFeedURL({
|
||||
provider: 'github',
|
||||
owner,
|
||||
repo,
|
||||
const testBuild = typeof __OPENCHAMBER_UPDATER_E2E_BUILD__ !== 'undefined'
|
||||
&& __OPENCHAMBER_UPDATER_E2E_BUILD__ === true;
|
||||
const feed = resolveUpdaterFeed({ testBuild });
|
||||
autoUpdater.setFeedURL(feed);
|
||||
log.info('[electron] updater feed configured', {
|
||||
provider: feed.provider,
|
||||
target: feed.provider === 'github' ? `${feed.owner}/${feed.repo}` : feed.url,
|
||||
});
|
||||
|
||||
autoUpdater.on('download-progress', (progress) => {
|
||||
@@ -3798,7 +3803,7 @@ const handleInvoke = async (browserWindow, command, args = {}) => {
|
||||
emitToAllWindows('openchamber:installed-apps-updated', apps);
|
||||
};
|
||||
if (process.platform !== 'darwin' && process.platform !== 'win32') {
|
||||
throw new Error('desktop_get_installed_apps is only supported on macOS and Windows');
|
||||
return { apps: [], hasCache: false, isCacheStale: false, supported: false };
|
||||
}
|
||||
if (!hasCache || isCacheStale || args.force === true) {
|
||||
void refresh();
|
||||
@@ -3901,22 +3906,18 @@ const handleInvoke = async (browserWindow, command, args = {}) => {
|
||||
}
|
||||
|
||||
case 'desktop_check_for_updates': {
|
||||
assertUpdaterCapability({ packaged: app.isPackaged });
|
||||
const currentVersion = APP_VERSION;
|
||||
let updateResult = null;
|
||||
try {
|
||||
updateResult = await autoUpdater.checkForUpdates();
|
||||
} catch {
|
||||
}
|
||||
|
||||
const updateInfo = updateResult?.updateInfo;
|
||||
const nextVersion =
|
||||
(typeof updateInfo?.version === 'string' && updateInfo.version) ||
|
||||
currentVersion;
|
||||
const available = compareSemver(nextVersion, currentVersion) > 0;
|
||||
const { available, updateInfo, updateResult, nextVersion, pendingUpdate } = await checkForDesktopUpdate({
|
||||
autoUpdater,
|
||||
currentVersion,
|
||||
pendingUpdate: state.pendingUpdate,
|
||||
compareVersions: compareSemver,
|
||||
});
|
||||
const body =
|
||||
(typeof updateInfo?.releaseNotes === 'string' && updateInfo.releaseNotes.trim() ? updateInfo.releaseNotes : null) ||
|
||||
await parseRelevantChangelogNotes(currentVersion, nextVersion);
|
||||
state.pendingUpdate = available ? { version: nextVersion, electronUpdate: updateResult } : null;
|
||||
state.pendingUpdate = pendingUpdate;
|
||||
return {
|
||||
available,
|
||||
currentVersion,
|
||||
@@ -3929,6 +3930,7 @@ const handleInvoke = async (browserWindow, command, args = {}) => {
|
||||
}
|
||||
|
||||
case 'desktop_download_and_install_update':
|
||||
assertUpdaterCapability({ packaged: app.isPackaged });
|
||||
if (!state.pendingUpdate) {
|
||||
throw new Error('No pending update');
|
||||
}
|
||||
@@ -3974,6 +3976,7 @@ const handleInvoke = async (browserWindow, command, args = {}) => {
|
||||
|
||||
case 'desktop_restart': {
|
||||
const applyUpdate = Boolean(state.pendingUpdate?.downloaded && app.isPackaged);
|
||||
if (applyUpdate) assertUpdaterCapability({ packaged: app.isPackaged });
|
||||
log.info(`[electron] desktop_restart applyUpdate=${applyUpdate} packaged=${app.isPackaged}`);
|
||||
if (applyUpdate && process.platform === 'darwin' && typeof app.isInApplicationsFolder === 'function') {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user