Files
orca/mobile/src/session/initial-session-terminal.ts
T
Brennan Benson 292626eebb fix(mobile): keep closed sessions empty (#11251)
* fix(mobile): stop re-creating a terminal when the session tab list empties

The session route treated "zero session tabs" as "this workspace has never
had anything" and auto-created a terminal. Closing the last tab prunes
sessionTabs and nulls activeHandle locally, which is exactly that state, so
the close was immediately followed by a brand-new terminal — and the guard
re-arms on every route mount, so it recurs across visits (#9717, #7345).

Gate the auto-create on whether this route has ever published a non-empty tab
list for the workspace. A cold hydrate still gets its first terminal; an
emptied list gets the empty state and its create button.

Extracted to a hook because the route file sits at its max-lines cap; the
call site is 4 counted lines smaller than the effect it replaces.

* fix(mobile): keep emptied workspaces empty across visits

* fix(mobile): reach the auto-create callbacks without a render-time ref write

The hook kept `consumeCreationRoute`/`createTerminal` out of the effect deps by
writing latest-refs during render. React can replay or discard a render, so the
write can leak from UI that never commits — React Doctor flags it as a blocking
"Ref mutated during render" error, which failed PR Checks' static analysis.

useEffectEvent (React 19.2, already used in SourceControl.tsx) gives the same
stable-callback-outside-deps behaviour with no render-time mutation. Retire the
deprecated `MutableRefObject` for `RefObject` in the same pass.

Retargets the source pin at the new wiring; test counts unchanged.

* docs(mobile): document the two per-route reset contracts

Both exported helpers exist for a non-obvious reason — they must be re-created or
re-derived per worktree, or a reused route inherits the previous workspace's
hydration state and the resurrection guard silently disarms.

* fix(mobile): preserve terminal creation through reconnect
2026-07-30 12:58:24 -07:00

47 lines
1.9 KiB
TypeScript

// Mirrors the desktop `shouldAutoCreateInitialTerminal` gate: a newly created
// workspace that hydrates empty gets one terminal so the session isn't blank.
// Kept pure (no react-native imports) so the gate is unit-testable.
export type InitialSessionTerminalAutoCreateInput = {
/** Navigation came directly from creating this workspace. */
newlyCreatedWorkspace: boolean
/** Host connection is up and an RPC client is attached. */
connected: boolean
/** At least one session-tab snapshot has been applied on this route. */
tabsLoaded: boolean
/** Session tabs currently rendered on the phone. */
visibleTabCount: number
/** A terminal is still streaming even though the tab list reads empty. */
hasActiveTerminalHandle: boolean
/** A terminal, browser, or markdown create is already in flight. */
createInFlight: boolean
/** This route has shown at least one session tab for this workspace. */
sawSessionTabs: boolean
/** This route already auto-created a terminal for this workspace. */
autoCreatedForWorktree: boolean
}
/**
* Whether to auto-create the first terminal of a newly created mobile session.
*
* `sawSessionTabs` is the resurrection guard: emptiness that *follows* a
* populated tab list was produced by a close (the user's, or the host dropping
* a dead pane), and re-creating there spawns a brand-new terminal the user
* never asked for — issues #9717 / #7345. Only a workspace that has shown
* nothing since its creation route opened is eligible.
*/
export function shouldAutoCreateInitialSessionTerminal(
input: InitialSessionTerminalAutoCreateInput
): boolean {
if (!input.newlyCreatedWorkspace || !input.connected || !input.tabsLoaded) {
return false
}
if (input.visibleTabCount > 0 || input.hasActiveTerminalHandle) {
return false
}
if (input.createInFlight || input.autoCreatedForWorktree) {
return false
}
return !input.sawSessionTabs
}