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:
Serhii Dziupin
2026-07-13 08:59:31 +03:00
committed by GitHub
co-authored by Serhii Dziupin jibanez-staticduo
parent 7e248d4e9b
commit 502c96630e
55 changed files with 1679 additions and 112 deletions
+7
View File
@@ -31,3 +31,10 @@ jobs:
- name: Lint
run: bun run lint
- name: Electron Linux packaging unit tests
working-directory: packages/electron
run: |
bun run test:architecture
bun run test:updater
bun run type-check
+178 -1
View File
@@ -359,6 +359,148 @@ jobs:
path: packages/electron/dist/latest.yml
retention-days: 1
build-desktop-electron-linux:
needs: create-release
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-24.04
arch: x64
host_arch: x86_64
artifact_arch: x86_64
manifest: latest-linux.yml
- runner: ubuntu-24.04-arm
arch: arm64
host_arch: aarch64
artifact_arch: arm64
manifest: latest-linux-arm64.yml
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '22'
- name: Verify native Linux architecture
env:
EXPECTED_HOST_ARCH: ${{ matrix.host_arch }}
OPENCHAMBER_TARGET_ARCH: ${{ matrix.arch }}
run: |
set -euo pipefail
test "$(uname -m)" = "$EXPECTED_HOST_ARCH"
test "$(node -p 'process.arch')" = "$OPENCHAMBER_TARGET_ARCH"
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Get bundled OpenCode CLI version
id: opencode_cli_version
shell: bash
run: |
VERSION=$(node -p "require('./package.json').dependencies['@opencode-ai/sdk']")
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Cache bundled OpenCode CLI artifact
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: packages/electron/.cache/opencode-cli
key: opencode-cli-${{ runner.os }}-${{ matrix.arch }}-${{ steps.opencode_cli_version.outputs.version }}
restore-keys: |
opencode-cli-${{ runner.os }}-${{ matrix.arch }}-
- name: Run focused Electron release tests
working-directory: packages/electron
run: |
bun run test:architecture
bun run test:updater
- name: Build and package Linux AppImage
working-directory: packages/electron
env:
OPENCHAMBER_TARGET_ARCH: ${{ matrix.arch }}
run: |
set -euo pipefail
bun run build:web-assets
bun run prepare:opencode-cli
bun run verify:opencode-cli
bun run bundle:main
bun run rebuild:native
node ./scripts/package.mjs --linux --${{ matrix.arch }} --publish=never
bun run verify:opencode-cli:packaged
bun run verify:linux-appimage
- name: Validate Linux update manifest
working-directory: packages/electron
env:
VERSION: ${{ needs.create-release.outputs.version }}
ARTIFACT_ARCH: ${{ matrix.artifact_arch }}
MANIFEST: ${{ matrix.manifest }}
run: |
set -euo pipefail
APPIMAGE="dist/OpenChamber-${VERSION}-linux-${ARTIFACT_ARCH}.AppImage"
node ./scripts/verify-update-manifest.mjs "dist/${MANIFEST}" "$APPIMAGE" "$VERSION"
- name: Upload validated Linux release files
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: linux-release-${{ matrix.arch }}
path: |
packages/electron/dist/OpenChamber-${{ needs.create-release.outputs.version }}-linux-${{ matrix.artifact_arch }}.AppImage
packages/electron/dist/${{ matrix.manifest }}
if-no-files-found: error
retention-days: 1
publish-electron-linux:
needs: [create-release, build-desktop-electron-linux]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Download x64 Linux release files
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: linux-release-x64
path: artifacts/x64
- name: Download arm64 Linux release files
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: linux-release-arm64
path: artifacts/arm64
- name: Revalidate separate Linux manifests
env:
VERSION: ${{ needs.create-release.outputs.version }}
run: |
set -euo pipefail
node packages/electron/scripts/verify-update-manifest.mjs \
artifacts/x64/latest-linux.yml \
"artifacts/x64/OpenChamber-${VERSION}-linux-x86_64.AppImage" \
"$VERSION"
node packages/electron/scripts/verify-update-manifest.mjs \
artifacts/arm64/latest-linux-arm64.yml \
"artifacts/arm64/OpenChamber-${VERSION}-linux-arm64.AppImage" \
"$VERSION"
- name: Upload Linux AppImages and manifests to release
if: ${{ github.event.inputs.dry_run != 'true' }}
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
with:
tag_name: v${{ needs.create-release.outputs.version }}
files: |
artifacts/x64/OpenChamber-${{ needs.create-release.outputs.version }}-linux-x86_64.AppImage
artifacts/x64/latest-linux.yml
artifacts/arm64/OpenChamber-${{ needs.create-release.outputs.version }}-linux-arm64.AppImage
artifacts/arm64/latest-linux-arm64.yml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
combine-electron-manifests:
needs: [create-release, build-desktop-electron-macos]
runs-on: ubuntu-latest
@@ -404,12 +546,47 @@ jobs:
secrets: inherit
finalize-release:
needs: [create-release, build-desktop-electron-macos, build-desktop-electron-windows, publish-npm, combine-electron-manifests, mobile-release]
needs: [create-release, build-desktop-electron-macos, build-desktop-electron-windows, build-desktop-electron-linux, publish-electron-linux, publish-npm, combine-electron-manifests, mobile-release]
runs-on: ubuntu-latest
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
DISCORD_UPDATE_ROLE_ID: ${{ secrets.DISCORD_UPDATE_ROLE_ID }}
steps:
- name: Verify final Linux release asset inventory
if: ${{ github.event.inputs.dry_run != 'true' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPOSITORY: ${{ github.repository }}
VERSION: ${{ needs.create-release.outputs.version }}
run: |
node - <<'NODE'
(async () => {
const { REPOSITORY: repo, VERSION: version, GITHUB_TOKEN: token } = process.env;
const expected = [
`OpenChamber-${version}-linux-x86_64.AppImage`,
'latest-linux.yml',
`OpenChamber-${version}-linux-arm64.AppImage`,
'latest-linux-arm64.yml',
];
const response = await fetch(`https://api.github.com/repos/${repo}/releases/tags/v${version}`, {
headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json' },
});
if (!response.ok) throw new Error(`Failed to inspect release assets: ${response.status} ${await response.text()}`);
const release = await response.json();
for (const name of expected) {
const matches = release.assets.filter((asset) => asset.name === name);
if (matches.length !== 1) throw new Error(`Expected exactly one ${name} release asset, found ${matches.length}`);
if (!Number.isSafeInteger(matches[0].size) || matches[0].size <= 0) {
throw new Error(`Release asset ${name} has invalid size ${matches[0].size}`);
}
}
console.log(`Verified ${expected.length} Linux release assets and both architecture manifests.`);
})().catch((error) => {
console.error(error);
process.exit(1);
});
NODE
- name: Publish release
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
with: