feat: name the framework being debugged in the Rozenite app - #449
Merged
Conversation
Rozenite now debugs React Native, Lynx and web apps, and nothing in the UI said which one a window was showing. React Native DevTools gets it from the target's own application metadata (`ReactNativeApplication.metadataUpdated`), where React Native reports the device OS and the Chrome extension reports `web`, and puts it in front of the window title so several open windows stay tellable apart. The frontend rebuilds that title from its own parts on every metadata change, so the label is re-applied on each title mutation rather than written once. The standalone app gets it from the dev server instead: a Lynx dev server answers `/json/list` in Metro's dialect on purpose, so the connection gives nothing away and `/rozenite/app/config` now reports the platform, which the status footer names next to the connection badge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EmWnUnqTJoiJ9Kztnz8Ucx
Three platforms can now be behind a Rozenite window, and nothing said which one. React Native DevTools prefixes the framework onto its window title, and the standalone app names it in the status footer. Each target reports its own framework over the metadata event React Native already sends: `@rozenite/lynx-dev`'s bridge, which answers `ReactNativeApplication.enable` on behalf of a device with no such domain, now follows that reply with the `metadataUpdated` event the device would have sent. The framework goes in `integrationName` — the free-form "which integration drives this target" field — so `platform` keeps meaning the device OS, as it does everywhere else in the domain. The dev server also reports its platform in `/rozenite/app/config`. It is the coarser answer (it describes the server, not the target), so the device's own answer wins; it exists to fill the gap before a device has connected, which is what keeps the footer readable while connecting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EmWnUnqTJoiJ9Kztnz8Ucx
The dev server's platform answered a different question — which platform *it* serves, not what this target is — so the footer now waits for the target's own `ReactNativeApplication.metadataUpdated` and shows nothing until the handshake produces it. Drops `platform` from `/rozenite/app/config`, restoring that payload to what it was. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EmWnUnqTJoiJ9Kztnz8Ucx
Naming the framework inside a window already titled "React Native DevTools" says nothing, so the label is now written only when the target is something else — a web page today. Relabelling to React Native strips a stale label rather than leaving it behind. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EmWnUnqTJoiJ9Kztnz8Ucx
React Native DevTools sets its own window title and Rozenite no longer
touches it — the title tracker and its supporting type declaration are
gone, leaving @rozenite/runtime unchanged against main. The standalone
app instead titles its own window ("Lynx · Pixel 8 - Rozenite"), which
covers both places that app runs: a browser tab directly, and the
Electron shell through Electron's default page-title propagation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EmWnUnqTJoiJ9Kztnz8Ucx
V3RON
added a commit
that referenced
this pull request
Aug 31, 2026
`main` gained a `Framework` label read from `ReactNativeApplication.metadataUpdated` while this branch was open, so `device-connection.ts` carried two independent answers to "what is this target": that label, and the probe's `targetIsWeb`. Two readings of one fact can disagree — a target reporting `platform: 'web'` without a DOM would light up the footer and a compatibility gate differently — and the metadata reading is the racy one this branch exists to stop relying on. `resolveFramework` now takes every signal and applies them in precedence order: a known `integrationName` first, since it is the only thing that can identify Lynx and the probe cannot see it at all; then the probe, which is the same signal that resolves a target's `RozeniteIntegration`; then `platform` as a display-only fallback for when the probe could not answer, so the label still appears. The gate is never handed that fallback — it is told the target is unknown instead. The label still waits for the metadata event, as #449 made it. Publishing one from the probe alone would read a Lynx target as "React Native" until the integration name corrected it, trading a label that appears once for a visible flicker. Claude-Session: https://claude.ai/code/session_01AEzqE9P3sGFinTPJGK8CBi
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Names the framework a Rozenite window is attached to, now that three of them are supported and nothing said which one you were looking at.
Lynx · Pixel 8 - Rozenite. That covers both places this app runs — a browser tab shows the title directly, and the Electron shell picks it up through Electron's default page-title propagation (thetitle: 'Rozenite'inpackages/electron-app/src/main.jsis only the value before the page loads).@rozenite/runtimeis unchanged againstmain.@rozenite/lynx-devnow answersReactNativeApplication.enablewith themetadataUpdatedevent a device implementing that domain would have sent, so a Lynx target reports its own framework instead of the host having to infer one.Related Issue
Closes #450
Context
The framework is read per target, from
ReactNativeApplication.metadataUpdated— the event React Native already sends unprompted once the domain is enabled. Nothing new is invented on the wire for React Native; it was already reporting enough, and no code was reading it.Why
integrationNameand notplatform.platformmeans the device OS everywhere else in this domain — React Native sendsios/androidfromRCTPlatformNameand its Android equivalent — and a Lynx app runs on those same two, soplatform: "lynx"would be a category error.integrationNameis the free-form "which integration drives this target" field; React Native's own values there are strings likeiOS Bridge (RCTBridge). The reader's precedence: a knownintegrationNamewins, thenplatform === "web"(what@rozenite/chrome-extensionreports, and the only framework that is a platform), otherwise React Native. That default matters — a future OS must not silently cost the label.Why the bridge has to speak here.
translate-host-message.tsalready answersReactNativeApplication.enablelocally and never forwards it, because Lynx has no such domain and a-32601would fail the connection into a permanent retry loop. Having claimed to enable the domain, it now also has to speak for it — otherwise a host that enables it and waits learns nothing, forever. Every field in the synthesized event is a real value DebugRouter reported about the client (appDisplayName,deviceName,platformfromclient.os,appIdentifierwhen present);reactNativeVersionis omitted rather than faked.HostAction'sreplygained an optionalevents[], and the socket route resolves the client per message rather than capturing it at open, since a reload mints a fresh one.No dev-server involvement. An earlier revision of this branch also reported the server's platform in
/rozenite/app/configas a pre-handshake fallback. It answered a different question — which platform the server serves, not what the target is — so it was removed;middleware.tsandapp/src/config.tsare unchanged againstmain. The footer and title fill in once the handshake produces the event, andgetTarget()keeps the last known answer so neither blinks away while reloading or reconnecting.Note that the label never says "Web" in practice today: Rozenite for Web is debugged through React Native DevTools, which this PR leaves alone, so a web target never reaches this app. The reader handles it anyway — it costs nothing and it is what the extension already reports — and the same is true in reverse for Lynx, whose dev server serves no Fusebox frontend.
Testing
Automated, from the repository root after
git fetch origin main:pnpm checks:affected— 102/102 taskspnpm test:affected— 63/63 tasksNew tests:
@rozenite/lynx-dev— the metadata event's exact payload, the no-bundle-id and no-client-connected paths, thatRuntime.addBindingstays a bare reply, and a real-socket test overhttp+wsasserting the reply arrives before the event.@rozenite/app— the metadata reader's precedence (Lynx, web, React Native's own integration names, unreadable payloads), that the connection notifies subscribers without a status change, that the framework survives a reconnect, the title builder's fallbacks (either part missing degrades to something readable rather than a stray separator), and that the rendered app titles its window once the target reports.Not verified on a device: no Lynx device was available in this environment, so the Lynx handshake was exercised through the fakes above rather than end to end. The Electron title path is likewise reasoned from
main.jshaving nopage-title-updatedhandler, not observed — worth a quick look when someone next runs the shell. WithtitleBarStyle: 'hidden'the title is not drawn in the window frame, so what changes there is the window's name in Mission Control, alt-tab and the taskbar rather than anything in-window.