diff --git a/mobile/rpc-foundation/goldens/session-tab-close-host-unavailable-tells-user.json b/mobile/rpc-foundation/goldens/session-tab-close-host-unavailable-tells-user.json new file mode 100644 index 00000000000..19a41ad7993 --- /dev/null +++ b/mobile/rpc-foundation/goldens/session-tab-close-host-unavailable-tells-user.json @@ -0,0 +1,101 @@ +{ + "goldenFormatVersion": 6, + "operation": "session.tab-close", + "family": "session.tab-close", + "namedDeltas": [], + "values": { + "3e15def58214": { + "activeHandle": "terminal-1", + "sessionTabs": [ + { + "id": "tab-1", + "isActive": true, + "terminal": "terminal-1", + "title": "Terminal", + "type": "terminal" + } + ], + "terminals": [ + { + "handle": "terminal-1", + "isActive": true, + "title": "Terminal" + } + ] + }, + "50a46af5cf5c": { + "name": "session.tabs.close#1", + "ordinal": 2, + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"session.tabs.close\",\"params\":{\"worktree\":\"id:workspace-1\",\"tabId\":\"tab-1\",\"reason\":\"user\"}}" + }, + "b8970b98f1e3": { + "name": "toast", + "ordinal": 3, + "value": { + "message": "Couldn't close the tab. Orca on your computer is still loading. Try again in a moment." + } + }, + "d1730750ac1c": { + "name": "session.tabs.close#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "session.tabs.close" + }, + { + "name": "params", + "value": { + "reason": "user", + "tabId": "tab-1", + "worktree": "id:workspace-1" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "runtime_unavailable", + "message": "runtime_unavailable" + }, + "id": "frame-1", + "ok": false + } + } + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } + } + }, + "recording": { + "scenario": "session-tab-close-host-unavailable-tells-user", + "checkpoints": [ + { + "id": "kept", + "observation": { + "sender": ["d1730750ac1c"], + "payloads": ["50a46af5cf5c"], + "settlements": { + "close-tab": "eb79a9b3682a" + }, + "state": "3e15def58214", + "effects": ["b8970b98f1e3"] + } + } + ] + } +} diff --git a/mobile/rpc-foundation/goldens/session-tab-close-refused-keeps-tab.json b/mobile/rpc-foundation/goldens/session-tab-close-refused-keeps-tab.json index 3163e1f5b00..5f9eee6af52 100644 --- a/mobile/rpc-foundation/goldens/session-tab-close-refused-keeps-tab.json +++ b/mobile/rpc-foundation/goldens/session-tab-close-refused-keeps-tab.json @@ -4,6 +4,13 @@ "family": "session.tab-close", "namedDeltas": [], "values": { + "26d1891758e0": { + "name": "toast", + "ordinal": 3, + "value": { + "message": "Couldn't close the tab. Try again." + } + }, "3e15def58214": { "activeHandle": "terminal-1", "sessionTabs": [ @@ -84,7 +91,7 @@ "close-terminal": "eb79a9b3682a" }, "state": "3e15def58214", - "effects": [] + "effects": ["26d1891758e0"] } } ] diff --git a/mobile/rpc-foundation/pilot-scenarios.json b/mobile/rpc-foundation/pilot-scenarios.json index 2905f08f043..46f97142576 100644 --- a/mobile/rpc-foundation/pilot-scenarios.json +++ b/mobile/rpc-foundation/pilot-scenarios.json @@ -19386,6 +19386,37 @@ } ] }, + { + "id": "session-tab-close-host-unavailable-tells-user", + "operation": "session.tab-close", + "family": "session.tab-close", + "sites": ["mobile/src/session/tabs/use-mobile-session-close-actions.ts"], + "schedules": [], + "steps": [ + { + "action": "close-tab", + "id": "close-tab" + }, + { + "complete": "session.tabs.close#1", + "params": { + "reason": "user", + "tabId": "tab-1", + "worktree": "id:workspace-1" + }, + "reply": { + "ok": false, + "error": { + "code": "runtime_unavailable", + "message": "runtime_unavailable" + } + } + }, + { + "checkpoint": "kept" + } + ] + }, { "id": "review-mark-reviewed-persists", "operation": "session.diff-review-actions", diff --git a/mobile/src/i18n/locales/de.json b/mobile/src/i18n/locales/de.json index 3ec4346c8bb..3cf6847e407 100644 --- a/mobile/src/i18n/locales/de.json +++ b/mobile/src/i18n/locales/de.json @@ -2498,6 +2498,10 @@ "useAttachmentInputLeaseGate": { "reconnecting": "Anhängen fehlgeschlagen (erneuter Verbindungsaufbau)" }, + "useCloseActions": { + "closeFailed": "Der Tab konnte nicht geschlossen werden. Versuche es erneut.", + "closeFailedDesktopNotReady": "Der Tab konnte nicht geschlossen werden. Orca wird auf deinem Computer noch geladen. Versuche es gleich noch einmal." + }, "useContentCreateActions": { "browserCommandFailed": "Browser-Befehl fehlgeschlagen", "browserCreateFailed": "Browser konnte nicht erstellt werden", diff --git a/mobile/src/i18n/locales/en.json b/mobile/src/i18n/locales/en.json index 88ac126f646..46dc3c90822 100644 --- a/mobile/src/i18n/locales/en.json +++ b/mobile/src/i18n/locales/en.json @@ -2545,6 +2545,10 @@ "copyFailed": "Couldn't copy", "selectionCleared": "Selection cleared (scrolled out of buffer)" }, + "useCloseActions": { + "closeFailed": "Couldn't close the tab. Try again.", + "closeFailedDesktopNotReady": "Couldn't close the tab. Orca on your computer is still loading. Try again in a moment." + }, "useContentCreateActions": { "noteCreateFailed": "Failed to create markdown note", "untitledNoteCreateFailed": "Unable to create untitled markdown note", diff --git a/mobile/src/i18n/locales/es.json b/mobile/src/i18n/locales/es.json index 427c56e8627..fa5c23917d0 100644 --- a/mobile/src/i18n/locales/es.json +++ b/mobile/src/i18n/locales/es.json @@ -2568,6 +2568,10 @@ "useAttachmentInputLeaseGate": { "reconnecting": "No se pudo adjuntar (reconectando)" }, + "useCloseActions": { + "closeFailed": "No se pudo cerrar la pestaña. Inténtalo de nuevo.", + "closeFailedDesktopNotReady": "No se pudo cerrar la pestaña. Orca todavía se está cargando en tu ordenador. Vuelve a intentarlo en un momento." + }, "useContentCreateActions": { "browserCommandFailed": "No se pudo ejecutar el comando del navegador", "browserCreateFailed": "No se pudo crear el navegador", diff --git a/mobile/src/i18n/locales/fr.json b/mobile/src/i18n/locales/fr.json index 30ecaf4b389..55fb33e0a87 100644 --- a/mobile/src/i18n/locales/fr.json +++ b/mobile/src/i18n/locales/fr.json @@ -2568,6 +2568,10 @@ "useAttachmentInputLeaseGate": { "reconnecting": "Échec de l'ajout (reconnexion en cours)" }, + "useCloseActions": { + "closeFailed": "Impossible de fermer l’onglet. Réessayez.", + "closeFailedDesktopNotReady": "Impossible de fermer l’onglet. Orca est encore en cours de chargement sur votre ordinateur. Réessayez dans un instant." + }, "useContentCreateActions": { "browserCommandFailed": "Échec de la commande du navigateur", "browserCreateFailed": "Échec de la création du navigateur", diff --git a/mobile/src/i18n/locales/ja.json b/mobile/src/i18n/locales/ja.json index 30899d69590..43702cfb068 100644 --- a/mobile/src/i18n/locales/ja.json +++ b/mobile/src/i18n/locales/ja.json @@ -2494,6 +2494,10 @@ "useAttachmentInputLeaseGate": { "reconnecting": "添付に失敗しました(再接続中)。" }, + "useCloseActions": { + "closeFailed": "タブを閉じられませんでした。もう一度お試しください。", + "closeFailedDesktopNotReady": "タブを閉じられませんでした。コンピューターの Orca はまだ読み込み中です。少ししてからもう一度お試しください。" + }, "useContentCreateActions": { "browserCommandFailed": "ブラウザコマンドの実行に失敗しました。", "browserCreateFailed": "ブラウザの作成に失敗しました。", diff --git a/mobile/src/i18n/locales/ko.json b/mobile/src/i18n/locales/ko.json index ba2a6f6da02..508bae80ba0 100644 --- a/mobile/src/i18n/locales/ko.json +++ b/mobile/src/i18n/locales/ko.json @@ -2494,6 +2494,10 @@ "useAttachmentInputLeaseGate": { "reconnecting": "첨부 실패 (다시 연결 중)" }, + "useCloseActions": { + "closeFailed": "탭을 닫지 못했습니다. 다시 시도하세요.", + "closeFailedDesktopNotReady": "탭을 닫지 못했습니다. 컴퓨터에서 Orca를 아직 불러오는 중입니다. 잠시 후 다시 시도하세요." + }, "useContentCreateActions": { "browserCommandFailed": "브라우저 명령 실행에 실패했습니다.", "browserCreateFailed": "브라우저를 만들지 못했습니다.", diff --git a/mobile/src/i18n/locales/pt.json b/mobile/src/i18n/locales/pt.json index 90edff716b4..faf7f8cc7ea 100644 --- a/mobile/src/i18n/locales/pt.json +++ b/mobile/src/i18n/locales/pt.json @@ -2941,6 +2941,10 @@ "useAttachmentInputLeaseGate": { "reconnecting": "Falha ao anexar (reconectando)" }, + "useCloseActions": { + "closeFailed": "Não foi possível fechar a aba. Tente novamente.", + "closeFailedDesktopNotReady": "Não foi possível fechar a aba. O Orca ainda está carregando no seu computador. Tente novamente em instantes." + }, "useContentCreateActions": { "browserCommandFailed": "Falha no comando do navegador", "browserCreateFailed": "Falha ao criar o navegador", diff --git a/mobile/src/i18n/locales/ru.json b/mobile/src/i18n/locales/ru.json index a5af5d67b93..9025fa32b48 100644 --- a/mobile/src/i18n/locales/ru.json +++ b/mobile/src/i18n/locales/ru.json @@ -2638,6 +2638,10 @@ "useAttachmentInputLeaseGate": { "reconnecting": "Не удалось прикрепить (повторное подключение)" }, + "useCloseActions": { + "closeFailed": "Не удалось закрыть вкладку. Попробуйте ещё раз.", + "closeFailedDesktopNotReady": "Не удалось закрыть вкладку. Orca на компьютере ещё загружается. Попробуйте ещё раз через минуту." + }, "useContentCreateActions": { "browserCommandFailed": "Не удалось выполнить команду браузера", "browserCreateFailed": "Не удалось создать вкладку браузера", diff --git a/mobile/src/i18n/locales/tr.json b/mobile/src/i18n/locales/tr.json index aa7c3714a3d..fd01e212a54 100644 --- a/mobile/src/i18n/locales/tr.json +++ b/mobile/src/i18n/locales/tr.json @@ -2498,6 +2498,10 @@ "useAttachmentInputLeaseGate": { "reconnecting": "Eklenemedi (yeniden bağlanılıyor)" }, + "useCloseActions": { + "closeFailed": "Sekme kapatılamadı. Tekrar deneyin.", + "closeFailedDesktopNotReady": "Sekme kapatılamadı. Orca bilgisayarınızda hâlâ yükleniyor. Birazdan tekrar deneyin." + }, "useContentCreateActions": { "browserCommandFailed": "Tarayıcı komutu çalıştırılamadı", "browserCreateFailed": "Tarayıcı oluşturulamadı", diff --git a/mobile/src/i18n/locales/zh-Hant.json b/mobile/src/i18n/locales/zh-Hant.json index 83709ea42ca..23df25ab951 100644 --- a/mobile/src/i18n/locales/zh-Hant.json +++ b/mobile/src/i18n/locales/zh-Hant.json @@ -2494,6 +2494,10 @@ "useAttachmentInputLeaseGate": { "reconnecting": "加入附件失敗(重新連線中)" }, + "useCloseActions": { + "closeFailed": "無法關閉分頁。請再試一次。", + "closeFailedDesktopNotReady": "無法關閉分頁。電腦上的 Orca 仍在載入中,請稍後再試。" + }, "useContentCreateActions": { "browserCommandFailed": "瀏覽器命令執行失敗", "browserCreateFailed": "建立瀏覽器失敗", diff --git a/mobile/src/i18n/locales/zh.json b/mobile/src/i18n/locales/zh.json index 4a85c1fa457..1788567f012 100644 --- a/mobile/src/i18n/locales/zh.json +++ b/mobile/src/i18n/locales/zh.json @@ -2500,6 +2500,10 @@ "useAttachmentInputLeaseGate": { "reconnecting": "添加附件失败(重新连接中)" }, + "useCloseActions": { + "closeFailed": "无法关闭标签页。请重试。", + "closeFailedDesktopNotReady": "无法关闭标签页。电脑上的 Orca 仍在加载,请稍后重试。" + }, "useContentCreateActions": { "browserCommandFailed": "浏览器命令执行失败", "browserCreateFailed": "创建浏览器失败", diff --git a/mobile/src/session/tabs/use-mobile-session-close-actions.ts b/mobile/src/session/tabs/use-mobile-session-close-actions.ts index 17d236822f5..ae88a77563b 100644 --- a/mobile/src/session/tabs/use-mobile-session-close-actions.ts +++ b/mobile/src/session/tabs/use-mobile-session-close-actions.ts @@ -19,6 +19,7 @@ import { type HostMutationPhase } from '../../query/host-mutation' import type { RpcClient } from '../../transport/rpc/rpc-client-contract' +import { translate } from '../../i18n/mobile-i18n' async function sendTerminalRename( client: RpcClient, @@ -64,9 +65,22 @@ export function useMobileSessionCloseActions(scope: MobileSessionContentCreateAc scheduleDelayedAction, unsubscribeTerminal, subscribeToTerminal, - fetchTerminals + fetchTerminals, + showToast } = scope const terminalRenameWrite = useHostMutation(terminalRename, { hostId, client }) + // A refused or lost close keeps the tab, so say so rather than leave the tap unanswered. + function showCloseFailed(code?: string) { + showToast( + code === 'runtime_unavailable' + ? translate( + 'session.useCloseActions.closeFailedDesktopNotReady', + "Couldn't close the tab. Orca on your computer is still loading. Try again in a moment." + ) + : translate('session.useCloseActions.closeFailed', "Couldn't close the tab. Try again."), + 2400 + ) + } // Throws on refusal so the sheet keeps the typed name and offers Retry. async function handleRenameTerminal( value: string, @@ -96,10 +110,11 @@ export function useMobileSessionCloseActions(scope: MobileSessionContentCreateAc } try { - const response = sessionTerminalClose.interpret( - await sessionTerminalClose.request(client, { terminal: target.handle }) - ) - if (response.accepted) { + const reply = await sessionTerminalClose.request(client, { terminal: target.handle }) + const response = sessionTerminalClose.interpret(reply) + if (!response.accepted) { + showCloseFailed(reply.ok ? undefined : reply.error.code) + } else { unsubscribeTerminal(target.handle) terminalRefs.current.delete(target.handle) const next = terminalsRef.current.filter((terminal) => terminal.handle !== target.handle) @@ -121,7 +136,9 @@ export function useMobileSessionCloseActions(scope: MobileSessionContentCreateAc } } } - } catch {} + } catch { + showCloseFailed() + } } async function handleCloseSessionTab(tab: MobileSessionTab) { @@ -129,16 +146,17 @@ export function useMobileSessionCloseActions(scope: MobileSessionContentCreateAc return } try { - const response = sessionTabClose.interpret( - await sessionTabClose.request(client, { - worktree: `id:${worktreeId}`, - tabId: tab.id, - // Why: a tapped tab close is explicit user intent; older hosts strip - // the unknown field and keep their legacy behavior. - reason: 'user' - }) - ) - if (response.accepted && !response.value.refused) { + const reply = await sessionTabClose.request(client, { + worktree: `id:${worktreeId}`, + tabId: tab.id, + // Why: a tapped tab close is explicit user intent; older hosts strip + // the unknown field and keep their legacy behavior. + reason: 'user' + }) + const response = sessionTabClose.interpret(reply) + if (!response.accepted || response.value.refused) { + showCloseFailed(reply.ok ? undefined : reply.error.code) + } else { const chatScope = mobileNativeChatScopeKey(hostId, worktreeId, tab.id) if (chatScope && response.value.retirementConfirmed) { closeMobileNativeChatTabDraft(chatScope) @@ -195,7 +213,9 @@ export function useMobileSessionCloseActions(scope: MobileSessionContentCreateAc setActiveHandle(null) } } - } catch {} + } catch { + showCloseFailed() + } } return { handleRenameTerminal, diff --git a/mobile/src/test-support/rpc-recording/adapters/session-screen-tab-mount-adapters.ts b/mobile/src/test-support/rpc-recording/adapters/session-screen-tab-mount-adapters.ts index da77ee4ebc5..64e3c776843 100644 --- a/mobile/src/test-support/rpc-recording/adapters/session-screen-tab-mount-adapters.ts +++ b/mobile/src/test-support/rpc-recording/adapters/session-screen-tab-mount-adapters.ts @@ -134,6 +134,7 @@ export function sessionScreenTabMountAdapters( pendingSelectionRef: { current: null }, pendingBrowserFocusPageIdRef: { current: null }, unsubscribeTerminal: (handle: string) => effect('unsubscribe-terminal', { handle }), + showToast: (message: string) => effect('toast', { message }), subscribeToTerminal: (handle: string) => { effect('subscribe-terminal', { handle }) return true