fix(desktop updates): Linux package installs update via their package manager
CI / Build & Quality Checks (pull_request) Failing after 1m40s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Playwright smoke (e2e) (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 6s
CI / Build & Quality Checks (pull_request) Failing after 1m40s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Playwright smoke (e2e) (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 6s
Reported on CachyOS: "Check for Updates → The update downloaded but couldn't be installed … Permission denied (os error 13) at path /usr/bin/tauri_current_app…". The app was installed from the Arch package; Tauri's Linux updater can only replace an AppImage. With cinny-desktop's new update_install_kind command: - pacman / deb installs: the toast says the update is available and opens Settings → General → App Updates, which shows the package-manager command (`sudo pacman -U …pkg.tar.zst`, or the .deb + `sudo apt install`) with Copy command and Download package — no Install & Restart that can't work. "Copied" only when the clipboard write actually succeeded. - other distros: a link to the downloads page. - Windows, AppImage, and desktop builds without the command: unchanged in-app update. - a native "package-managed" refusal shows the same help. Tests: unit (kinds, commands, refusal detection); in the real client with a simulated desktop bridge: pacman → toast + Settings command/buttons, install never attempted; older desktop → in-app flow as before. Unit 1321, Playwright 26 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
747400ea25
commit
27d659118f
@@ -112,7 +112,13 @@ import {
|
||||
import { chromeTranslationEngine } from '../../../utils/translation/chromeEngine';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { UpdateProgress, useTauriUpdater } from '../../../hooks/useTauriUpdater';
|
||||
import { describeUpdateError, manualDownloadUrl } from '../../../utils/updateErrors';
|
||||
import {
|
||||
describeUpdateError,
|
||||
isPackageManagedError,
|
||||
manualDownloadUrl,
|
||||
packageUpdateHelp,
|
||||
} from '../../../utils/updateErrors';
|
||||
import { copyToClipboard } from '../../../utils/dom';
|
||||
import { isTauri as isTauriEnv, invokeTauri, tauriInvoke } from '../../../hooks/useTauri';
|
||||
import { useKeychainMirrorStatus } from '../../../state/keychainMirror';
|
||||
import { isSafeGlobalToggleKey } from '../../../hooks/useCallHotkeys';
|
||||
@@ -2794,12 +2800,76 @@ function updateProgressText(progress: UpdateProgress | undefined): string {
|
||||
return `Downloading update… ${pct}% (${formatMb(downloaded)} of ${formatMb(total)})${attempt}`;
|
||||
}
|
||||
|
||||
function AppUpdates() {
|
||||
const { isTauri, status, check, install } = useTauriUpdater();
|
||||
if (!isTauri) return null;
|
||||
/**
|
||||
* A Linux package install is updated with its package manager (the in-app
|
||||
* updater can't write to /usr/bin): the command, Copy and a download link.
|
||||
*/
|
||||
function PackageUpdate({ help }: { help: NonNullable<ReturnType<typeof packageUpdateHelp>> }) {
|
||||
const [copied, setCopied] = useState<'no' | 'yes' | 'failed'>('no');
|
||||
const copy = () => {
|
||||
const text = help.command ?? '';
|
||||
if (!navigator.clipboard) {
|
||||
copyToClipboard(text);
|
||||
setCopied('yes');
|
||||
return;
|
||||
}
|
||||
navigator.clipboard.writeText(text).then(
|
||||
() => setCopied('yes'),
|
||||
() => setCopied('failed'),
|
||||
);
|
||||
};
|
||||
let copyLabel = 'Copy command';
|
||||
if (copied === 'yes') copyLabel = 'Copied';
|
||||
else if (copied === 'failed') copyLabel = 'Copy failed: select it above';
|
||||
return (
|
||||
<Box direction="Column" gap="200">
|
||||
<Text size="T200">
|
||||
Lotus Chat was installed as a system package, so update it the same way
|
||||
{help.command ? ' (your chats and settings are kept):' : '.'}
|
||||
</Text>
|
||||
{help.command && (
|
||||
<Text
|
||||
size="T200"
|
||||
style={{ fontFamily: 'monospace', wordBreak: 'break-all', userSelect: 'all' }}
|
||||
>
|
||||
{help.command}
|
||||
</Text>
|
||||
)}
|
||||
<Box gap="200" wrap="Wrap">
|
||||
{help.command && (
|
||||
<Button size="300" radii="300" variant="Secondary" onClick={copy}>
|
||||
<Text size="B300">{copyLabel}</Text>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="300"
|
||||
radii="300"
|
||||
variant="Secondary"
|
||||
fill="None"
|
||||
outlined
|
||||
onClick={() => window.open(help.url, '_blank')}
|
||||
>
|
||||
<Text size="B300">{help.download}</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const description =
|
||||
status.state === 'checking'
|
||||
function AppUpdates() {
|
||||
const { isTauri, status, check, install, installKind } = useTauriUpdater();
|
||||
if (!isTauri) return null;
|
||||
const packageHelp = packageUpdateHelp(installKind);
|
||||
const packageUpdate =
|
||||
!!packageHelp &&
|
||||
(status.state === 'available' ||
|
||||
(status.state === 'error' && isPackageManagedError(status.message)));
|
||||
|
||||
const description = packageUpdate
|
||||
? status.state === 'available'
|
||||
? `Update available: v${status.version}`
|
||||
: 'An update is available.'
|
||||
: status.state === 'checking'
|
||||
? 'Checking for updates...'
|
||||
: status.state === 'up-to-date'
|
||||
? 'Lotus Chat is up to date.'
|
||||
@@ -2816,43 +2886,43 @@ function AppUpdates() {
|
||||
else check();
|
||||
};
|
||||
|
||||
const after =
|
||||
status.state === 'available' ? (
|
||||
<Button size="300" radii="300" onClick={() => install()}>
|
||||
<Text size="B300">Install & Restart</Text>
|
||||
const after = packageUpdate ? undefined : status.state === 'available' ? (
|
||||
<Button size="300" radii="300" onClick={() => install()}>
|
||||
<Text size="B300">Install & Restart</Text>
|
||||
</Button>
|
||||
) : status.state === 'checking' || status.state === 'installing' ? (
|
||||
<Spinner variant="Secondary" size="200" />
|
||||
) : status.state === 'error' ? (
|
||||
<Box gap="200" wrap="Wrap" justifyContent="End">
|
||||
<Button size="300" radii="300" variant="Secondary" onClick={retry}>
|
||||
<Text size="B300">Try again</Text>
|
||||
</Button>
|
||||
) : status.state === 'checking' || status.state === 'installing' ? (
|
||||
<Spinner variant="Secondary" size="200" />
|
||||
) : status.state === 'error' ? (
|
||||
<Box gap="200" wrap="Wrap" justifyContent="End">
|
||||
<Button size="300" radii="300" variant="Secondary" onClick={retry}>
|
||||
<Text size="B300">Try again</Text>
|
||||
{status.phase !== 'check' && (
|
||||
<Button
|
||||
size="300"
|
||||
radii="300"
|
||||
variant="Secondary"
|
||||
fill="None"
|
||||
outlined
|
||||
onClick={() => window.open(manualDownloadUrl(navigator.userAgent), '_blank')}
|
||||
>
|
||||
<Text size="B300">Download installer</Text>
|
||||
</Button>
|
||||
{status.phase !== 'check' && (
|
||||
<Button
|
||||
size="300"
|
||||
radii="300"
|
||||
variant="Secondary"
|
||||
fill="None"
|
||||
outlined
|
||||
onClick={() => window.open(manualDownloadUrl(navigator.userAgent), '_blank')}
|
||||
>
|
||||
<Text size="B300">Download installer</Text>
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
<Button size="300" radii="300" variant="Secondary" onClick={check}>
|
||||
<Text size="B300">Check</Text>
|
||||
</Button>
|
||||
);
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
<Button size="300" radii="300" variant="Secondary" onClick={check}>
|
||||
<Text size="B300">Check</Text>
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">App Updates</Text>
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile title="Check for Updates" description={description} after={after} />
|
||||
{status.state === 'error' && (
|
||||
{packageUpdate && packageHelp && <PackageUpdate help={packageHelp} />}
|
||||
{status.state === 'error' && !packageUpdate && (
|
||||
<Text size="T200" priority="300" style={{ wordBreak: 'break-word' }}>
|
||||
Details: {status.message}
|
||||
</Text>
|
||||
|
||||
Reference in New Issue
Block a user