Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion MODULE.bazel.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

79 changes: 77 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,78 @@
# Player DevTools
# Player UI Devtools

TODO
Tools for inspecting and debugging live [Player UI](https://player-ui.github.io)
experiences across web, Android, and iOS. Devtools is itself plugin-driven: a
Devtools **plugin** runs inside the Player you want to inspect and publishes its
state; a Devtools **client** renders that state and sends interactions back.

## Architecture

Every part of Devtools is wired together by the [messenger](./devtools/messenger) — a
transport-agnostic, lossless protocol. A plugin on the Player side and a client
on the tooling side each run a `Messenger`; the plugin publishes Player state and
the client drives interactions.

```
Player (web / Android / iOS) Tooling
┌──────────────────────────────┐ ┌────────────────────────────┐
│ Devtools plugin │ │ Devtools client │
│ (basic, or your own) │◀────▶│ • browser extension │
│ taps Player hooks │ msgr │ • Flipper plugin (mobile) │
│ publishes flow/data/logs │ │ • MCP server (agents) │
└──────────────────────────────┘ └────────────────────────────┘
```

The content a plugin publishes is rendered by the clients as a Player experience
itself, using the [devtools-assets](https://github.com/player-ui/devtools-assets).

## Packages

### Foundations

| Package | Platforms | Description |
| -------------------------- | ------------------ | ------------------------------------------------------- |
| [`messenger`](./devtools/messenger) | TS · JVM · iOS | The communication protocol all of Devtools is built on. |
| [`types`](./devtools/types) | TS · iOS | Shared event, transaction, and state types. |
| [`utils`](./devtools/utils) | TS · iOS · SwiftUI | Shared utilities (e.g. `dsetAssign`). |

### Plugins (Player side)

| Package | Platforms | Description |
| ---------------------------------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| [`plugin`](./devtools/plugin) | TS · React · Android · JVM · iOS · SwiftUI | Base classes for building Devtools plugins. |
| [`plugins/basic`](./devtools/plugins/basic) | all of the above | The standard plugin — exposes flow, data, logs, config; supports expression evaluation and flow overrides. The reference implementation. |
| [`plugins/profiler`](./devtools/plugins/profiler) | all of the above | Profiles Player hook execution timing and exposes it as a flame graph. |

### Clients (tooling side)

| Package | Platform | Description |
| ------------------------------------ | --------------- | -------------------------------------------------------------------------------------------------------- |
| [`client`](./devtools/client) | Web/React | The `Panel` component that renders plugin content; embedded by the browser extension and Flipper plugin. |
| [`flipper-plugin`](./devtools/flipper-plugin) | Flipper desktop | Client for inspecting mobile (Android/iOS) Players. |
| [`mcp`](./devtools/mcp) | stdio / MCP | Client that exposes Devtools to AI agents as MCP tools. |

> The browser-extension client lives in a separate repo:
> [player-ui/browser-devtools](https://github.com/player-ui/browser-devtools).

## Getting started

To debug an existing Player, add the [basic plugin](./devtools/plugins/basic) for your
platform and connect a client:

- **Web** — add `BasicReactDevtoolsPlugin` and activate the connection from the
browser extension popup.
- **Mobile** — add the platform basic plugin and connect Flipper (see
[`just install-flipper-client`](./devtools/flipper-plugin)).
- **Agents** — run the [MCP server](./devtools/mcp) against a running Flipper server.

To debug capabilities specific to your integration, build your own plugin on top
of the [`plugin`](./devtools/plugin) base classes — the basic plugin is the best
reference.

## Building

Devtools is built with [Bazel](https://bazel.build) via
[`rules_player`](https://github.com/player-ui/rules_player); common tasks are
wrapped in the repo `justfile` (e.g. `just install-flipper-client`, `just mcp`).
Each package's BUILD file uses the standard `rules_player` macros (`js_pipeline`,
`kt_jvm`, `kt_android`, `ios_library`, `swiftui_plugin`).
12 changes: 0 additions & 12 deletions devtools/README.md

This file was deleted.

28 changes: 16 additions & 12 deletions devtools/client/README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,8 @@
# @player-devtools/client

[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)

The `@player-devtools/client` exposes the Panel with the ReactPlayer, which is responsible for running content sent by Player devtool plugins on the inspected Player UI instance.

You can check how to use it in the [browser-extension](https://github.com/player-ui/browser-devtools) and [Flipper plugin](../flipper-plugin).

## Overview

The Devtools client is a part of the Player UI Devtools architecture. It allows you to create custom devtools panels that can be used to debug and inspect your Player UI experiences, using the same plugin system used by other Player UI plugins.

The Devtools client conveniently receives its content from the devtools plugins running into the Player UI in use by the inspected page. This feature allows you to extend the dev tools with custom panels, without the need to create a new extension. You can create your own devtools plugins and use them in the Player UI Devtools Browser Extension.

For a more comprehensive understanding of the architecture of the Devtools client, you can always refer to the detailed information provided in the Devtools Browser Extension README.
The `Panel` is the shared devtools UI surface, hosted by each client: the [browser extension](https://github.com/player-ui/browser-devtools) for web and the [Flipper plugin](../flipper-plugin) for mobile. The agent-facing [MCP server](../mcp) consumes the same Player devtools instrumentation without rendering the `Panel`.

## Installation

Expand All @@ -26,6 +16,14 @@ npm install @player-devtools/client
yarn add @player-devtools/client
```

## Overview

The Devtools client is a part of the Player UI Devtools architecture. It allows you to create custom devtools panels that can be used to debug and inspect your Player UI experiences, using the same plugin system used by other Player UI plugins.

The Devtools client conveniently receives its content from the devtools plugins running into the Player UI in use by the inspected page. This feature allows you to extend the dev tools with custom panels, without the need to create a new extension. You can create your own devtools plugins and use them in the Player UI Devtools Browser Extension.

For a more comprehensive understanding of the architecture of the Devtools client, see the [Devtools root README](../../README.md) for the overall picture and the [plugin authoring guide](../plugin) for how plugins instrument a Player and feed content to this client.

## Usage

The Devtools client is a React component that receives content from devtools plugins running in the Player UI used by the inspected page. It can be used in your React application like any other React component.
Expand Down Expand Up @@ -57,6 +55,12 @@ const communicationLayer: Pick<
root.render(<Panel communicationLayer={communicationLayer} />);
```

## Related

- Sibling clients that host this `Panel`: the [Flipper plugin](../flipper-plugin) (mobile) and the browser extension (web).
- [`../mcp`](../mcp) — the agent-facing devtools surface.
- [`../README.md`](../../README.md) — the overall Player UI Devtools architecture.

## Contributing

We welcome contributions to the Player UI Devtools Browser Extension.
We welcome contributions to the Player UI Devtools.
1 change: 1 addition & 0 deletions devtools/client/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { Panel } from "./panel";
export { createExtensionClient, type ExtensionClient } from "./state/client";
97 changes: 97 additions & 0 deletions devtools/client/src/state/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Messenger } from "@player-devtools/messenger";
import type {
CommunicationLayerMethods,
ExtensionState,
ExtensionSupportedEvents,
} from "@player-devtools/types";
import { useStateReducer } from "@player-devtools/utils";

import { INITIAL_EXTENSION_STATE } from "../constants";
import { reducer } from "./reducer";

const NOOP_ID = -1;

export type ExtensionClient = {
getState: () => ExtensionState;
subscribe: (fn: (state: ExtensionState) => void) => () => void;
selectPlayer: (playerID: string) => void;
selectPlugin: (pluginID: string) => void;
handleInteraction: (interaction: {
type: string;
payload?: string;
target?: string;
}) => void;
destroy: () => void;
};

export const createExtensionClient = (
communicationLayer: CommunicationLayerMethods,
): ExtensionClient => {
const store = useStateReducer(reducer, INITIAL_EXTENSION_STATE);

const messenger = new Messenger<ExtensionSupportedEvents>({
context: "devtools",
messageCallback: (message) => store.dispatch(message),
...communicationLayer,
logger: console,
});

const selectPlayer = (playerID: string): void => {
store.dispatch({
id: NOOP_ID,
sender: "internal",
context: "devtools",
_messenger_: false,
timestamp: Date.now(),
type: "PLAYER_DEVTOOLS_PLAYER_SELECTED",
payload: { playerID },
});

messenger.sendMessage({
type: "PLAYER_DEVTOOLS_PLUGIN_INTERACTION",
payload: {
type: "player-selected",
payload: playerID,
},
});
};

const selectPlugin = (pluginID: string): void => {
store.dispatch({
id: NOOP_ID,
sender: "internal",
context: "devtools",
_messenger_: false,
timestamp: Date.now(),
type: "PLAYER_DEVTOOLS_PLUGIN_SELECTED",
payload: { pluginID },
});
};

const handleInteraction = ({
type,
payload,
target,
}: {
type: string;
payload?: string;
/** Player to address; defaults to the currently selected player. */
target?: string;
}): void => {
const resolvedTarget = target ?? store.getState().current.player;
messenger.sendMessage({
type: "PLAYER_DEVTOOLS_PLUGIN_INTERACTION",
payload: { type, payload },
...(resolvedTarget ? { target: resolvedTarget } : {}),
});
};

return {
getState: store.getState,
subscribe: store.subscribe,
selectPlayer,
selectPlugin,
handleInteraction,
destroy: () => messenger.destroy(),
};
};
129 changes: 18 additions & 111 deletions devtools/client/src/state/index.ts
Original file line number Diff line number Diff line change
@@ -1,126 +1,33 @@
import { Messenger } from "@player-devtools/messenger";
import type {
ExtensionSupportedEvents,
MessengerOptions,
} from "@player-devtools/types";
import { useCallback, useEffect, useMemo, useReducer } from "react";
import type { CommunicationLayerMethods } from "@player-devtools/types";
import { useEffect, useMemo, useSyncExternalStore } from "react";

import { INITIAL_EXTENSION_STATE } from "../constants";
import { reducer } from "./reducer";

const NOOP_ID = -1;
import { createExtensionClient } from "./client";

/**
* Custom React hook for managing the state of the devtools extension.
*
* This hook initializes the extension's state and sets up a communication layer
* using the `Messenger` class. It provides methods to select a player or plugin,
* and handle interactions, which dispatch actions to update the state accordingly.
* Thin React adapter over `createExtensionClient`.
*
* Creates the client once per `communicationLayer` identity, subscribes to
* state via `useSyncExternalStore`, and tears down the Messenger on unmount.
*/
export const useExtensionState = ({
communicationLayer,
}: {
/** the communication layer to use for the extension */
communicationLayer: Pick<
MessengerOptions<ExtensionSupportedEvents>,
"sendMessage" | "addListener" | "removeListener"
>;
communicationLayer: CommunicationLayerMethods;
}) => {
const [state, dispatch] = useReducer(reducer, INITIAL_EXTENSION_STATE);

const messengerOptions = useMemo<MessengerOptions<ExtensionSupportedEvents>>(
() => ({
context: "devtools",
target: "player",
messageCallback: (message) => {
dispatch(message);
},
...communicationLayer,
logger: console,
}),
[dispatch, communicationLayer],
const client = useMemo(
() => createExtensionClient(communicationLayer),
[communicationLayer],
);

const messenger = useMemo(
() => new Messenger(messengerOptions),
[messengerOptions],
);

useEffect(() => {
return () => {
messenger.destroy();
};
}, []);

const selectPlayer = useCallback(
(playerID: string) => {
dispatch({
id: NOOP_ID,
sender: "internal",
context: "devtools",
_messenger_: false,
timestamp: Date.now(),
type: "PLAYER_DEVTOOLS_PLAYER_SELECTED",
payload: {
playerID,
},
});
useEffect(() => () => client.destroy(), [client]);

messenger.sendMessage({
type: "PLAYER_DEVTOOLS_PLUGIN_INTERACTION",
payload: {
type: "player-selected",
payload: playerID,
},
});
},
[dispatch],
);

const selectPlugin = useCallback(
(pluginID: string) => {
dispatch({
id: NOOP_ID,
sender: "internal",
context: "devtools",
_messenger_: false,
timestamp: Date.now(),
type: "PLAYER_DEVTOOLS_PLUGIN_SELECTED",
payload: {
pluginID,
},
});
},
[dispatch],
);

/**
* Plugin authors can add interactive elements to the Player-UI content by leveraging
* the pub-sub plugin and having the handle interaction proxy the message to the inspected
* Player-UI instance.
*/
const handleInteraction = useCallback(
({
type,
payload,
}: {
/** interaction type */
type: string;
/** interaction payload */
payload?: string;
}) => {
messenger.sendMessage({
type: "PLAYER_DEVTOOLS_PLUGIN_INTERACTION",
payload: {
type,
payload,
},
...(state.current.player ? { target: state.current.player } : {}),
});
},
[messenger],
);
const state = useSyncExternalStore(client.subscribe, client.getState);

return { state, selectPlayer, selectPlugin, handleInteraction };
return {
state,
selectPlayer: client.selectPlayer,
selectPlugin: client.selectPlugin,
handleInteraction: client.handleInteraction,
};
};
Loading