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
@@ -14,14 +14,29 @@ import {
|
||||
setDesktopKeepAwake,
|
||||
setDesktopLaunchAtLogin,
|
||||
setDesktopMinimizeToTray,
|
||||
usesFramelessElectronChrome,
|
||||
type DesktopWindowControlsPosition,
|
||||
} from '@/lib/desktop';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { updateDesktopSettings } from '@/lib/persistence';
|
||||
import { runtimeFetch } from '@/lib/runtime-fetch';
|
||||
import { getRuntimeApiBaseUrl } from '@/lib/runtime-switch';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const WINDOW_CONTROLS_POSITION_OPTIONS: Array<{ id: DesktopWindowControlsPosition; labelKey: string }> = [
|
||||
{ id: 'auto', labelKey: 'settings.openchamber.desktopNetwork.option.windowControlsAuto' },
|
||||
{ id: 'left', labelKey: 'settings.openchamber.desktopNetwork.option.windowControlsLeft' },
|
||||
{ id: 'right', labelKey: 'settings.openchamber.desktopNetwork.option.windowControlsRight' },
|
||||
];
|
||||
|
||||
export const DesktopNetworkSettings: React.FC = () => {
|
||||
const { t } = useI18n();
|
||||
const tUnsafe = React.useCallback((key: string) => t(key as Parameters<typeof t>[0]), [t]);
|
||||
const isLocalDesktop = isDesktopShell() && isDesktopLocalOriginActive();
|
||||
const showWindowControlsPosition = usesFramelessElectronChrome();
|
||||
const desktopWindowControlsPosition = useUIStore((state) => state.desktopWindowControlsPosition);
|
||||
const setDesktopWindowControlsPosition = useUIStore((state) => state.setDesktopWindowControlsPosition);
|
||||
const [savedValue, setSavedValue] = React.useState(false);
|
||||
const [draftValue, setDraftValue] = React.useState(false);
|
||||
const [savedPassword, setSavedPassword] = React.useState('');
|
||||
@@ -211,6 +226,11 @@ export const DesktopNetworkSettings: React.FC = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleWindowControlsPositionChange = React.useCallback((value: DesktopWindowControlsPosition) => {
|
||||
setDesktopWindowControlsPosition(value);
|
||||
void updateDesktopSettings({ desktopWindowControlsPosition: value });
|
||||
}, [setDesktopWindowControlsPosition]);
|
||||
|
||||
const handleLaunchAtLoginToggle = React.useCallback(async () => {
|
||||
if (!launchAtLoginSupported || isSavingLaunchAtLogin) {
|
||||
return;
|
||||
@@ -324,12 +344,54 @@ export const DesktopNetworkSettings: React.FC = () => {
|
||||
}
|
||||
}, [draftPassword, draftValue, isDirty, t]);
|
||||
|
||||
if (!isLocalDesktop) {
|
||||
if (!isLocalDesktop && !showWindowControlsPosition) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-8">
|
||||
{showWindowControlsPosition ? (
|
||||
<>
|
||||
<div className="mb-1 px-1">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">{t('settings.openchamber.desktopNetwork.field.windowControlsPosition')}</h3>
|
||||
</div>
|
||||
<section className="space-y-2 px-2 pb-2 pt-0">
|
||||
<div data-settings-item="sessions.desktop-window-controls-position" className="space-y-1 py-1.5">
|
||||
<div className="typography-micro text-muted-foreground/70">
|
||||
{t('settings.openchamber.desktopNetwork.field.windowControlsPositionDescription')}
|
||||
</div>
|
||||
<div
|
||||
className="mt-1 flex flex-wrap items-center gap-1"
|
||||
role="group"
|
||||
aria-label={t('settings.openchamber.desktopNetwork.field.windowControlsPositionAria')}
|
||||
>
|
||||
{WINDOW_CONTROLS_POSITION_OPTIONS.map((option) => {
|
||||
const selected = desktopWindowControlsPosition === option.id;
|
||||
return (
|
||||
<Button
|
||||
key={option.id}
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
className={cn(
|
||||
'!font-normal',
|
||||
selected ? 'border-[var(--primary-base)] text-[var(--primary-base)] bg-[var(--primary-base)]/10' : 'text-foreground',
|
||||
)}
|
||||
aria-pressed={selected}
|
||||
onClick={() => handleWindowControlsPositionChange(option.id)}
|
||||
>
|
||||
{tUnsafe(option.labelKey)}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{!isLocalDesktop ? null : (
|
||||
<>
|
||||
<div className="mb-1 px-1">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">{t('settings.openchamber.desktopNetwork.title')}</h3>
|
||||
</div>
|
||||
@@ -502,6 +564,8 @@ export const DesktopNetworkSettings: React.FC = () => {
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ import { DesktopNetworkSettings } from './DesktopNetworkSettings';
|
||||
import { KeyboardShortcutsSettings } from './KeyboardShortcutsSettings';
|
||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { isDesktopLocalOriginActive, isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
|
||||
import { isDesktopLocalOriginActive, isDesktopShell, isVSCodeRuntime, isWebRuntime, usesFramelessElectronChrome } from '@/lib/desktop';
|
||||
import { subscribeRuntimeEndpointChanged } from '@/lib/runtime-switch';
|
||||
import type { OpenChamberSection } from './types';
|
||||
|
||||
@@ -39,7 +39,7 @@ export const OpenChamberPage: React.FC<OpenChamberPageProps> = ({ section }) =>
|
||||
const showAbout = isMobile && isWebRuntime();
|
||||
const isVSCode = isVSCodeRuntime();
|
||||
void runtimeEndpointEpoch;
|
||||
const showDesktopNetworkSettings = isDesktopShell() && isDesktopLocalOriginActive();
|
||||
const showDesktopNetworkSettings = isDesktopShell() && (isDesktopLocalOriginActive() || usesFramelessElectronChrome());
|
||||
|
||||
// If no section specified, show all (mobile/legacy behavior)
|
||||
if (!section) {
|
||||
@@ -153,7 +153,7 @@ const SessionsSectionContent: React.FC = () => {
|
||||
const isVSCode = isVSCodeRuntime();
|
||||
const runtimeEndpointEpoch = useRuntimeEndpointEpoch();
|
||||
void runtimeEndpointEpoch;
|
||||
const showDesktopNetworkSettings = isDesktopShell() && isDesktopLocalOriginActive();
|
||||
const showDesktopNetworkSettings = isDesktopShell() && (isDesktopLocalOriginActive() || usesFramelessElectronChrome());
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<DefaultsSettings />
|
||||
|
||||
Reference in New Issue
Block a user