fix(mobile): derive browser navigability in render and keep offset-keyed syntax segments

react-doctor flagged the tab-prop resync effect as derived state and the index key in MobileSyntaxSegments. Navigability is now resolved in render from the tab props plus the host refinement it superseded, so a tab update never lags a commit. appendSegment already drops zero-length segments, so the source-offset key is unique without an index.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
Jinwoo-H
2026-09-04 17:13:05 -04:00
parent ac3c620368
commit 3f44679b23
4 changed files with 45 additions and 14 deletions
+20 -10
View File
@@ -22,6 +22,11 @@ import {
} from './mobile-browser-frame-state'
import { displayBrowserUrl, normalizeBrowserUrl } from './browser-url'
import { resolveMobileBrowserAddressSync } from './mobile-browser-address-sync'
import {
resolveMobileBrowserNavigationState,
type MobileBrowserNavigationRefinement,
type MobileBrowserNavigationState
} from './mobile-browser-navigation-state'
import { MobileBrowserPaneView } from './MobileBrowserPaneView'
import { useMobileBrowserInteractions } from './use-mobile-browser-interactions'
import { useMobileBrowserPaneLayers } from './use-mobile-browser-pane-layers'
@@ -84,10 +89,21 @@ export function MobileBrowserPane({
const cachedInitialFrame = peekCachedBrowserFrame(cacheKey)
const [addressValue, setAddressValue] = useState(displayBrowserUrl(tab.url))
const [addressFocused, setAddressFocused] = useState(false)
const [navigationState, setNavigationState] = useState({
canGoBack: tab.canGoBack,
canGoForward: tab.canGoForward
})
// A host `navigation` event refines the tab's navigability until the tab itself republishes;
// an older host that never emits it leaves the tab props in charge. Derived in render so a tab
// update never lags one commit behind.
const [navigationRefinement, setNavigationRefinement] =
useState<MobileBrowserNavigationRefinement | null>(null)
const navigationState = resolveMobileBrowserNavigationState(tab, navigationRefinement)
const setNavigationState = useCallback(
(next: MobileBrowserNavigationState) => {
setNavigationRefinement({
...next,
base: { canGoBack: tab.canGoBack, canGoForward: tab.canGoForward }
})
},
[tab.canGoBack, tab.canGoForward]
)
const [addressSyncState, setAddressSyncState] = useState({
focused: false,
url: tab.url
@@ -177,12 +193,6 @@ export function MobileBrowserPane({
focused: addressFocused,
url: tab.url
})
// The screencast `navigation` event is a newer-host refinement; the tab props stay
// the baseline so an older host that never emits it does not freeze Back/Forward.
useEffect(() => {
setNavigationState({ canGoBack: tab.canGoBack, canGoForward: tab.canGoForward })
}, [tab.canGoBack, tab.canGoForward])
useEffect(() => {
if (addressSync.nextState === addressSyncState) {
return
@@ -0,0 +1,22 @@
export type MobileBrowserNavigationState = { canGoBack: boolean; canGoForward: boolean }
/** A host `navigation` refinement plus the tab props it refined, so a later tab update wins. */
export type MobileBrowserNavigationRefinement = MobileBrowserNavigationState & {
base: MobileBrowserNavigationState
}
// Why: the tab props are the baseline every host publishes; the screencast `navigation` event is
// a newer-host refinement that only stands until the tab itself republishes navigability.
export function resolveMobileBrowserNavigationState(
tab: MobileBrowserNavigationState,
refinement: MobileBrowserNavigationRefinement | null
): MobileBrowserNavigationState {
if (
refinement &&
refinement.base.canGoBack === tab.canGoBack &&
refinement.base.canGoForward === tab.canGoForward
) {
return { canGoBack: refinement.canGoBack, canGoForward: refinement.canGoForward }
}
return { canGoBack: tab.canGoBack, canGoForward: tab.canGoForward }
}
@@ -60,7 +60,7 @@ type MobileBrowserStreamArgs = {
setError: Dispatch<SetStateAction<string | null>>
setFrameMetadata: Dispatch<SetStateAction<BrowserScreencastFrameMetadata | null>>
setFrameUri: Dispatch<SetStateAction<string | null>>
setNavigationState?: Dispatch<SetStateAction<{ canGoBack: boolean; canGoForward: boolean }>>
setNavigationState?: (next: { canGoBack: boolean; canGoForward: boolean }) => void
setZoom: Dispatch<SetStateAction<BrowserZoomState>>
streamGenerationRef: { current: number }
tab: MobileBrowserTab
@@ -6,9 +6,8 @@ export function MobileSyntaxSegments({ segments }: { segments: MobileSyntaxSegme
let sourceOffset = 0
return (
<>
{segments.map((segment, index) => {
// The index keeps two adjacent zero-length segments of the same kind distinct.
const key = `${index}:${sourceOffset}:${segment.kind}`
{segments.map((segment) => {
const key = `${sourceOffset}:${segment.kind}`
sourceOffset += segment.text.length
return (
<Text key={key} style={[webSyntaxTextStyle, syntaxTokenStyles[segment.kind]]}>