Skip to content

Commit 9f8be0b

Browse files
committed
fix: stabilize WebKit inspector connection
1 parent 3e3b681 commit 9f8be0b

5 files changed

Lines changed: 354 additions & 116 deletions

File tree

client/src/app/AppShell.tsx

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@ export function AppShell({
367367
refresh,
368368
simulators,
369369
} = useSimulatorList({ remote: remoteStream });
370+
const providerDisconnected = isProviderDisconnected(listError);
370371
const [debugVisible, setDebugVisible] = useState(() =>
371372
readStoredFlag(DEBUG_VISIBLE_STORAGE_KEY),
372373
);
@@ -1019,6 +1020,17 @@ export function AppShell({
10191020
return;
10201021
}
10211022

1023+
if (providerDisconnected) {
1024+
setAccessibilityRoots([]);
1025+
setAccessibilitySelectedId("");
1026+
setAccessibilityHoveredId(null);
1027+
setAccessibilityAvailableSources([]);
1028+
setAccessibilitySource("");
1029+
setAccessibilityError("Not connected");
1030+
setAccessibilityLoading(false);
1031+
return;
1032+
}
1033+
10221034
if (!selectedSimulator?.isBooted) {
10231035
setAccessibilityRoots([]);
10241036
setAccessibilitySelectedId("");
@@ -1034,7 +1046,9 @@ export function AppShell({
10341046
accessibilityRequestIdRef.current = requestId;
10351047
accessibilityLoadingRef.current = true;
10361048
setAccessibilityLoading(true);
1037-
setAccessibilityError("");
1049+
setAccessibilityError((current) =>
1050+
current === "Not connected" ? current : "",
1051+
);
10381052

10391053
try {
10401054
const snapshot = await fetchAccessibilityTree(
@@ -1098,7 +1112,7 @@ export function AppShell({
10981112
setAccessibilityLoading(false);
10991113
}
11001114
}
1101-
}, [accessibilityPreferredSource, selectedSimulator]);
1115+
}, [accessibilityPreferredSource, providerDisconnected, selectedSimulator]);
11021116

11031117
const changeAccessibilitySource = useCallback(
11041118
(source: AccessibilitySource) => {
@@ -2191,6 +2205,7 @@ export function AppShell({
21912205
accessibilityPanel={
21922206
<AccessibilityInspector
21932207
availableSources={accessibilityAvailableSources}
2208+
disconnected={providerDisconnected}
21942209
error={accessibilityError}
21952210
isLoading={accessibilityLoading}
21962211
onHover={setAccessibilityHoveredId}
@@ -2291,6 +2306,7 @@ export function AppShell({
22912306
viewMode={viewMode}
22922307
devtoolsPanel={
22932308
<DevToolsPanel
2309+
disconnected={providerDisconnected}
22942310
onClose={() => setDevToolsVisible(false)}
22952311
overviewRequestKey={devToolsOverviewRequestKey}
22962312
selectedSimulator={selectedSimulator}
@@ -2497,6 +2513,9 @@ function userFacingAccessibilityError(message: string): string {
24972513
}
24982514

24992515
const lower = normalized.toLowerCase();
2516+
if (isProviderDisconnected(normalized)) {
2517+
return "Not connected";
2518+
}
25002519
if (
25012520
lower.includes("no app inspector found") ||
25022521
lower.includes("no connected websocket inspector found") ||
@@ -2510,6 +2529,20 @@ function userFacingAccessibilityError(message: string): string {
25102529
return normalized;
25112530
}
25122531

2532+
function isProviderDisconnected(message: string): boolean {
2533+
const lower = message.trim().toLowerCase();
2534+
if (!lower || lower === AUTH_REQUIRED_MESSAGE.toLowerCase()) {
2535+
return false;
2536+
}
2537+
return (
2538+
lower.includes("failed to fetch") ||
2539+
lower.includes("load failed") ||
2540+
lower.includes("networkerror") ||
2541+
lower.includes("network error") ||
2542+
lower.includes("timed out waiting for provider")
2543+
);
2544+
}
2545+
25132546
function mergeStreamQualityResponse(
25142547
current: StreamConfig,
25152548
response: StreamQualityResponse,

client/src/features/accessibility/AccessibilityInspector.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { usePanelPresence } from "../../shared/hooks/usePanelPresence";
2626

2727
interface AccessibilityInspectorProps {
2828
availableSources: AccessibilitySource[];
29+
disconnected: boolean;
2930
error: string;
3031
isLoading: boolean;
3132
onHover: (id: string | null) => void;
@@ -44,6 +45,7 @@ type InspectorTab = "console" | "inspector" | "performance";
4445

4546
export function AccessibilityInspector({
4647
availableSources,
48+
disconnected,
4749
error,
4850
isLoading,
4951
onHover,
@@ -210,6 +212,7 @@ export function AccessibilityInspector({
210212
? findAccessibilityItem(tree, selectedId)
211213
: null;
212214
const sourceOptions = hierarchySourceOptions(availableSources, source);
215+
const effectivelyDisconnected = disconnected || error === "Not connected";
213216

214217
return (
215218
<aside
@@ -256,7 +259,11 @@ export function AccessibilityInspector({
256259
<PerformanceIcon />
257260
</button>
258261
</div>
259-
{activeTab === "console" ? (
262+
{effectivelyDisconnected ? (
263+
<div className="hierarchy-tree">
264+
<div className="hierarchy-empty">Not connected</div>
265+
</div>
266+
) : activeTab === "console" ? (
260267
<ConsolePanel
261268
accessibilityRoots={roots}
262269
selectedSimulator={selectedSimulator}

client/src/features/devtools/DevToolsPanel.tsx

Lines changed: 146 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@ const DEVTOOLS_PANEL_DEFAULT_WIDTH = 720;
3838
const DEVTOOLS_PANEL_MIN_WIDTH = 420;
3939
const DEVTOOLS_PANEL_MIN_VIEWPORT_WIDTH = 340;
4040
const DEVTOOLS_PANEL_WIDTH_STEP = 40;
41+
const NOT_CONNECTED_MESSAGE = "Not connected";
4142

4243
interface DevToolsPanelProps {
44+
disconnected: boolean;
4345
onClose: () => void;
4446
overviewRequestKey: number;
4547
selectedSimulator: SimulatorMetadata | null;
@@ -73,8 +75,16 @@ interface DevToolsDiscovery {
7375
type ChromeDiscoveryResult =
7476
PromiseSettledResult<ChromeDevToolsTargetDiscovery>;
7577
type WebKitDiscoveryResult = PromiseSettledResult<WebKitTargetDiscovery>;
78+
type WebKitSocketState =
79+
| ""
80+
| "connecting"
81+
| "connected"
82+
| "reconnecting"
83+
| "disconnected"
84+
| "failed";
7685

7786
export function DevToolsPanel({
87+
disconnected,
7888
onClose,
7989
overviewRequestKey,
8090
selectedSimulator,
@@ -89,6 +99,8 @@ export function DevToolsPanel({
8999
const [error, setError] = useState("");
90100
const [frameLoaded, setFrameLoaded] = useState(false);
91101
const [overviewVisible, setOverviewVisible] = useState(false);
102+
const [webKitSocketState, setWebKitSocketState] =
103+
useState<WebKitSocketState>("");
92104
const discoveryRef = useRef<DevToolsDiscovery | null>(null);
93105
const frameRef = useRef<HTMLIFrameElement | null>(null);
94106
const loadingTargetsRef = useRef(false);
@@ -142,6 +154,15 @@ export function DevToolsPanel({
142154
return;
143155
}
144156

157+
if (disconnected) {
158+
applyDiscovery(null);
159+
applySelectedTargetId("");
160+
setError("");
161+
setIsLoading(false);
162+
setIsWebKitLoading(false);
163+
return;
164+
}
165+
145166
if (!selectedSimulator) {
146167
applyDiscovery(null);
147168
applySelectedTargetId("");
@@ -160,7 +181,7 @@ export function DevToolsPanel({
160181
}
161182
const requestId = ++requestIdRef.current;
162183
setIsLoading(true);
163-
setError("");
184+
setError((current) => (current === NOT_CONNECTED_MESSAGE ? current : ""));
164185
try {
165186
const chromeTargets = requestWithTimeout(
166187
(signal) =>
@@ -350,6 +371,7 @@ export function DevToolsPanel({
350371
}, [
351372
applyDiscovery,
352373
applySelectedTargetId,
374+
disconnected,
353375
selectedSimulator?.isBooted,
354376
selectedSimulator?.udid,
355377
]);
@@ -369,8 +391,24 @@ export function DevToolsPanel({
369391
setIsLoading(false);
370392
setIsWebKitLoading(false);
371393
setOverviewVisible(false);
394+
setWebKitSocketState("");
372395
}, [applyDiscovery, applySelectedTargetId, selectedSimulator?.udid]);
373396

397+
useEffect(() => {
398+
if (!disconnected) {
399+
return;
400+
}
401+
requestIdRef.current += 1;
402+
applyDiscovery(null);
403+
applySelectedTargetId("");
404+
setError("");
405+
setFrameLoaded(false);
406+
setIsLoading(false);
407+
setIsWebKitLoading(false);
408+
setOverviewVisible(false);
409+
setWebKitSocketState("");
410+
}, [applyDiscovery, applySelectedTargetId, disconnected]);
411+
374412
useEffect(() => {
375413
if (!visible) {
376414
return;
@@ -384,8 +422,38 @@ export function DevToolsPanel({
384422

385423
useEffect(() => {
386424
setFrameLoaded(false);
425+
setWebKitSocketState("");
387426
}, [frameUrl]);
388427

428+
useEffect(() => {
429+
function handleWebKitSocketState(event: MessageEvent) {
430+
if (frameRef.current?.contentWindow !== event.source) {
431+
return;
432+
}
433+
const data = event.data;
434+
if (
435+
!data ||
436+
typeof data !== "object" ||
437+
data.type !== "simdeck:webkit-inspector:socket"
438+
) {
439+
return;
440+
}
441+
const state = data.state;
442+
if (
443+
state === "connecting" ||
444+
state === "connected" ||
445+
state === "reconnecting" ||
446+
state === "disconnected" ||
447+
state === "failed"
448+
) {
449+
setWebKitSocketState(state);
450+
}
451+
}
452+
453+
window.addEventListener("message", handleWebKitSocketState);
454+
return () => window.removeEventListener("message", handleWebKitSocketState);
455+
}, []);
456+
389457
useEffect(() => {
390458
if (overviewRequestKey <= 0) {
391459
return;
@@ -518,20 +586,34 @@ export function DevToolsPanel({
518586
}
519587

520588
const isDiscoveringTargets = isLoading || isWebKitLoading;
521-
const statusMessage =
522-
error ||
523-
(!selectedSimulator
524-
? "No simulator selected."
525-
: isDiscoveringTargets && targets.length === 0
526-
? "Loading..."
527-
: targets.length === 0
528-
? selectedSimulator.isBooted
529-
? "No DevTools targets. Open Safari, enable inspectable WKWebViews, start Metro, or launch a Chrome remote debugging target."
530-
: "No DevTools targets. Boot the simulator for Safari/WebKit, or start Metro or Chrome remote debugging."
531-
: "");
532-
const emptyOverviewMessage = isDiscoveringTargets
533-
? "Loading..."
534-
: "No targets";
589+
const effectivelyDisconnected =
590+
disconnected || error === NOT_CONNECTED_MESSAGE;
591+
const chromeDevToolsBlocked = Boolean(
592+
selectedTarget && isChromeTarget(selectedTarget) && isSafariBrowser(),
593+
);
594+
const webKitConnectionMessage =
595+
selectedTarget && isWebKitTarget(selectedTarget)
596+
? webKitSocketStatusMessage(webKitSocketState)
597+
: "";
598+
const statusMessage = effectivelyDisconnected
599+
? NOT_CONNECTED_MESSAGE
600+
: chromeDevToolsBlocked
601+
? "Chrome DevTools don't work in Safari"
602+
: error ||
603+
(!selectedSimulator
604+
? "No simulator selected."
605+
: isDiscoveringTargets && targets.length === 0
606+
? "Loading..."
607+
: targets.length === 0
608+
? selectedSimulator.isBooted
609+
? "No DevTools targets. Open Safari, enable inspectable WKWebViews, start Metro, or launch a Chrome remote debugging target."
610+
: "No DevTools targets. Boot the simulator for Safari/WebKit, or start Metro or Chrome remote debugging."
611+
: "");
612+
const emptyOverviewMessage = effectivelyDisconnected
613+
? NOT_CONNECTED_MESSAGE
614+
: isDiscoveringTargets
615+
? "Loading..."
616+
: "No targets";
535617
const panelStyle = {
536618
"--webkit-panel-width": `${panelWidth}px`,
537619
} as CSSProperties;
@@ -618,7 +700,11 @@ export function DevToolsPanel({
618700
) : null}
619701

620702
<div className="webkit-frame-wrap">
621-
{overviewVisible ? (
703+
{effectivelyDisconnected || chromeDevToolsBlocked ? (
704+
<div className={`webkit-status ${error ? "error" : ""}`}>
705+
{statusMessage}
706+
</div>
707+
) : overviewVisible ? (
622708
<DevToolsOverview
623709
emptyMessage={emptyOverviewMessage}
624710
targets={targets}
@@ -638,6 +724,10 @@ export function DevToolsPanel({
638724
<div className="webkit-status" role="status">
639725
Loading...
640726
</div>
727+
) : webKitConnectionMessage ? (
728+
<div className="webkit-status" role="status">
729+
{webKitConnectionMessage}
730+
</div>
641731
) : null}
642732
</>
643733
) : (
@@ -830,6 +920,31 @@ function isWebKitTarget(target: DevToolsTarget): boolean {
830920
);
831921
}
832922

923+
function isSafariBrowser(): boolean {
924+
if (typeof navigator === "undefined") {
925+
return false;
926+
}
927+
const userAgent = navigator.userAgent;
928+
return (
929+
/safari/i.test(userAgent) &&
930+
!/chrome|chromium|crios|fxios|edg/i.test(userAgent)
931+
);
932+
}
933+
934+
function webKitSocketStatusMessage(state: WebKitSocketState): string {
935+
switch (state) {
936+
case "connecting":
937+
return "Connecting...";
938+
case "reconnecting":
939+
case "failed":
940+
return "Reconnecting...";
941+
case "disconnected":
942+
return "Not connected";
943+
default:
944+
return "";
945+
}
946+
}
947+
833948
function readStoredPanelWidth(): number {
834949
if (typeof window === "undefined") {
835950
return DEVTOOLS_PANEL_DEFAULT_WIDTH;
@@ -1037,6 +1152,9 @@ function userFacingDevToolsMessage(message: string): string {
10371152
}
10381153

10391154
const lower = normalized.toLowerCase();
1155+
if (isProviderDisconnectedMessage(normalized)) {
1156+
return NOT_CONNECTED_MESSAGE;
1157+
}
10401158
if (
10411159
lower.includes("no app inspector found") ||
10421160
lower.includes("no connected websocket inspector found") ||
@@ -1058,6 +1176,18 @@ function userFacingDevToolsMessage(message: string): string {
10581176
return normalized;
10591177
}
10601178

1179+
function isProviderDisconnectedMessage(message: string): boolean {
1180+
const lower = message.trim().toLowerCase();
1181+
return (
1182+
lower.includes("failed to fetch") ||
1183+
lower.includes("load failed") ||
1184+
lower.includes("networkerror") ||
1185+
lower.includes("network error") ||
1186+
lower.includes("timed out loading chrome devtools targets") ||
1187+
lower.includes("timed out loading webkit targets")
1188+
);
1189+
}
1190+
10611191
function cleanDevToolsMessages(messages: string[]): string[] {
10621192
return messages.flatMap((message) => {
10631193
const nextMessage = userFacingDevToolsMessage(message);

0 commit comments

Comments
 (0)