diff --git a/desktop/flipper-ui-core/src/chrome/MetroButton.tsx b/desktop/flipper-ui-core/src/chrome/MetroButton.tsx
new file mode 100644
index 000000000..54652960a
--- /dev/null
+++ b/desktop/flipper-ui-core/src/chrome/MetroButton.tsx
@@ -0,0 +1,94 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @format
+ */
+
+import React, {useCallback, useEffect, useState} from 'react';
+import {MetroReportableEvent} from 'flipper-common';
+import {useStore} from '../utils/useStore';
+import {Button as AntButton} from 'antd';
+import {MenuOutlined, ReloadOutlined} from '@ant-design/icons';
+import {theme} from 'flipper-plugin';
+import {BaseDevice} from 'flipper-frontend-core';
+
+export default function MetroButton() {
+ const device = useStore((state) =>
+ state.connections.devices.find(
+ (device) => device.os === 'Metro' && device.connected.get(),
+ ),
+ ) as BaseDevice | undefined;
+
+ const sendCommand = useCallback(
+ (command: string) => {
+ device?.sendMetroCommand(command);
+ },
+ [device],
+ );
+ const [progress, setProgress] = useState(1);
+ const [_hasBuildError, setHasBuildError] = useState(false);
+
+ useEffect(() => {
+ if (!device) {
+ return;
+ }
+ function metroEventListener(event: MetroReportableEvent) {
+ if (event.type === 'bundle_build_started') {
+ setHasBuildError(false);
+ setProgress(0);
+ } else if (event.type === 'bundle_build_failed') {
+ setHasBuildError(true);
+ setProgress(1);
+ } else if (event.type === 'bundle_build_done') {
+ setHasBuildError(false);
+ setProgress(1);
+ } else if (event.type === 'bundle_transform_progressed') {
+ setProgress(event.transformedFileCount / event.totalFileCount);
+ }
+ }
+
+ const handle = device.addLogListener((l) => {
+ if (l.tag !== 'client_log') {
+ try {
+ metroEventListener(JSON.parse(l.message));
+ } catch (e) {
+ console.warn('Failed to parse metro message: ', l, e);
+ }
+ }
+ });
+
+ return () => {
+ device.removeLogListener(handle);
+ };
+ }, [device]);
+
+ if (!device) {
+ return null;
+ }
+
+ return (
+ <>
+ }
+ title="Reload React Native App"
+ type="ghost"
+ onClick={() => {
+ sendCommand('reload');
+ }}
+ loading={progress < 1}
+ style={{color: _hasBuildError ? theme.errorColor : undefined}}
+ />
+ }
+ title="Open the React Native Dev Menu on the device"
+ type="ghost"
+ onClick={() => {
+ sendCommand('devMenu');
+ }}
+ />
+ >
+ );
+}
diff --git a/desktop/flipper-ui-core/src/sandy-chrome/appinspect/AppInspect.tsx b/desktop/flipper-ui-core/src/sandy-chrome/appinspect/AppInspect.tsx
index b54caee10..ce9aa6586 100644
--- a/desktop/flipper-ui-core/src/sandy-chrome/appinspect/AppInspect.tsx
+++ b/desktop/flipper-ui-core/src/sandy-chrome/appinspect/AppInspect.tsx
@@ -13,6 +13,7 @@ import {LeftSidebar, SidebarTitle} from '../LeftSidebar';
import {Layout, styled} from '../../ui';
import {theme, useValue} from 'flipper-plugin';
import {PluginList} from './PluginList';
+import MetroButton from '../../chrome/MetroButton';
import {BookmarkSection} from './BookmarkSection';
import Client from '../../Client';
import {BaseDevice} from 'flipper-frontend-core';
@@ -41,6 +42,11 @@ export function AppInspect() {
{isDeviceConnected && isAppConnected && }
+ {isDeviceConnected && activeDevice && (
+
+
+
+ )}