In ChatGPT Work Mode, run sites-preview start "$PWD", open http://terminal.local:4173/ in the cloud browser, and verify the rendered app and its primary interactions. Keep that preview open and tell the user to inspect it in the cloud browser; do not present the local URL as a user-facing chat link. In Codex Desktop, run the local server yourself, open the preview in the in-app browser, and provide the clickable local URL. Do not deploy to Sites unless the user explicitly asks to share, publish, or deploy. Do not give the user server-start instructions when you can run it.
Before planning or implementing any mobile-app change, read this AGENTS.md in full. It is the source of truth for the template's runtime and component guidance.
Before making substantial visual changes, use the Product Design plugin's get-context skill when the visual source is unclear or no longer matches the current goal. When the user gives durable prototype-specific design feedback, preferences, or decisions, record them in AGENTS.md.
When implementing from a selected generated mock, treat that image as the source of truth for layout, component anatomy, density, spacing, color, typography, visible content, and hierarchy.
src/Prototype.tsx and src/prototype.css.src/App.tsx, src/main.tsx, src/styles.css, src/mobile/, public/assets/iphone/, public/assets/android/, public/assets/status/, vite.config.ts, worker/index.js, and scripts/prepare-sites-build.mjs as protected runtime files. Do not edit, replace, remove, or recreate them unless the user explicitly asks to change the mobile runtime itself. For an explicit runtime change, update the affected lock hashes only after verifying the new runtime behavior.npm run check:runtime before preview or handoff. If it fails, restore the protected runtime instead of weakening or bypassing the check.npm run build preserves the mobile runtime and prepares the static Cloudflare Worker output required by Sites. Before a Sites handoff, confirm dist/client/index.html, dist/server/index.js, dist/.openai/hosting.json, and source .openai/hosting.json exist, then run npm run test:sites. Do not replace this project with a Vinext starter.App composed around PhoneFrame -> KeyboardProvider, with StatusBar, app content, HomeIndicator, and KeyboardDock mounted inside the phone frame. StatusBar and the iOS home indicator are overlaid device chrome. When the Android keyboard is closed, the app viewport reserves the protected navigation-bar region instead of painting behind it. When the Android keyboard is open, preserve the current full-screen keyboard layout: its asset includes the IME navigation strip and the separate black navigation bar is hidden. iOS screens continue to paint behind the home-indicator area and own their safe-area content padding.iPhone / Pixel 10 device picker and both calibrated device presets. The Pixel screen is 427 x 952; its 32 x 32 camera circle and public/assets/android/navigation-bar.svg bottom navigation bar are protected device chrome, not app content.StatusBar as live device chrome, including its platform-specific typography, source status-icon assets, and spacing. Pixel 10 uses Roboto, Android indicators, and 32px top, left, and right padding. iPhone uses its iOS indicators, system typography, and calibrated spacing. Do not hardcode screenshot times like 9:41 into the status bar, replace its real-time clock, or move status bar content into app markup unless the user explicitly asks for a fixed/mock device time.PhoneFrame owns the calibrated device frame, screen portal, device picker, camera cutout, and custom cursor. Keep device assets in public/assets/iphone/ and public/assets/android/; if an asset fails to load, repair the asset path or restore the asset instead of removing the frame, keyboard, or image render.MobileScroll directly for simple single-screen prototypes. Use FlowStack for conventional multi-screen flows whose routes can own their fixed header and footer; when using it, define each route as a FlowScreen: { id, header?, headerHeight?, footer?, footerHeight?, render }, and use flow.push(screen), flow.pop(), and flow.replace(screen) from FlowStack render callbacks or useFlow() instead of introducing another router.Carousel for a carousel, horizontal rail, swipeable cards, image or media strip, horizontally scrollable cards, chip rail, or other horizontal collection.Prototype.tsx rather than forcing it through FlowStack. Keep app-owned fixed chrome as sibling layers outside MobileScroll.FlowScreen, put route-owned fixed headers or footers in FlowScreen.header or FlowScreen.footer. Set headerHeight to the visible app-toolbar height; FlowStack adds the device's top safe-area/status-bar inset automatically. Do not include StatusBar or its height in the header. Set footerHeight to the full app-footer height. FlowScreen.footer is an overlay, not reserved layout space; screens using it must add their own bottom content padding such as padding-bottom: calc(var(--flow-footer-height) + var(--mobile-safe-area-height) + 24px) so final content can scroll above the footer while still painting behind it.MobileScroll; it is for content that should move with scroll and rubber-band overscroll. Keep app-owned headers, nav bars, tabs, composers, and overlays outside it. This keeps scroll physics, safe areas, keyboard insets, scrollbars, and drag click suppression active without letting content paint under fixed chrome.MobileScroll should still allow drag scrolling when the pointer moves beyond tap slop. Use data-scroll-drag="ignore" only for rare controls that must own the drag gesture themselves.var(--keyboard-height) to ordinary screen/content padding inside MobileScroll; the scroll viewport already shrinks above the simulated keyboard. For custom fixed composers, search bars, or toast chrome, use useKeyboardInsets().bottomInset. It is relative to the app viewport: Android returns 0 while the closed-keyboard viewport already reserves navigation, then returns the keyboard height while open; iOS continues to clear the home indicator while closed and ride directly above the keyboard while open. Do not pin custom bottom chrome to bottom: 0 or only keyboardHeight.KeyboardInput, KeyboardTextarea, or MobileTextField for every text-entry control. A raw input or textarea disconnects focus, keyboard animation, safe-area insets, and attached surfaces.BottomSheet for phone-scoped sheets. Its props are open, onOpenChange, title, optional description, optional snap, and children; it renders through the phone screen portal and dismisses the keyboard before opening.Carousel for horizontally draggable cards, images, media, chips, or other horizontal collections. Do not recreate these with overflow-x, custom pointer handlers, or a generic div.Carousel can be nested directly inside MobileScroll. It owns horizontal gestures and automatically yields vertical gestures to the parent.data-scroll-drag="ignore" on or around a Carousel; doing so prevents vertical parent scrolling when a gesture begins inside it.Carousel; its runtime owns momentum and release motion.data-scroll-drag="ignore" only when a control must prevent parent scrolling in every drag direction.See src/mobile/COMPONENTS.md for the full component and gesture contract.
The simulated keyboard is a separate top-layer component. Before presenting anything that behaves like iOS navigation or modal UI, dismiss it first.
Call keyboard.hide() before:
FlowStack already hides the keyboard for push, pop, and replace. BottomSheet already hides it before opening. If you add new modal/sheet/navigation primitives, follow the same rule.
When a composer, search surface, or other keyboard-attached component closes, call keyboard.hide() in the same event before changing that component's open state. Position attached surfaces from useKeyboardInsets() rather than a separate timer or visibility flag so both dismiss together.
When any text-entry control loses focus, dismiss the simulated keyboard. If the control is custom or does not use the runtime's keyboard-aware fields, handle its blur event and call keyboard.hide() explicitly. Keep the keyboard open only when focus is moving directly to another text-entry control that should share the same keyboard session.
MobileScroll.dragstart suppression and non-draggable image styles so scroll drags that begin on images still scroll the prototype.KeyboardInput, KeyboardTextarea, or MobileTextField for text entry so the simulated keyboard and safe-area insets stay connected.