From f8bbf77f6601ee9d30ec8d020c5b130bf8c62b8e Mon Sep 17 00:00:00 2001 From: Rob Sandhu Date: Wed, 21 Oct 2026 21:54:30 -0700 Subject: [PATCH] feat(app): show Metro project path beside each device run Both platforms load their JavaScript from one shared Metro, so a successful native build alone does not identify which project's bundle is being served. Each live Expo or React Native run in the device tab now names the directory Metro serves, home-abbreviated and clipped from the start so the project folder stays visible in narrow panes. Co-Authored-By: Claude Opus 5.5 --- .../e2e/regression/device-controls.spec.ts | 10 ++++++ .../components/session/session-device-tab.tsx | 31 ++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/packages/app/e2e/regression/device-controls.spec.ts b/packages/app/e2e/regression/device-controls.spec.ts index 245d702b9..69c07da00 100644 --- a/packages/app/e2e/regression/device-controls.spec.ts +++ b/packages/app/e2e/regression/device-controls.spec.ts @@ -120,6 +120,15 @@ for (const platforms of [["ios", "android"], ["ios"], ["android"]] as const) { ).toBeEnabled() } + // Only a live JavaScript run names the project Metro serves; the failed iOS build loads nothing. + const metro = page.locator('[data-slot="device-metro"]') + await expect(metro).toHaveCount(expo ? 1 : 0) + if (expo) { + const android = page.getByRole("toolbar", { name: labels.android, exact: true }).locator("xpath=..") + await expect(android.locator('[data-slot="device-metro"]')).toContainText("Metro~/DeviceControls") + await expect(android.getByTitle(directory, { exact: true })).toBeVisible() + } + for (const platform of platforms) { const label = labels[platform] const pane = page.getByRole("toolbar", { name: label, exact: true }) @@ -138,6 +147,7 @@ for (const platforms of [["ios", "android"], ["ios"], ["android"]] as const) { ).toBeEnabled() } if (expo) await expect(controls.getByRole("button", { name: "Stop", exact: true })).toBeEnabled() + await expect(metro).toHaveCount(0) expect(calls).toEqual([ ...(expo ? [{ path: "/api/device-preview/bundler/start", directory }] : []), ...platforms.map((platform) => ({ path: "/api/device-preview/run", directory, platform })), diff --git a/packages/app/src/components/session/session-device-tab.tsx b/packages/app/src/components/session/session-device-tab.tsx index e7b207481..bb1bf955c 100644 --- a/packages/app/src/components/session/session-device-tab.tsx +++ b/packages/app/src/components/session/session-device-tab.tsx @@ -9,8 +9,9 @@ import { createStore } from "solid-js/store" import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" import { useServerSDK } from "@/context/server-sdk" +import { useServerSync } from "@/context/server-sync" import { devicePreviewUrl } from "@/utils/device-preview" -import { createDeviceState, deviceBuildBusy, devicePreviewIcon } from "./device-state" +import { createDeviceState, deviceBuildBusy, deviceFrameworkIcon, devicePreviewIcon } from "./device-state" const ALL_PLATFORMS: DevicePreview.Platform[] = ["ios", "android"] const LOG_TAIL = 40 @@ -127,6 +128,7 @@ function DevicePane(props: { classList?: Record }) { const serverSDK = useServerSDK() + const serverSync = useServerSync() const platform = usePlatform() const language = useLanguage() let frame: HTMLIFrameElement | undefined @@ -154,6 +156,18 @@ function DevicePane(props: { return { text: url() ?? server()?.command ?? "", failed: false } }) + // Both apps load their JavaScript from one Metro, so a successful native build does not say + // which project's bundle is running. Name Metro's project beside every live JS run. + const metro = createMemo(() => { + const current = build() + const bundler = props.device.info()?.bundler + if (!current || !bundler || bundler.status === "exited") return + if (!deviceBuildBusy(current) && current.status !== "running") return + if (!deviceFrameworkIcon(current.framework ?? props.device.info()?.framework)) return + const home = serverSync().data.path.home + return { full: bundler.directory, short: home ? bundler.directory.replace(home, "~") : bundler.directory } + }) + const reload = () => { const next = url() if (!frame || !next) return @@ -250,6 +264,21 @@ function DevicePane(props: { + + {(value) => ( +
+ + {language.t("session.device.bundler.label")} + {/* Clip the start so the project folder stays visible; the LTR isolate keeps `~/` in front. */} + + {value().short} + +
+ )} +