fix(mobile): tell the user when closing a session tab fails (#41404)

The close actions swallowed refusals and transport errors, so a tap on
close did nothing when the host refused (for example while the desktop
window is still loading). Show a short toast instead, with a specific
message for runtime_unavailable.
This commit is contained in:
Kelvin Amoaba
2026-11-12 06:42:25 +00:00
committed by GitHub
parent 176943e8fe
commit a3bbd507fd
16 changed files with 222 additions and 18 deletions
@@ -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"]
}
}
]
}
}
@@ -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"]
}
}
]
@@ -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",
+4
View File
@@ -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",
+4
View File
@@ -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",
+4
View File
@@ -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",
+4
View File
@@ -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",
+4
View File
@@ -2494,6 +2494,10 @@
"useAttachmentInputLeaseGate": {
"reconnecting": "添付に失敗しました(再接続中)。"
},
"useCloseActions": {
"closeFailed": "タブを閉じられませんでした。もう一度お試しください。",
"closeFailedDesktopNotReady": "タブを閉じられませんでした。コンピューターの Orca はまだ読み込み中です。少ししてからもう一度お試しください。"
},
"useContentCreateActions": {
"browserCommandFailed": "ブラウザコマンドの実行に失敗しました。",
"browserCreateFailed": "ブラウザの作成に失敗しました。",
+4
View File
@@ -2494,6 +2494,10 @@
"useAttachmentInputLeaseGate": {
"reconnecting": "첨부 실패 (다시 연결 중)"
},
"useCloseActions": {
"closeFailed": "탭을 닫지 못했습니다. 다시 시도하세요.",
"closeFailedDesktopNotReady": "탭을 닫지 못했습니다. 컴퓨터에서 Orca를 아직 불러오는 중입니다. 잠시 후 다시 시도하세요."
},
"useContentCreateActions": {
"browserCommandFailed": "브라우저 명령 실행에 실패했습니다.",
"browserCreateFailed": "브라우저를 만들지 못했습니다.",
+4
View File
@@ -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",
+4
View File
@@ -2638,6 +2638,10 @@
"useAttachmentInputLeaseGate": {
"reconnecting": "Не удалось прикрепить (повторное подключение)"
},
"useCloseActions": {
"closeFailed": "Не удалось закрыть вкладку. Попробуйте ещё раз.",
"closeFailedDesktopNotReady": "Не удалось закрыть вкладку. Orca на компьютере ещё загружается. Попробуйте ещё раз через минуту."
},
"useContentCreateActions": {
"browserCommandFailed": "Не удалось выполнить команду браузера",
"browserCreateFailed": "Не удалось создать вкладку браузера",
+4
View File
@@ -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ı",
+4
View File
@@ -2494,6 +2494,10 @@
"useAttachmentInputLeaseGate": {
"reconnecting": "加入附件失敗(重新連線中)"
},
"useCloseActions": {
"closeFailed": "無法關閉分頁。請再試一次。",
"closeFailedDesktopNotReady": "無法關閉分頁。電腦上的 Orca 仍在載入中,請稍後再試。"
},
"useContentCreateActions": {
"browserCommandFailed": "瀏覽器命令執行失敗",
"browserCreateFailed": "建立瀏覽器失敗",
+4
View File
@@ -2500,6 +2500,10 @@
"useAttachmentInputLeaseGate": {
"reconnecting": "添加附件失败(重新连接中)"
},
"useCloseActions": {
"closeFailed": "无法关闭标签页。请重试。",
"closeFailedDesktopNotReady": "无法关闭标签页。电脑上的 Orca 仍在加载,请稍后重试。"
},
"useContentCreateActions": {
"browserCommandFailed": "浏览器命令执行失败",
"browserCreateFailed": "创建浏览器失败",
@@ -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,
@@ -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