fix(mobile): clear state after closing final tab (#11637)

* fix(mobile): clear state after closing final tab

* fix(mobile): clear terminal on empty snapshots

* fix(mobile): preserve terminal during transient empty snapshot
This commit is contained in:
Brennan Benson
2026-07-31 00:39:44 -07:00
committed by GitHub
parent 336cef3185
commit 451baa1bc4
2 changed files with 35 additions and 2 deletions
@@ -1894,6 +1894,7 @@ export default function SessionScreen() {
setActiveHandle(active.terminal)
subscribeToTerminal(active.terminal)
} else if (active) {
// Why: an empty snapshot can transiently omit a live terminal; explicit close clears it on RPC success.
const previous = activeHandleRef.current
if (previous) {
unsubscribeTerminal(previous)
@@ -4133,6 +4134,7 @@ export default function SessionScreen() {
reason: 'user'
})
if (response.ok) {
const remainingTabs = sessionTabsRef.current.filter((candidate) => candidate.id !== tab.id)
if (tab.type === 'browser' && tab.browserPageId === pendingBrowserFocusPageIdRef.current) {
pendingBrowserFocusPageIdRef.current = null
}
@@ -4143,14 +4145,16 @@ export default function SessionScreen() {
initializedHandlesRef.current.delete(terminalHandle)
clearTerminalLiveInputDefault(terminalHandle)
}
setSessionTabs((prev) => prev.filter((candidate) => candidate.id !== tab.id))
sessionTabsRef.current = remainingTabs
setSessionTabs(remainingTabs)
// Why: tombstone the closed tab and rely on the snapshot, not a blind refetch that often re-added the not-yet-closed tab.
closedTabTombstonesRef.current.set(tab.id, Date.now() + 10_000)
// Why: bulk close re-activates the anchor before awaiting each close;
// the render-synced ref sees that switch while this closure would not,
// so comparing against the ref keeps the anchor from being nulled out.
if (activeSessionTabIdRef.current === tab.id) {
if (activeSessionTabIdRef.current === tab.id || remainingTabs.length === 0) {
activeSessionTabTypeRef.current = null
activeSessionTabIdRef.current = null
setActiveSessionTabId(null)
activeHandleRef.current = null
setActiveHandle(null)
@@ -0,0 +1,29 @@
import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
const sessionRouteSource = readFileSync(
new URL('../../app/h/[hostId]/session/[worktreeId].tsx', import.meta.url),
'utf8'
)
describe('mobile session last-tab close', () => {
it('preserves terminal identity while an empty snapshot may be transient', () => {
const start = sessionRouteSource.indexOf('const applySessionTabs = useCallback')
const end = sessionRouteSource.indexOf('const readMarkdownTab', start)
const block = sessionRouteSource.slice(start, end)
expect(block).toContain('} else if (active) {')
})
it('clears stale active identity when closing leaves no tabs', () => {
const start = sessionRouteSource.indexOf('async function handleCloseSessionTab')
const end = sessionRouteSource.indexOf('const bulkCloseActions', start)
const block = sessionRouteSource.slice(start, end)
expect(block).toContain(
'activeSessionTabIdRef.current === tab.id || remainingTabs.length === 0'
)
expect(block).toContain('activeSessionTabIdRef.current = null')
expect(block).toContain('activeHandleRef.current = null')
})
})