feat(app): native terminal on WebView with xterm.js

Replace the Expo DOM-backed terminal on iOS and Android with a managed
WebView running xterm.js. Web and desktop continue to use the existing
DOM implementation.

Mobile mounted tabs switch from display:none to opacity:0 for the hidden
slot on purpose: terminals stay mounted under the LRU tab cache, and
keeping the WebView in the layer tree avoids cold-starting it every time
the user returns to a terminal tab.

EAS native builds rebuild the WebView bundle post-install so the
generated HTML stays in sync with the entry source.

This does not fix the WS 1006 / NSPOSIXErrorDomain Code=54 host-disconnect
symptom that prompted the investigation. After restoring the baseline,
that symptom could not be replicated; do not read this commit as its
root cause or fix.
This commit is contained in:
Mohamed Boudra
2026-05-22 15:33:58 +07:00
parent af10e64f82
commit c46ff2e045
6 changed files with 921 additions and 7 deletions
@@ -714,13 +714,12 @@ const MobileMountedTabSlot = memo(function MobileMountedTabSlot({
[buildPaneContentModel, paneId, tabDescriptor],
);
const slotStyle = useMemo(
() => ({ display: isVisible ? ("flex" as const) : ("none" as const), flex: 1 }),
[isVisible],
);
const slotStyle = isVisible
? styles.mobileMountedTabSlotVisible
: styles.mobileMountedTabSlotHidden;
return (
<View style={slotStyle}>
<View style={slotStyle} pointerEvents={isVisible ? "auto" : "none"}>
<WorkspacePaneContent
content={content}
isWorkspaceFocused={isWorkspaceFocused}
@@ -2854,7 +2853,7 @@ function WorkspaceScreenContent({
const focusedPaneTabIds = useMemo(() => tabs.map((tab) => tab.tabId), [tabs]);
const focusedPaneTabDescriptorMap = useStableTabDescriptorMap(tabs);
const { mountedTabIds: mountedFocusedPaneTabIdsSet } = useMountedTabSet({
activeTabId: activeTabDescriptor?.tabId ?? null,
activeTabId,
allTabIds: focusedPaneTabIds,
cap: 3,
});
@@ -3654,6 +3653,15 @@ const styles = StyleSheet.create((theme) => ({
flex: 1,
minHeight: 0,
backgroundColor: theme.colors.surface0,
position: "relative",
},
mobileMountedTabSlotVisible: {
...StyleSheet.absoluteFillObject,
opacity: 1,
},
mobileMountedTabSlotHidden: {
...StyleSheet.absoluteFillObject,
opacity: 0,
},
contentPlaceholder: {
flex: 1,