@@ -30,14 +30,19 @@ import {
3030 toggleAppearance ,
3131 type ControlMessage ,
3232} from "../api/controls" ;
33- import { fetchAccessibilityTree , fetchChromeProfile } from "../api/simulators" ;
33+ import {
34+ fetchAccessibilityTree ,
35+ fetchChromeProfile ,
36+ fetchSimulatorState ,
37+ } from "../api/simulators" ;
3438import type {
3539 AccessibilityNode ,
3640 AccessibilitySource ,
3741 AccessibilitySourcePreference ,
3842 AccessibilityTreeResponse ,
3943 ChromeProfile ,
4044 SimulatorMetadata ,
45+ SimulatorStateResponse ,
4146 TouchPhase ,
4247} from "../api/types" ;
4348import { AccessibilityInspector } from "../features/accessibility/AccessibilityInspector" ;
@@ -430,6 +435,8 @@ export function AppShell({
430435 const [ streamConfigApplyKey , setStreamConfigApplyKey ] = useState ( 0 ) ;
431436 const [ streamConfigReady , setStreamConfigReady ] = useState ( false ) ;
432437 const [ touchIndicators , setTouchIndicators ] = useState < TouchIndicator [ ] > ( [ ] ) ;
438+ const [ selectedSimulatorState , setSelectedSimulatorState ] =
439+ useState < SimulatorStateResponse | null > ( null ) ;
433440
434441 const menuRef = useRef < HTMLDivElement | null > ( null ) ;
435442 const outerCanvasRef = useRef < HTMLDivElement | null > ( null ) ;
@@ -494,7 +501,7 @@ export function AppShell({
494501 . includes ( searchNeedle ) ;
495502 } ) ;
496503
497- const selectedSimulator =
504+ const baseSelectedSimulator =
498505 ( fixedSimulatorUDID
499506 ? ( simulators . find (
500507 ( simulator ) => simulator . udid === fixedSimulatorUDID ,
@@ -511,6 +518,14 @@ export function AppShell({
511518 filteredSimulators . find ( ( simulator ) => simulator . isBooted ) ??
512519 filteredSimulators [ 0 ] ??
513520 null ;
521+ const selectedSimulator =
522+ baseSelectedSimulator &&
523+ selectedSimulatorState ?. udid === baseSelectedSimulator . udid
524+ ? {
525+ ...baseSelectedSimulator ,
526+ ...selectedSimulatorState . simulator ,
527+ }
528+ : baseSelectedSimulator ;
514529 const selectedSimulatorTransitionKind =
515530 selectedSimulator != null &&
516531 simulatorTransition ?. udid === selectedSimulator . udid
@@ -532,6 +547,59 @@ export function AppShell({
532547 : "Stopping..."
533548 : "" ;
534549
550+ useEffect ( ( ) => {
551+ const udid = baseSelectedSimulator ?. udid ;
552+ if ( ! udid ) {
553+ setSelectedSimulatorState ( null ) ;
554+ return ;
555+ }
556+
557+ let cancelled = false ;
558+ let timeoutId = 0 ;
559+ let controller : AbortController | null = null ;
560+
561+ const loadState = ( ) => {
562+ controller ?. abort ( ) ;
563+ controller =
564+ typeof AbortController !== "undefined" ? new AbortController ( ) : null ;
565+ void fetchSimulatorState (
566+ udid ,
567+ controller ? { signal : controller . signal } : { } ,
568+ )
569+ . then ( ( state ) => {
570+ if ( ! cancelled ) {
571+ setSelectedSimulatorState ( state ) ;
572+ }
573+ } )
574+ . catch ( ( error ) => {
575+ if (
576+ ! cancelled &&
577+ ! ( error instanceof DOMException && error . name === "AbortError" )
578+ ) {
579+ setSelectedSimulatorState ( null ) ;
580+ }
581+ } )
582+ . finally ( ( ) => {
583+ if ( ! cancelled ) {
584+ timeoutId = window . setTimeout (
585+ loadState ,
586+ baseSelectedSimulator ?. isBooted ? 1000 : 3000 ,
587+ ) ;
588+ }
589+ } ) ;
590+ } ;
591+
592+ setSelectedSimulatorState ( null ) ;
593+ loadState ( ) ;
594+ return ( ) => {
595+ cancelled = true ;
596+ if ( timeoutId ) {
597+ window . clearTimeout ( timeoutId ) ;
598+ }
599+ controller ?. abort ( ) ;
600+ } ;
601+ } , [ baseSelectedSimulator ?. isBooted , baseSelectedSimulator ?. udid ] ) ;
602+
535603 const handleStreamCanvasRef = useCallback (
536604 ( node : HTMLCanvasElement | null ) => {
537605 streamCanvasRef . current = node ;
@@ -1354,9 +1422,29 @@ export function AppShell({
13541422 ? `${ visibleStreamError } ${ streamStatus . detail } `
13551423 : visibleStreamError
13561424 : "" ;
1425+ const serverFrameSequence =
1426+ selectedSimulatorState ?. frameSequence ??
1427+ selectedSimulator ?. privateDisplay ?. frameSequence ??
1428+ 0 ;
1429+ const browserFramePending = Boolean (
1430+ selectedSimulator ?. isBooted &&
1431+ serverFrameSequence > 0 &&
1432+ ! hasFrame &&
1433+ streamStatus . state !== "error" &&
1434+ ! visibleStreamError ,
1435+ ) ;
1436+ const streamStatusLabel = streamAgentStatusLabel ( {
1437+ hasFrame,
1438+ simulator : selectedSimulator ,
1439+ simulatorState : selectedSimulatorState ,
1440+ stats,
1441+ streamStatusDetail : streamStatus . detail ,
1442+ streamStatusState : streamStatus . state ,
1443+ } ) ;
13571444 const viewportStatusOverlayLabel =
13581445 simulatorStatusOverlayLabel ||
13591446 streamStatusMessage ||
1447+ ( browserFramePending ? "Stream connected, browser frame pending" : "" ) ||
13601448 ( selectedSimulator ? visibleListError : "" ) ;
13611449 const viewportHasStreamError = Boolean (
13621450 streamStatus . state === "error" ||
@@ -2117,6 +2205,7 @@ export function AppShell({
21172205 chromeScreenStyle = { viewportScreenStyle }
21182206 chromeUrl = { chromeUrl }
21192207 chromeButtonUrl = { chromeButtonUrl }
2208+ browserFramePending = { browserFramePending }
21202209 debugPanel = {
21212210 debugVisible ? (
21222211 < DebugPanel
@@ -2180,6 +2269,7 @@ export function AppShell({
21802269 streamCanvasRef = { handleStreamCanvasRef }
21812270 streamBackend = { streamBackend }
21822271 streamCanvasKey = { streamCanvasKey }
2272+ streamStatusLabel = { streamStatusLabel }
21832273 statusOverlayLabel = { viewportStatusOverlayLabel }
21842274 touchIndicators = { touchIndicators }
21852275 touchOverlayVisible = { touchOverlayVisible }
@@ -2315,6 +2405,76 @@ function friendlyStreamError(
23152405 return friendlyClientError ( normalized ) ;
23162406}
23172407
2408+ function streamAgentStatusLabel ( {
2409+ hasFrame,
2410+ simulator,
2411+ simulatorState,
2412+ stats,
2413+ streamStatusDetail,
2414+ streamStatusState,
2415+ } : {
2416+ hasFrame : boolean ;
2417+ simulator : SimulatorMetadata | null ;
2418+ simulatorState : SimulatorStateResponse | null ;
2419+ stats : {
2420+ frameSequence : number ;
2421+ latestFrameGapMs : number ;
2422+ renderedFrames : number ;
2423+ } ;
2424+ streamStatusDetail ?: string ;
2425+ streamStatusState : string ;
2426+ } ) : string {
2427+ if ( ! simulator ) {
2428+ return "No simulator selected" ;
2429+ }
2430+
2431+ const display = simulator . privateDisplay ;
2432+ const serverFrameSequence =
2433+ simulatorState ?. frameSequence ?? display ?. frameSequence ?? 0 ;
2434+ const serverFrameAgeMs =
2435+ simulatorState ?. lastFrameAgeMs ??
2436+ ( display ?. lastFrameAt
2437+ ? Math . max ( 0 , Date . now ( ) - display . lastFrameAt )
2438+ : null ) ;
2439+ const foreground =
2440+ simulatorState ?. foregroundApp ?. appName ??
2441+ simulatorState ?. foregroundApp ?. bundleIdentifier ??
2442+ "unknown foreground app" ;
2443+ const parts = [
2444+ simulator . isBooted ? "Booted" : "Shutdown" ,
2445+ display ?. displayReady || simulatorState ?. displayReady
2446+ ? "display ready"
2447+ : "display not ready" ,
2448+ `server ${ display ?. displayStatus ?? simulatorState ?. displayStatus ?? "Unknown" } ` ,
2449+ `server frame ${ serverFrameSequence } ` ,
2450+ serverFrameAgeMs == null
2451+ ? "server frame age unknown"
2452+ : `server frame ${ formatMilliseconds ( serverFrameAgeMs ) } ago` ,
2453+ hasFrame
2454+ ? stats . frameSequence > 0
2455+ ? `browser frame ${ stats . frameSequence } `
2456+ : `browser rendered ${ stats . renderedFrames } `
2457+ : "browser frame pending" ,
2458+ `browser gap ${ formatMilliseconds ( stats . latestFrameGapMs ) } ` ,
2459+ `foreground ${ foreground } ` ,
2460+ `client ${ streamStatusState } ` ,
2461+ ] ;
2462+ if ( streamStatusDetail ) {
2463+ parts . push ( streamStatusDetail ) ;
2464+ }
2465+ return parts . join ( " · " ) ;
2466+ }
2467+
2468+ function formatMilliseconds ( value : number ) : string {
2469+ if ( ! Number . isFinite ( value ) || value <= 0 ) {
2470+ return "0ms" ;
2471+ }
2472+ if ( value < 1000 ) {
2473+ return `${ Math . round ( value ) } ms` ;
2474+ }
2475+ return `${ ( value / 1000 ) . toFixed ( 1 ) } s` ;
2476+ }
2477+
23182478function userFacingAccessibilityError ( message : string ) : string {
23192479 const normalized = message . trim ( ) ;
23202480 if ( ! normalized ) {
0 commit comments