feat: add iOS simulator worktree preview

Adds the worktree service, setup cache seeding, and Metro preview mount for /.sim. Also includes the related startup and mobile sidebar fixes needed by the updated worktree app flow.
This commit is contained in:
Mohamed Boudra
2026-06-11 12:23:53 +07:00
parent 6db285b1d9
commit a682476d11
21 changed files with 1143 additions and 189 deletions
@@ -655,21 +655,6 @@ function buildComposerConfig(input: {
};
}
function computeWorkspaceTitle(
workspace: ReturnType<typeof normalizeWorkspaceDescriptor> | null,
displayName: string,
sourceDirectory: string | null,
): string {
const fallbackDirectoryName = sourceDirectory?.split(/[\\/]/).findLast(Boolean) ?? null;
return (
workspace?.name ||
workspace?.projectDisplayName ||
displayName ||
fallbackDirectoryName ||
"Choose project"
);
}
function collectAttachedPrNumbers(attachments: ReadonlyArray<UserComposerAttachment>): Set<number> {
const numbers = new Set<number>();
for (const attachment of attachments) {
@@ -801,7 +786,6 @@ export function NewWorkspaceScreen({
projects,
selectedProject,
selectedSourceDirectory,
selectedDisplayName,
projectPickerOptions,
projectByOptionId,
selectedProjectOptionId,
@@ -1091,12 +1075,6 @@ export function NewWorkspaceScreen({
[composerState, draftKey, ensureWorkspace, serverId, toast],
);
const workspaceTitle = computeWorkspaceTitle(
workspace,
selectedDisplayName,
selectedSourceDirectory,
);
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null);
const handleAddImagesCallback = useCallback((addImages: (images: ImageAttachment[]) => void) => {
addImagesRef.current = addImages;
@@ -1308,30 +1286,18 @@ export function NewWorkspaceScreen({
triggerLabel,
],
);
const screenHeaderLeft = useMemo(() => <SidebarMenuToggle />, []);
return (
<FileDropZone onFilesDropped={handleFilesDropped}>
<View style={styles.container}>
<ScreenHeader
left={
<>
<SidebarMenuToggle />
<View style={styles.headerTitleContainer}>
<Text style={styles.headerTitle} numberOfLines={1}>
New workspace
</Text>
<Text style={styles.headerProjectTitle} numberOfLines={1}>
{workspaceTitle}
</Text>
</View>
</>
}
leftStyle={styles.headerLeft}
borderless
/>
<ScreenHeader left={screenHeaderLeft} borderless />
<View style={contentStyle}>
<TitlebarDragRegion />
<View style={styles.centered}>
<View style={styles.composerTitleContainer}>
<Text style={styles.composerTitle}>New workspace</Text>
</View>
<Composer
agentId={draftKey}
serverId={serverId}
@@ -1385,29 +1351,15 @@ const styles = StyleSheet.create((theme) => ({
width: "100%",
maxWidth: MAX_CONTENT_WIDTH,
},
headerLeft: {
gap: theme.spacing[2],
composerTitleContainer: {
marginBottom: theme.spacing[8],
paddingLeft: theme.spacing[6],
paddingRight: theme.spacing[4],
},
headerTitleContainer: {
flexShrink: 1,
minWidth: 0,
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
},
headerTitle: {
fontSize: theme.fontSize.base,
fontWeight: {
xs: "400",
md: "300",
},
composerTitle: {
fontSize: theme.fontSize.xl,
fontWeight: theme.fontWeight.normal,
color: theme.colors.foreground,
flexShrink: 0,
},
headerProjectTitle: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.base,
flexShrink: 1,
},
errorText: {
fontSize: theme.fontSize.sm,
@@ -26,6 +26,7 @@ import {
RotateCw,
Rows2,
Globe,
Plus,
SquarePen,
SquareTerminal,
X,
@@ -48,6 +49,7 @@ import { Shortcut } from "@/components/ui/shortcut";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
import { WORKSPACE_SECONDARY_HEADER_HEIGHT } from "@/constants/layout";
import type { ShortcutKey } from "@/utils/format-shortcut";
import { useWorkspaceTabLayout } from "@/screens/workspace/use-workspace-tab-layout";
import {
WorkspaceTabPresentationResolver,
@@ -66,6 +68,7 @@ import { RenderProfile } from "@/utils/render-profiler";
const DROPDOWN_WIDTH = 220;
const LOADING_TAB_LABEL_SKELETON_WIDTH = 80;
const DEFAULT_INLINE_ADD_BUTTON_RESERVED_WIDTH = 36;
const ThemedActivityIndicator = withUnistyles(ActivityIndicator);
const ThemedX = withUnistyles(X);
@@ -80,6 +83,7 @@ const ThemedSquareTerminal = withUnistyles(SquareTerminal);
const ThemedGlobe = withUnistyles(Globe);
const ThemedColumns2 = withUnistyles(Columns2);
const ThemedRows2 = withUnistyles(Rows2);
const ThemedPlus = withUnistyles(Plus);
const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
@@ -88,6 +92,51 @@ function newTabActionButtonStyle({ hovered, pressed }: PressableStateCallbackTyp
return [styles.newTabActionButton, (hovered || pressed) && styles.newTabActionButtonHovered];
}
function inlineAddActionButtonStyle({ hovered, pressed }: PressableStateCallbackType) {
return [styles.inlineAddActionButton, (hovered || pressed) && styles.newTabActionButtonHovered];
}
function updateMeasuredWidth(setWidth: Dispatch<SetStateAction<number>>, event: LayoutChangeEvent) {
const nextWidth = Math.round(event.nativeEvent.layout.width);
setWidth((current) => (Math.abs(current - nextWidth) > 1 ? nextWidth : current));
}
interface WorkspaceInlineAddTabButtonProps {
shortcutKeys: ShortcutKey[][] | null;
onPress: () => void;
onLayout: (event: LayoutChangeEvent) => void;
}
function WorkspaceInlineAddTabButton({
shortcutKeys,
onPress,
onLayout,
}: WorkspaceInlineAddTabButtonProps) {
return (
<View style={styles.inlineAddButton} onLayout={onLayout}>
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
<TooltipTrigger
testID="workspace-new-agent-tab-inline"
onPress={onPress}
accessibilityRole="button"
accessibilityLabel="New agent tab"
style={inlineAddActionButtonStyle}
>
<ThemedPlus size={16} uniProps={mutedColorMapping} />
</TooltipTrigger>
<TooltipContent side="bottom" align="center" offset={8}>
<View style={styles.newTabTooltipRow}>
<Text style={styles.newTabTooltipText}>New agent tab</Text>
{shortcutKeys ? (
<Shortcut chord={shortcutKeys} style={styles.newTabTooltipShortcut} />
) : null}
</View>
</TooltipContent>
</Tooltip>
</View>
);
}
function TabContextMenuItem({
entry,
}: {
@@ -490,21 +539,27 @@ export function WorkspaceDesktopTabsRow({
const splitDownKeys = useShortcutKeys("workspace-pane-split-down");
const [tabsContainerWidth, setTabsContainerWidth] = useState<number>(0);
const [tabsActionsWidth, setTabsActionsWidth] = useState<number>(0);
const [inlineAddButtonWidth, setInlineAddButtonWidth] = useState<number>(0);
const handleTabsContainerLayout = useCallback((event: LayoutChangeEvent) => {
const nextWidth = Math.round(event.nativeEvent.layout.width);
setTabsContainerWidth((current) => (Math.abs(current - nextWidth) > 1 ? nextWidth : current));
updateMeasuredWidth(setTabsContainerWidth, event);
}, []);
const handleTabsActionsLayout = useCallback((event: LayoutChangeEvent) => {
const nextWidth = Math.round(event.nativeEvent.layout.width);
setTabsActionsWidth((current) => (Math.abs(current - nextWidth) > 1 ? nextWidth : current));
updateMeasuredWidth(setTabsActionsWidth, event);
}, []);
const handleInlineAddButtonLayout = useCallback((event: LayoutChangeEvent) => {
updateMeasuredWidth(setInlineAddButtonWidth, event);
}, []);
const layoutMetrics = useMemo(
() => ({
rowHorizontalInset: 0,
actionsReservedWidth: Math.max(0, tabsActionsWidth),
actionsReservedWidth: Math.max(
0,
tabsActionsWidth + (inlineAddButtonWidth || DEFAULT_INLINE_ADD_BUTTON_RESERVED_WIDTH),
),
rowPaddingHorizontal: 0,
tabGap: 0,
maxTabWidth: 200,
@@ -513,7 +568,7 @@ export function WorkspaceDesktopTabsRow({
estimatedCharWidth: 7,
closeButtonWidth: 22,
}),
[tabsActionsWidth],
[inlineAddButtonWidth, tabsActionsWidth],
);
const tabLabelLengths = useMemo(
@@ -672,6 +727,11 @@ export function WorkspaceDesktopTabsRow({
getItemData={getTabDragData}
renderItem={renderTab}
/>
<WorkspaceInlineAddTabButton
shortcutKeys={newTabKeys}
onPress={handleCreateAgentTab}
onLayout={handleInlineAddButtonLayout}
/>
</ScrollView>
<View style={styles.tabsActions} onLayout={handleTabsActionsLayout}>
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
@@ -928,6 +988,11 @@ const styles = StyleSheet.create((theme) => ({
alignItems: "center",
paddingHorizontal: theme.spacing[2],
},
inlineAddButton: {
paddingHorizontal: theme.spacing[1],
alignItems: "center",
justifyContent: "center",
},
tab: {
paddingHorizontal: theme.spacing[3],
paddingVertical: theme.spacing[2],
@@ -1028,6 +1093,13 @@ const styles = StyleSheet.create((theme) => ({
alignItems: "center",
justifyContent: "center",
},
inlineAddActionButton: {
width: 28,
height: 28,
borderRadius: theme.borderRadius.md,
alignItems: "center",
justifyContent: "center",
},
newTabActionButtonDisabled: {
opacity: 0.5,
},