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
@@ -1,7 +1,22 @@
import { describe, expect, it } from "vitest";
import {
canCloseLeftSidebarGesture,
canCloseRightSidebarGesture,
canOpenLeftSidebarGesture,
canOpenRightSidebarGesture,
getLeftSidebarAnimationTargets,
getRightSidebarAnimationTargets,
MOBILE_PANEL_STATE_AGENT,
MOBILE_PANEL_STATE_AGENT_LIST_CLOSING,
MOBILE_PANEL_STATE_AGENT_LIST_OPEN,
MOBILE_PANEL_STATE_AGENT_LIST_OPENING,
MOBILE_PANEL_STATE_FILE_EXPLORER_CLOSING,
MOBILE_PANEL_STATE_FILE_EXPLORER_OPEN,
MOBILE_PANEL_STATE_FILE_EXPLORER_OPENING,
MOBILE_PANEL_TARGET_AGENT,
MOBILE_PANEL_TARGET_AGENT_LIST,
MOBILE_PANEL_TARGET_FILE_EXPLORER,
shouldSettleMobilePanelTransition,
shouldSyncSidebarAnimation,
} from "./sidebar-animation-state";
@@ -41,4 +56,56 @@ describe("sidebar-animation-state", () => {
backdropOpacity: 0,
});
});
it("allows the left open gesture only after the app is settled on the agent panel", () => {
expect(canOpenLeftSidebarGesture(MOBILE_PANEL_STATE_AGENT, -430, 430)).toBe(true);
expect(canOpenLeftSidebarGesture(MOBILE_PANEL_STATE_AGENT, -240, 430)).toBe(false);
expect(canOpenLeftSidebarGesture(MOBILE_PANEL_STATE_AGENT_LIST_CLOSING, -430, 430)).toBe(false);
expect(canOpenLeftSidebarGesture(MOBILE_PANEL_STATE_AGENT_LIST_OPENING, -430, 430)).toBe(false);
expect(canOpenLeftSidebarGesture(MOBILE_PANEL_STATE_FILE_EXPLORER_OPEN, -430, 430)).toBe(false);
});
it("allows the left close gesture only while the left sidebar is settled open", () => {
expect(canCloseLeftSidebarGesture(MOBILE_PANEL_STATE_AGENT_LIST_OPEN)).toBe(true);
expect(canCloseLeftSidebarGesture(MOBILE_PANEL_STATE_AGENT_LIST_OPENING)).toBe(false);
expect(canCloseLeftSidebarGesture(MOBILE_PANEL_STATE_AGENT_LIST_CLOSING)).toBe(false);
expect(canCloseLeftSidebarGesture(MOBILE_PANEL_STATE_AGENT)).toBe(false);
});
it("allows the right open gesture only after the app is settled on the agent panel", () => {
expect(canOpenRightSidebarGesture(MOBILE_PANEL_STATE_AGENT, 430, 430)).toBe(true);
expect(canOpenRightSidebarGesture(MOBILE_PANEL_STATE_AGENT, 240, 430)).toBe(false);
expect(canOpenRightSidebarGesture(MOBILE_PANEL_STATE_FILE_EXPLORER_CLOSING, 430, 430)).toBe(
false,
);
expect(canOpenRightSidebarGesture(MOBILE_PANEL_STATE_FILE_EXPLORER_OPENING, 430, 430)).toBe(
false,
);
expect(canOpenRightSidebarGesture(MOBILE_PANEL_STATE_AGENT_LIST_OPEN, 430, 430)).toBe(false);
});
it("allows the right close gesture only while the right sidebar is settled open", () => {
expect(canCloseRightSidebarGesture(MOBILE_PANEL_STATE_FILE_EXPLORER_OPEN)).toBe(true);
expect(canCloseRightSidebarGesture(MOBILE_PANEL_STATE_FILE_EXPLORER_OPENING)).toBe(false);
expect(canCloseRightSidebarGesture(MOBILE_PANEL_STATE_FILE_EXPLORER_CLOSING)).toBe(false);
expect(canCloseRightSidebarGesture(MOBILE_PANEL_STATE_AGENT)).toBe(false);
});
it("rejects stale settle callbacks from a panel that is no longer the destination", () => {
expect(
shouldSettleMobilePanelTransition(
MOBILE_PANEL_TARGET_AGENT_LIST,
MOBILE_PANEL_TARGET_AGENT_LIST,
),
).toBe(true);
expect(
shouldSettleMobilePanelTransition(MOBILE_PANEL_TARGET_AGENT_LIST, MOBILE_PANEL_TARGET_AGENT),
).toBe(false);
expect(
shouldSettleMobilePanelTransition(
MOBILE_PANEL_TARGET_AGENT_LIST,
MOBILE_PANEL_TARGET_FILE_EXPLORER,
),
).toBe(false);
});
});
@@ -15,6 +15,20 @@ interface SidebarAnimationTargets {
backdropOpacity: number;
}
export const MOBILE_PANEL_STATE_AGENT = 0;
export const MOBILE_PANEL_STATE_AGENT_LIST_OPENING = 1;
export const MOBILE_PANEL_STATE_AGENT_LIST_OPEN = 2;
export const MOBILE_PANEL_STATE_AGENT_LIST_CLOSING = 3;
export const MOBILE_PANEL_STATE_FILE_EXPLORER_OPENING = 4;
export const MOBILE_PANEL_STATE_FILE_EXPLORER_OPEN = 5;
export const MOBILE_PANEL_STATE_FILE_EXPLORER_CLOSING = 6;
export const MOBILE_PANEL_TARGET_AGENT = 0;
export const MOBILE_PANEL_TARGET_AGENT_LIST = 1;
export const MOBILE_PANEL_TARGET_FILE_EXPLORER = 2;
const CLOSED_POSITION_TOLERANCE = 1;
export function shouldSyncSidebarAnimation(input: SidebarAnimationSyncInput): boolean {
return (
input.previousIsOpen !== input.nextIsOpen || input.previousWindowWidth !== input.nextWindowWidth
@@ -38,3 +52,45 @@ export function getRightSidebarAnimationTargets(
backdropOpacity: input.isOpen ? 1 : 0,
};
}
export function canOpenLeftSidebarGesture(
mobilePanelState: number,
translateX: number,
windowWidth: number,
): boolean {
"worklet";
return (
mobilePanelState === MOBILE_PANEL_STATE_AGENT &&
translateX <= -windowWidth + CLOSED_POSITION_TOLERANCE
);
}
export function canCloseLeftSidebarGesture(mobilePanelState: number): boolean {
"worklet";
return mobilePanelState === MOBILE_PANEL_STATE_AGENT_LIST_OPEN;
}
export function canOpenRightSidebarGesture(
mobilePanelState: number,
translateX: number,
windowWidth: number,
): boolean {
"worklet";
return (
mobilePanelState === MOBILE_PANEL_STATE_AGENT &&
translateX >= windowWidth - CLOSED_POSITION_TOLERANCE
);
}
export function canCloseRightSidebarGesture(mobilePanelState: number): boolean {
"worklet";
return mobilePanelState === MOBILE_PANEL_STATE_FILE_EXPLORER_OPEN;
}
export function shouldSettleMobilePanelTransition(
activeTarget: number,
settledTarget: number,
): boolean {
"worklet";
return activeTarget === settledTarget;
}