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
@@ -0,0 +1,96 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
import { linuxAppImageArchSuffix, readElfArchitecture, verifyExtractedPayload } from './verify-linux-appimage.mjs';
|
||||
|
||||
const writeElf = (filePath, architecture) => {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
const header = Buffer.alloc(20);
|
||||
header.set([0x7f, 0x45, 0x4c, 0x46, 2, 1]);
|
||||
header.writeUInt16LE(architecture === 'x64' ? 62 : 183, 18);
|
||||
fs.writeFileSync(filePath, header, { mode: 0o755 });
|
||||
};
|
||||
|
||||
const createPayload = () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-payload-test-'));
|
||||
fs.writeFileSync(path.join(root, 'openchamber.desktop'), [
|
||||
'[Desktop Entry]', 'Name=OpenChamber', 'Exec=AppRun --no-sandbox %U', 'Icon=openchamber', 'StartupWMClass=openchamber', '',
|
||||
].join('\n'));
|
||||
writeElf(path.join(root, 'openchamber'), 'x64');
|
||||
writeElf(path.join(root, 'resources/opencode-cli/opencode'), 'x64');
|
||||
for (const name of ['better_sqlite3.node', 'pty.node', 'sherpa-onnx.node']) {
|
||||
writeElf(path.join(root, 'resources/app.asar.unpacked/node_modules', name), 'x64');
|
||||
}
|
||||
return root;
|
||||
};
|
||||
|
||||
test('reads supported ELF architectures', () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-elf-test-'));
|
||||
try {
|
||||
writeElf(path.join(root, 'x64'), 'x64');
|
||||
writeElf(path.join(root, 'arm64'), 'arm64');
|
||||
assert.equal(readElfArchitecture(path.join(root, 'x64')), 'x64');
|
||||
assert.equal(readElfArchitecture(path.join(root, 'arm64')), 'arm64');
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('AppImage artifact names use electron-builder arch suffixes', () => {
|
||||
assert.equal(linuxAppImageArchSuffix('x64'), 'x86_64');
|
||||
assert.equal(linuxAppImageArchSuffix('arm64'), 'arm64');
|
||||
});
|
||||
|
||||
test('verifies identity, version, and native payload architecture', () => {
|
||||
const root = createPayload();
|
||||
try {
|
||||
const result = verifyExtractedPayload({
|
||||
root,
|
||||
targetArchitecture: 'x64',
|
||||
expectedOpenCodeVersion: '1.17.18',
|
||||
runCliVersion: () => '1.17.18',
|
||||
});
|
||||
assert.equal(result.nativeModuleCount, 3);
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('fails on a missing native module', () => {
|
||||
const root = createPayload();
|
||||
try {
|
||||
fs.rmSync(path.join(root, 'resources/app.asar.unpacked/node_modules/pty.node'));
|
||||
assert.throws(() => verifyExtractedPayload({
|
||||
root,
|
||||
targetArchitecture: 'x64',
|
||||
expectedOpenCodeVersion: '1.17.18',
|
||||
runCliVersion: () => '1.17.18',
|
||||
}), /Missing packaged native module: pty\.node/);
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('fails on wrong CLI version or native architecture', () => {
|
||||
const root = createPayload();
|
||||
try {
|
||||
assert.throws(() => verifyExtractedPayload({
|
||||
root,
|
||||
targetArchitecture: 'x64',
|
||||
expectedOpenCodeVersion: '1.17.18',
|
||||
runCliVersion: () => '1.17.17',
|
||||
}), /OpenCode CLI version mismatch/);
|
||||
writeElf(path.join(root, 'resources/app.asar.unpacked/node_modules/pty.node'), 'arm64');
|
||||
assert.throws(() => verifyExtractedPayload({
|
||||
root,
|
||||
targetArchitecture: 'x64',
|
||||
expectedOpenCodeVersion: '1.17.18',
|
||||
runCliVersion: () => '1.17.18',
|
||||
}), /Native module architecture mismatch/);
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user