mirror of
https://github.com/stablyai/orca.git
synced 2026-10-26 00:03:55 +00:00
* 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
107 lines
3.1 KiB
TypeScript
107 lines
3.1 KiB
TypeScript
import { useEffect, useEffectEvent, useReducer, type RefObject } from 'react'
|
|
import { shouldAutoCreateInitialSessionTerminal } from './initial-session-terminal'
|
|
|
|
type InitialSessionTerminalAutoCreateState = {
|
|
autoCreatedForWorktree: string | null
|
|
sawSessionTabs: boolean
|
|
}
|
|
|
|
/**
|
|
* Fresh auto-create bookkeeping. The route must re-create this on every worktree
|
|
* change, or a revisit inherits the previous workspace's `sawSessionTabs`.
|
|
*/
|
|
export function createInitialSessionAutoCreateState(): InitialSessionTerminalAutoCreateState {
|
|
return { autoCreatedForWorktree: null, sawSessionTabs: false }
|
|
}
|
|
|
|
type InitialSessionTerminalAutoCreateArgs = {
|
|
client: unknown
|
|
newlyCreatedWorkspace: boolean
|
|
connState: string
|
|
terminalsLoaded: boolean
|
|
visibleTabCount: number
|
|
activeHandle: string | null
|
|
createInFlight: boolean
|
|
stateRef: RefObject<InitialSessionTerminalAutoCreateState>
|
|
worktreeId: string
|
|
consumeCreationRoute: () => void
|
|
createTerminal: () => void
|
|
}
|
|
|
|
/**
|
|
* Creates the first terminal of a newly created mobile workspace that hydrates
|
|
* empty, at most once per route. See initial-session-terminal.
|
|
*/
|
|
export function useInitialSessionTerminalAutoCreate(
|
|
args: InitialSessionTerminalAutoCreateArgs
|
|
): void {
|
|
const {
|
|
client,
|
|
newlyCreatedWorkspace,
|
|
connState,
|
|
terminalsLoaded,
|
|
visibleTabCount,
|
|
activeHandle,
|
|
createInFlight,
|
|
stateRef,
|
|
worktreeId
|
|
} = args
|
|
// Why: the route re-creates both callbacks every render; useEffectEvent keeps them
|
|
// out of the deps without mutating a ref during render.
|
|
const consumeCreationRoute = useEffectEvent(args.consumeCreationRoute)
|
|
const createTerminal = useEffectEvent(args.createTerminal)
|
|
|
|
useEffect(() => {
|
|
if (
|
|
newlyCreatedWorkspace &&
|
|
client &&
|
|
connState === 'connected' &&
|
|
terminalsLoaded &&
|
|
(visibleTabCount > 0 || activeHandle !== null)
|
|
) {
|
|
consumeCreationRoute()
|
|
}
|
|
if (
|
|
!client ||
|
|
!shouldAutoCreateInitialSessionTerminal({
|
|
newlyCreatedWorkspace,
|
|
connected: connState === 'connected',
|
|
tabsLoaded: terminalsLoaded,
|
|
visibleTabCount,
|
|
hasActiveTerminalHandle: activeHandle !== null,
|
|
createInFlight,
|
|
sawSessionTabs: stateRef.current.sawSessionTabs,
|
|
autoCreatedForWorktree: stateRef.current.autoCreatedForWorktree === worktreeId
|
|
})
|
|
) {
|
|
return
|
|
}
|
|
stateRef.current.autoCreatedForWorktree = worktreeId
|
|
createTerminal()
|
|
}, [
|
|
activeHandle,
|
|
client,
|
|
connState,
|
|
createInFlight,
|
|
newlyCreatedWorkspace,
|
|
stateRef,
|
|
terminalsLoaded,
|
|
visibleTabCount,
|
|
worktreeId
|
|
])
|
|
}
|
|
|
|
/**
|
|
* Tracks "tabs hydrated" per worktree so a reused route reports `false` until the
|
|
* new workspace's own snapshot lands, rather than inheriting the old one's.
|
|
*/
|
|
export function useWorktreeSessionTabsLoaded(
|
|
worktreeId: string
|
|
): readonly [boolean, (loaded: boolean) => void] {
|
|
const [loadedForWorktree, setLoaded] = useReducer(
|
|
(_current: string | null, loaded: boolean) => (loaded ? worktreeId : null),
|
|
null
|
|
)
|
|
return [loadedForWorktree === worktreeId, setLoaded]
|
|
}
|