diff --git a/packages/ui/src/apps/MobileApp.tsx b/packages/ui/src/apps/MobileApp.tsx
index bc7f9a2c..3d5432fd 100644
--- a/packages/ui/src/apps/MobileApp.tsx
+++ b/packages/ui/src/apps/MobileApp.tsx
@@ -6,6 +6,7 @@ import { McpIcon } from '@/components/icons/McpIcon';
import { McpDropdownContent } from '@/components/mcp/McpDropdown';
import { AboutSettings } from '@/components/sections/openchamber/AboutSettings';
import { OpenCodeUpdateToast } from '@/components/update/OpenCodeUpdateToast';
+import { MobileAppUpdateToast } from '@/components/update/MobileAppUpdateToast';
import { ConfigUpdateOverlay } from '@/components/ui/ConfigUpdateOverlay';
import { Button } from '@/components/ui/button';
import { OpenChamberLogo } from '@/components/ui/OpenChamberLogo';
@@ -2485,6 +2486,7 @@ export function MobileApp({ apis }: MobileAppProps) {
+
{
switchRuntimeEndpoint({ apiBaseUrl: '', clientToken: null, runtimeKey: 'mobile-disconnected' });
setConnectionEpoch((value) => value + 1);
diff --git a/packages/ui/src/components/update/MobileAppUpdateToast.tsx b/packages/ui/src/components/update/MobileAppUpdateToast.tsx
new file mode 100644
index 00000000..24983f31
--- /dev/null
+++ b/packages/ui/src/components/update/MobileAppUpdateToast.tsx
@@ -0,0 +1,60 @@
+import * as React from 'react';
+
+import { Icon } from '@/components/icon/Icon';
+import { toast } from '@/components/ui/toast';
+import { useI18n } from '@/lib/i18n';
+import { getClientPlatform } from '@/lib/platform';
+import { openExternalUrl } from '@/lib/url';
+import { useUpdateStore } from '@/stores/useUpdateStore';
+import { getDeferredSafeStorage } from '@/stores/utils/safeStorage';
+
+const TOAST_ID = 'mobile-app-update-available';
+const DISMISSED_VERSION_KEY = 'mobile-app-update-toast-dismissed-version';
+
+export const MobileAppUpdateToast: React.FC = () => {
+ const { t } = useI18n();
+ const available = useUpdateStore((state) => state.available);
+ const runtimeType = useUpdateStore((state) => state.runtimeType);
+ const version = useUpdateStore((state) => state.info?.version);
+ const downloadUrl = useUpdateStore((state) => state.info?.downloadUrl);
+ const seenVersionsRef = React.useRef(new Set());
+
+ React.useEffect(() => {
+ if (getClientPlatform() !== 'android' || runtimeType !== 'mobile' || !available || !version || !downloadUrl) {
+ toast.dismiss(TOAST_ID);
+ return;
+ }
+
+ if (getDeferredSafeStorage().getItem(DISMISSED_VERSION_KEY) === version) {
+ return;
+ }
+
+ if (seenVersionsRef.current.has(version)) {
+ return;
+ }
+
+ seenVersionsRef.current.add(version);
+
+ toast.info(t('mobileUpdate.toast.available.title'), {
+ id: TOAST_ID,
+ description: t('mobileUpdate.toast.available.description', { version }),
+ duration: Infinity,
+ icon: ,
+ action: {
+ label: t('mobileUpdate.toast.actions.download'),
+ onClick: () => {
+ void openExternalUrl(downloadUrl);
+ },
+ },
+ cancel: {
+ label: t('mobileUpdate.toast.actions.dismiss'),
+ onClick: () => {
+ getDeferredSafeStorage().setItem(DISMISSED_VERSION_KEY, version);
+ toast.dismiss(TOAST_ID);
+ },
+ },
+ });
+ }, [available, downloadUrl, runtimeType, t, version]);
+
+ return null;
+};
diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts
index 3994b998..b2c4d9a0 100644
--- a/packages/ui/src/lib/i18n/messages/en.ts
+++ b/packages/ui/src/lib/i18n/messages/en.ts
@@ -2598,6 +2598,10 @@ export const dict = {
'updateDialog.status.updating': 'Updating...',
'updateDialog.error.updateFailed': 'Update failed',
'updateDialog.error.takingLonger': 'Update is taking longer than expected. Wait a bit and refresh, or run: openchamber update',
+ 'mobileUpdate.toast.available.title': 'OpenChamber update available',
+ 'mobileUpdate.toast.available.description': 'Version {version} is ready for Android.',
+ 'mobileUpdate.toast.actions.download': 'Download',
+ 'mobileUpdate.toast.actions.dismiss': 'Dismiss',
'opencodeUpdate.toast.available.title': 'OpenCode update',
'opencodeUpdate.toast.available.description': 'Version {version} available.',
'opencodeUpdate.toast.actions.update': 'Update',
diff --git a/packages/ui/src/lib/i18n/messages/es.ts b/packages/ui/src/lib/i18n/messages/es.ts
index 1a070aa1..4c1a15d5 100644
--- a/packages/ui/src/lib/i18n/messages/es.ts
+++ b/packages/ui/src/lib/i18n/messages/es.ts
@@ -2564,6 +2564,10 @@ export const dict: Record = {
"updateDialog.status.updating": "Actualizando...",
"updateDialog.error.updateFailed": "No se pudo actualizar",
"updateDialog.error.takingLonger": "La actualización está tardando más de lo esperado. Espera un poco y refresca, o ejecuta: openchamber update",
+ "mobileUpdate.toast.available.title": "Actualización de OpenChamber disponible",
+ "mobileUpdate.toast.available.description": "La versión {version} está lista para Android.",
+ "mobileUpdate.toast.actions.download": "Descargar",
+ "mobileUpdate.toast.actions.dismiss": "Descartar",
"opencodeUpdate.toast.available.title": "Actualización de OpenCode",
"opencodeUpdate.toast.available.description": "Versión {version} disponible.",
"opencodeUpdate.toast.actions.update": "Actualizar",
diff --git a/packages/ui/src/lib/i18n/messages/fr.ts b/packages/ui/src/lib/i18n/messages/fr.ts
index e237e79d..9e247d6d 100644
--- a/packages/ui/src/lib/i18n/messages/fr.ts
+++ b/packages/ui/src/lib/i18n/messages/fr.ts
@@ -2369,6 +2369,10 @@ export const dict = {
'updateDialog.status.updating': 'Mise à jour...',
'updateDialog.error.updateFailed': 'La mise à jour a échoué',
'updateDialog.error.takingLonger': 'La mise à jour prend plus de temps que prévu. Attendez un peu et actualisez, ou exécutez : openchamber update',
+ 'mobileUpdate.toast.available.title': 'Mise à jour OpenChamber disponible',
+ 'mobileUpdate.toast.available.description': 'La version {version} est prête pour Android.',
+ 'mobileUpdate.toast.actions.download': 'Télécharger',
+ 'mobileUpdate.toast.actions.dismiss': 'Ignorer',
'opencodeUpdate.toast.available.title': 'Mise à jour OpenCode',
'opencodeUpdate.toast.available.description': 'Version {version} disponible.',
'opencodeUpdate.toast.actions.update': 'Mise à jour',
diff --git a/packages/ui/src/lib/i18n/messages/ja.ts b/packages/ui/src/lib/i18n/messages/ja.ts
index 792c42a5..78c2f82f 100644
--- a/packages/ui/src/lib/i18n/messages/ja.ts
+++ b/packages/ui/src/lib/i18n/messages/ja.ts
@@ -2594,6 +2594,10 @@ export const dict: Record = {
'updateDialog.status.updating': '更新中...',
'updateDialog.error.updateFailed': '更新に失敗しました',
'updateDialog.error.takingLonger': '更新に予想以上に時間がかかっています。しばらく待ってから更新するか、次を実行: openchamber update',
+ 'mobileUpdate.toast.available.title': 'OpenChamberの更新があります',
+ 'mobileUpdate.toast.available.description': 'バージョン{version}をAndroidで利用できます。',
+ 'mobileUpdate.toast.actions.download': 'ダウンロード',
+ 'mobileUpdate.toast.actions.dismiss': '閉じる',
'opencodeUpdate.toast.available.title': 'OpenCode更新',
'opencodeUpdate.toast.available.description': 'バージョン{version}が利用可能です。',
'opencodeUpdate.toast.actions.update': '更新',
diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts
index 5cac4838..982a49b6 100644
--- a/packages/ui/src/lib/i18n/messages/ko.ts
+++ b/packages/ui/src/lib/i18n/messages/ko.ts
@@ -2598,6 +2598,10 @@ export const dict: Record = {
'updateDialog.status.updating': '업데이트 중…',
'updateDialog.error.updateFailed': '업데이트 실패',
'updateDialog.error.takingLonger': '업데이트가 예상보다 오래 걸립니다. 잠시 기다린 뒤 새로고침하거나 `openchamber update`를 실행하세요.',
+ 'mobileUpdate.toast.available.title': 'OpenChamber 업데이트 사용 가능',
+ 'mobileUpdate.toast.available.description': 'Android용 버전 {version}이 준비되었습니다.',
+ 'mobileUpdate.toast.actions.download': '다운로드',
+ 'mobileUpdate.toast.actions.dismiss': '닫기',
'opencodeUpdate.toast.available.title': 'OpenCode 업데이트',
'opencodeUpdate.toast.available.description': '버전 {version} 사용 가능.',
'opencodeUpdate.toast.actions.update': '업데이트',
diff --git a/packages/ui/src/lib/i18n/messages/pl.ts b/packages/ui/src/lib/i18n/messages/pl.ts
index 0f258195..96e66b5f 100644
--- a/packages/ui/src/lib/i18n/messages/pl.ts
+++ b/packages/ui/src/lib/i18n/messages/pl.ts
@@ -2567,6 +2567,10 @@ export const dict: Record = {
'updateDialog.actions.updateNow': 'Aktualizuj teraz',
'updateDialog.error.takingLonger': 'Aktualizacja trwa dłużej niż oczekiwano. Poczekaj chwilę i odśwież albo uruchom: openchamber update',
'updateDialog.error.updateFailed': 'Aktualizacja nie powiodła się',
+ 'mobileUpdate.toast.available.title': 'Dostępna aktualizacja OpenChamber',
+ 'mobileUpdate.toast.available.description': 'Wersja {version} jest gotowa dla Androida.',
+ 'mobileUpdate.toast.actions.download': 'Pobierz',
+ 'mobileUpdate.toast.actions.dismiss': 'Odrzuć',
'updateDialog.fallback.updateViaTerminal': 'Lub zaktualizuj przez terminal:',
'updateDialog.header.updateAvailable': 'Dostępna aktualizacja',
'updateDialog.header.updating': 'Aktualizowanie OpenChamber...',
diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts
index bbff90b3..34ed5122 100644
--- a/packages/ui/src/lib/i18n/messages/pt-BR.ts
+++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts
@@ -2564,6 +2564,10 @@ export const dict: Record = {
"updateDialog.status.updating": "Atualizando...",
"updateDialog.error.updateFailed": "Não foi possível atualizar",
"updateDialog.error.takingLonger": "A atualização está demorando mais do que o esperado. Aguarde um pouco e atualize, ou execute: openchamber update",
+ "mobileUpdate.toast.available.title": "Atualização do OpenChamber disponível",
+ "mobileUpdate.toast.available.description": "A versão {version} está pronta para Android.",
+ "mobileUpdate.toast.actions.download": "Baixar",
+ "mobileUpdate.toast.actions.dismiss": "Dispensar",
"opencodeUpdate.toast.available.title": "Atualização do OpenCode",
"opencodeUpdate.toast.available.description": "Versão {version} disponível.",
"opencodeUpdate.toast.actions.update": "Atualizar",
diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts
index c063aea4..3575f0ba 100644
--- a/packages/ui/src/lib/i18n/messages/uk.ts
+++ b/packages/ui/src/lib/i18n/messages/uk.ts
@@ -2564,6 +2564,10 @@ export const dict: Record = {
"updateDialog.status.updating": "Оновлення...",
"updateDialog.error.updateFailed": "Помилка оновлення",
"updateDialog.error.takingLonger": "Оновлення триває довше, ніж очікувалося. Зачекайте трохи та оновіть або запустіть: openchamber update",
+ "mobileUpdate.toast.available.title": "Доступне оновлення OpenChamber",
+ "mobileUpdate.toast.available.description": "Версія {version} готова для Android.",
+ "mobileUpdate.toast.actions.download": "Завантажити",
+ "mobileUpdate.toast.actions.dismiss": "Сховати",
"opencodeUpdate.toast.available.title": "Оновлення OpenCode",
"opencodeUpdate.toast.available.description": "Версія {version} доступна.",
"opencodeUpdate.toast.actions.update": "Оновити",
diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts
index ce7ea749..12908732 100644
--- a/packages/ui/src/lib/i18n/messages/zh-CN.ts
+++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts
@@ -2564,6 +2564,10 @@ export const dict: Record = {
'updateDialog.status.updating': '更新中...',
'updateDialog.error.updateFailed': '更新失败',
'updateDialog.error.takingLonger': '更新耗时超出预期。请稍等后刷新,或运行:openchamber update',
+ 'mobileUpdate.toast.available.title': 'OpenChamber 更新可用',
+ 'mobileUpdate.toast.available.description': '版本 {version} 已可用于 Android。',
+ 'mobileUpdate.toast.actions.download': '下载',
+ 'mobileUpdate.toast.actions.dismiss': '忽略',
'opencodeUpdate.toast.available.title': 'OpenCode 更新',
'opencodeUpdate.toast.available.description': '版本 {version} 可用。',
'opencodeUpdate.toast.actions.update': '更新',
diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.ts b/packages/ui/src/lib/i18n/messages/zh-TW.ts
index 926a651c..c241ff23 100644
--- a/packages/ui/src/lib/i18n/messages/zh-TW.ts
+++ b/packages/ui/src/lib/i18n/messages/zh-TW.ts
@@ -2561,6 +2561,10 @@ export const dict: Record = {
'updateDialog.status.updating': '更新中...',
'updateDialog.error.updateFailed': '更新失敗',
'updateDialog.error.takingLonger': '更新耗時超出預期。請稍等後重新整理,或執行:openchamber update',
+ 'mobileUpdate.toast.available.title': 'OpenChamber 更新可用',
+ 'mobileUpdate.toast.available.description': '版本 {version} 已可用於 Android。',
+ 'mobileUpdate.toast.actions.download': '下載',
+ 'mobileUpdate.toast.actions.dismiss': '忽略',
'opencodeUpdate.toast.available.title': 'OpenCode 更新',
'opencodeUpdate.toast.available.description': '版本 {version} 可安裝。',
'opencodeUpdate.toast.actions.update': '更新',