1import { useEffect, useState, type ReactNode } from "react";
2import { View } from "react-native";
3
4import { RenderErrorBoundary, RenderFailureView } from "../../components/RenderErrorBoundary";
5
6export type ThreadInspectorMode = "route" | "git" | "files";
7
8const INSPECTOR_PREWARM_DELAY_MS = 350;
9
10function InspectorContentPane(props: {
11 readonly children: ReactNode;
12 readonly mounted: boolean;
13 readonly resetKeys: readonly [string | null, string | null];
14 readonly visible: boolean;
15}) {
16 if (!props.mounted) {
17 return null;
18 }
19
20 return (
21 <View
22 accessibilityElementsHidden={!props.visible}
23 focusable={props.visible}
24 importantForAccessibility={props.visible ? "auto" : "no-hide-descendants"}
25 pointerEvents={props.visible ? "auto" : "none"}
26 style={{
27 position: "absolute",
28 inset: 0,
29 opacity: props.visible ? 1 : 0,
30 zIndex: props.visible ? 1 : 0,
31 }}
32 >
33 <RenderErrorBoundary
34 resetKeys={props.resetKeys}
35 renderFallback={(fallback) => (
36 <RenderFailureView {...fallback} title="The inspector couldn't be displayed" />
37 )}
38 >
39 {props.children}
40 </RenderErrorBoundary>
41 </View>
42 );
43}
44
45export function ThreadInspectorContentStack(props: {
46 readonly renderFiles: () => ReactNode;
47 readonly renderGit?: () => ReactNode;
48 readonly mode: ThreadInspectorMode;
49 readonly resetKeys: readonly [string | null, string | null];
50 readonly renderRoute?: () => ReactNode;
51}) {
52 const [mountedModes, setMountedModes] = useState<ReadonlySet<ThreadInspectorMode>>(
53 () => new Set([props.mode]),
54 );
55
56 useEffect(() => {
57 setMountedModes((current) => {
58 if (current.has(props.mode)) {
59 return current;
60 }
61 return new Set([...current, props.mode]);
62 });
63
64 if (props.mode === "route") {
65 return;
66 }
67
68 // The file tree is expensive to detach because UIKit rebuilds its focus
69 // graph. Keep both chat inspectors alive after the opening animation so a
70 // later Files/Git switch only changes visibility.
71 const alternateMode = props.mode === "files" ? "git" : "files";
72 const timeout = setTimeout(() => {
73 setMountedModes((current) => {
74 if (current.has(alternateMode)) {
75 return current;
76 }
77 return new Set([...current, alternateMode]);
78 });
79 }, INSPECTOR_PREWARM_DELAY_MS);
80
81 return () => clearTimeout(timeout);
82 }, [props.mode]);
83
84 return (
85 <View className="flex-1">
86 <InspectorContentPane
87 mounted={mountedModes.has("files") || props.mode === "files"}
88 resetKeys={props.resetKeys}
89 visible={props.mode === "files"}
90 >
91 <InspectorRenderer render={props.renderFiles} />
92 </InspectorContentPane>
93 {props.renderGit ? (
94 <InspectorContentPane
95 mounted={mountedModes.has("git") || props.mode === "git"}
96 resetKeys={props.resetKeys}
97 visible={props.mode === "git"}
98 >
99 <InspectorRenderer render={props.renderGit} />
100 </InspectorContentPane>
101 ) : null}
102 {props.renderRoute ? (
103 <InspectorContentPane
104 mounted={mountedModes.has("route") || props.mode === "route"}
105 resetKeys={props.resetKeys}
106 visible={props.mode === "route"}
107 >
108 <InspectorRenderer render={props.renderRoute} />
109 </InspectorContentPane>
110 ) : null}
111 </View>
112 );
113}
114
115// Render callbacks carry changing route data; they are not component types.
116function InspectorRenderer(props: { readonly render: () => ReactNode }) {
117 return <>{props.render()}</>;
118}