| author | |
| committer | |
| log | 23c7ab901afa66dc8e791a3873b600726a6ba8c7 |
| tree | 5dda066ac437f3cb10409391cebe006314b3ad5d |
| parent | 10882bea739a98f11b942c20c6570eb6126dd010 |
| signature | Signed by PGP key B5690EEEBB952194 |
7 files changed, 281 insertions(+), 55 deletions(-)
apps/mobile/src/Stack.tsx+41-5| ... | ... | @@ -10,7 +10,7 @@ import { |
| 10 | 10 | createNativeStackScreen, |
| 11 | 11 | type NativeStackNavigationOptions, |
| 12 | 12 | } from "@react-navigation/native-stack"; |
| 13 | import { useEffect, useRef } from "react"; | |
| 13 | import { useEffect, useRef, type ReactNode } from "react"; | |
| 14 | 14 | import { |
| 15 | 15 | Platform, |
| 16 | 16 | Pressable, |
| ... | ... | @@ -23,6 +23,11 @@ import { useResolveClassNames } from "uniwind"; |
| 23 | 23 | |
| 24 | 24 | import { AppText as Text } from "./components/AppText"; |
| 25 | 25 | import { getCompactBrandHeaderOptions } from "./components/CompactBrandTitle"; |
| 26 | import { | |
| 27 | RenderErrorBoundary, | |
| 28 | RenderFailureView, | |
| 29 | type RenderFailureProps, | |
| 30 | } from "./components/RenderErrorBoundary"; | |
| 26 | 31 | import { ArchivedThreadsRouteScreen } from "./features/archive/ArchivedThreadsRouteScreen"; |
| 27 | 32 | import { useAgentNotificationNavigation } from "./features/agent-awareness/notificationNavigation"; |
| 28 | 33 | import { ConnectOnboardingRouteScreen } from "./features/cloud/ConnectOnboardingRouteScreen"; |
| ... | ... | @@ -777,10 +782,13 @@ const RootStackConfig = createNativeStackNavigator({ |
| 777 | 782 | // The whole new-task flow (choose project → draft → add project) shares |
| 778 | 783 | // draft state via NewTaskFlowProvider. The expo-router era mounted it in |
| 779 | 784 | // app/new/_layout.tsx; this layout wrapper is the native-stack equivalent. |
| 780 | layout: ({ children }) => ( | |
| 781 | <NewTaskFlowProvider> | |
| 782 | <View className="flex-1 bg-sheet-solid">{children}</View> | |
| 783 | </NewTaskFlowProvider> | |
| 785 | // A screen's layout replaces the navigator's screenLayout. | |
| 786 | layout: ({ children, route }) => ( | |
| 787 | <GuardedScreenLayout route={route}> | |
| 788 | <NewTaskFlowProvider> | |
| 789 | <View className="flex-1 bg-sheet-solid">{children}</View> | |
| 790 | </NewTaskFlowProvider> | |
| 791 | </GuardedScreenLayout> | |
| 784 | 792 | ), |
| 785 | 793 | options: { |
| 786 | 794 | gestureEnabled: true, |
| ... | ... | @@ -794,6 +802,33 @@ const RootStackConfig = createNativeStackNavigator({ |
| 794 | 802 | }, |
| 795 | 803 | }); |
| 796 | 804 | |
| 805 | function GuardedScreenLayout(props: { | |
| 806 | readonly children: ReactNode; | |
| 807 | readonly route: { readonly name: string; readonly params?: object | undefined }; | |
| 808 | }) { | |
| 809 | return ( | |
| 810 | <RenderErrorBoundary | |
| 811 | resetKeys={[props.route.params]} | |
| 812 | renderFallback={(fallback) => ( | |
| 813 | <ScreenRenderFallback {...fallback} routeName={props.route.name} /> | |
| 814 | )} | |
| 815 | > | |
| 816 | {props.children} | |
| 817 | </RenderErrorBoundary> | |
| 818 | ); | |
| 819 | } | |
| 820 | ||
| 821 | function ScreenRenderFallback(props: RenderFailureProps & { readonly routeName: string }) { | |
| 822 | const navigation = useNavigation(); | |
| 823 | const exit = navigation.canGoBack() | |
| 824 | ? { label: "Go back", onPress: () => navigation.goBack() } | |
| 825 | : props.routeName === "Home" | |
| 826 | ? { label: "Open settings", onPress: () => navigation.navigate("SettingsSheet") } | |
| 827 | : { label: "Return home", onPress: () => navigation.dispatch(StackActions.replace("Home")) }; | |
| 828 | ||
| 829 | return <RenderFailureView {...props} exit={exit} />; | |
| 830 | } | |
| 831 | ||
| 797 | 832 | export const RootStack = RootStackConfig.with(function AdaptiveRootStack({ Navigator }) { |
| 798 | 833 | const { width, height } = useWindowDimensions(); |
| 799 | 834 | const usesWorkspaceFlowScreens = |
| ... | ... | @@ -801,6 +836,7 @@ export const RootStack = RootStackConfig.with(function AdaptiveRootStack({ Navig |
| 801 | 836 | |
| 802 | 837 | return ( |
| 803 | 838 | <Navigator |
| 839 | screenLayout={GuardedScreenLayout} | |
| 804 | 840 | screenOptions={({ route }) => { |
| 805 | 841 | if (route.name !== "SettingsSheet" && route.name !== "NewTaskSheet") { |
| 806 | 842 | return {}; |
apps/mobile/src/components/RenderErrorBoundary.tsx created+127| ... | ... | @@ -0,0 +1,127 @@ |
| 1 | import { Component, type ReactNode } from "react"; | |
| 2 | import { ScrollView, View } from "react-native"; | |
| 3 | ||
| 4 | import { AppText as Text } from "./AppText"; | |
| 5 | import { MaterialButton } from "./MaterialButton"; | |
| 6 | import { copyTextWithHaptic } from "../lib/copyTextWithHaptic"; | |
| 7 | ||
| 8 | interface RenderErrorBoundaryProps { | |
| 9 | readonly children: ReactNode; | |
| 10 | readonly resetKeys?: ReadonlyArray<unknown>; | |
| 11 | readonly renderFallback?: (props: RenderFailureProps) => ReactNode; | |
| 12 | } | |
| 13 | ||
| 14 | interface RenderErrorBoundaryState { | |
| 15 | readonly failed: boolean; | |
| 16 | readonly error: unknown; | |
| 17 | readonly componentStack?: string; | |
| 18 | readonly resetKeys?: ReadonlyArray<unknown>; | |
| 19 | } | |
| 20 | ||
| 21 | export interface RenderFailureProps { | |
| 22 | readonly details: string; | |
| 23 | readonly retry: () => void; | |
| 24 | } | |
| 25 | ||
| 26 | function errorDetails(error: unknown, componentStack?: string): string { | |
| 27 | let description: string; | |
| 28 | try { | |
| 29 | description = error instanceof Error ? (error.stack ?? error.message) : String(error); | |
| 30 | } catch { | |
| 31 | description = "Unknown render error"; | |
| 32 | } | |
| 33 | return componentStack ? `${description}\nComponent stack:\n${componentStack}` : description; | |
| 34 | } | |
| 35 | ||
| 36 | /** A failed subtree remounts on retry or when its identifying inputs change. */ | |
| 37 | export class RenderErrorBoundary extends Component< | |
| 38 | RenderErrorBoundaryProps, | |
| 39 | RenderErrorBoundaryState | |
| 40 | > { | |
| 41 | override state: RenderErrorBoundaryState = { | |
| 42 | failed: false, | |
| 43 | error: null, | |
| 44 | resetKeys: this.props.resetKeys, | |
| 45 | }; | |
| 46 | ||
| 47 | static getDerivedStateFromProps( | |
| 48 | { resetKeys }: RenderErrorBoundaryProps, | |
| 49 | state: RenderErrorBoundaryState, | |
| 50 | ): Partial<RenderErrorBoundaryState> | null { | |
| 51 | if ( | |
| 52 | resetKeys?.length !== state.resetKeys?.length || | |
| 53 | resetKeys?.some((key, index) => !Object.is(key, state.resetKeys?.[index])) | |
| 54 | ) { | |
| 55 | return { failed: false, error: null, componentStack: undefined, resetKeys }; | |
| 56 | } | |
| 57 | return null; | |
| 58 | } | |
| 59 | ||
| 60 | static getDerivedStateFromError(error: unknown): Partial<RenderErrorBoundaryState> { | |
| 61 | return { failed: true, error }; | |
| 62 | } | |
| 63 | ||
| 64 | override componentDidCatch(_error: unknown, info: { componentStack?: string }) { | |
| 65 | this.setState({ componentStack: info.componentStack }); | |
| 66 | } | |
| 67 | ||
| 68 | private readonly retry = () => { | |
| 69 | this.setState({ failed: false, error: null, componentStack: undefined }); | |
| 70 | }; | |
| 71 | ||
| 72 | override render() { | |
| 73 | if (!this.state.failed) return this.props.children; | |
| 74 | const fallback = | |
| 75 | this.props.renderFallback ?? | |
| 76 | ((props: RenderFailureProps) => <RenderFailureView {...props} />); | |
| 77 | return fallback({ | |
| 78 | details: errorDetails(this.state.error, this.state.componentStack), | |
| 79 | retry: this.retry, | |
| 80 | }); | |
| 81 | } | |
| 82 | } | |
| 83 | ||
| 84 | export function RenderFailureView( | |
| 85 | props: RenderFailureProps & { | |
| 86 | readonly title?: string; | |
| 87 | readonly bottomInset?: number; | |
| 88 | readonly exit?: { readonly label: string; readonly onPress: () => void }; | |
| 89 | }, | |
| 90 | ) { | |
| 91 | const title = props.title ?? "This screen couldn't be displayed"; | |
| 92 | return ( | |
| 93 | <ScrollView | |
| 94 | className="flex-1 bg-screen" | |
| 95 | contentContainerClassName="flex-grow items-center justify-center gap-5 px-6 py-8" | |
| 96 | contentContainerStyle={ | |
| 97 | props.bottomInset ? { paddingBottom: 32 + props.bottomInset } : undefined | |
| 98 | } | |
| 99 | > | |
| 100 | <Text accessibilityRole="header" className="text-center text-xl font-t3-bold"> | |
| 101 | {title} | |
| 102 | </Text> | |
| 103 | <Text className="text-center text-sm text-foreground-muted"> | |
| 104 | Try again. If it keeps happening, copy the details for a bug report. | |
| 105 | </Text> | |
| 106 | <Text selectable className="text-center font-mono text-xs text-danger-foreground"> | |
| 107 | {props.details.split("\n", 1)[0]?.slice(0, 300)} | |
| 108 | </Text> | |
| 109 | <View className="w-full max-w-xs gap-2"> | |
| 110 | <MaterialButton label="Try again" onPress={props.retry} tone="primary" fullWidth /> | |
| 111 | <MaterialButton | |
| 112 | label="Copy details" | |
| 113 | onPress={() => copyTextWithHaptic(props.details, { target: "error details" })} | |
| 114 | fullWidth | |
| 115 | /> | |
| 116 | {props.exit ? ( | |
| 117 | <MaterialButton | |
| 118 | label={props.exit.label} | |
| 119 | onPress={props.exit.onPress} | |
| 120 | tone="text" | |
| 121 | fullWidth | |
| 122 | /> | |
| 123 | ) : null} | |
| 124 | </View> | |
| 125 | </ScrollView> | |
| 126 | ); | |
| 127 | } |
apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx+27-15| ... | ... | @@ -57,6 +57,7 @@ import { |
| 57 | 57 | import { AndroidHomeFabLayout } from "../home/AndroidHomeFab"; |
| 58 | 58 | import { HomeListOptionsProvider } from "../home/home-list-options"; |
| 59 | 59 | import { ThreadNavigationSidebar } from "../threads/ThreadNavigationSidebar"; |
| 60 | import { RenderErrorBoundary, RenderFailureView } from "../../components/RenderErrorBoundary"; | |
| 60 | 61 | import { WORKSPACE_PANE_TIMING } from "./workspace-pane-animation"; |
| 61 | 62 | import { WorkspaceInspectorPane } from "./workspace-inspector-pane"; |
| 62 | 63 | import { WorkspaceContentWidthContext } from "./workspace-content-width"; |
| ... | ... | @@ -580,21 +581,31 @@ function AdaptiveWorkspaceLayoutContent( |
| 580 | 581 | style={sidebarAnimatedStyle} |
| 581 | 582 | > |
| 582 | 583 | <View className="flex-1" style={{ width: layout.listPaneWidth }}> |
| 583 | <AndroidHomeFabLayout sidebar onStartNewTask={handleStartNewTask}> | |
| 584 | <ThreadNavigationSidebar | |
| 585 | width={layout.listPaneWidth} | |
| 586 | visible={panes.primarySidebarVisible} | |
| 587 | onRequestVisibility={revealPrimarySidebar} | |
| 588 | selectedThreadKey={selectedThreadKey} | |
| 589 | onOpenSettings={handleOpenSettings} | |
| 590 | onOpenEnvironmentSettings={handleOpenEnvironmentSettings} | |
| 591 | onNewThreadInProject={handleNewThreadInProject} | |
| 592 | onNewThreadOnBranch={handleNewThreadOnBranch} | |
| 593 | onSelectThread={handleSelectThread} | |
| 594 | onSearchQueryChange={setPrimarySidebarSearchQuery} | |
| 595 | searchQuery={primarySidebarSearchQuery} | |
| 596 | /> | |
| 597 | </AndroidHomeFabLayout> | |
| 584 | <RenderErrorBoundary | |
| 585 | renderFallback={(fallback) => ( | |
| 586 | <RenderFailureView | |
| 587 | {...fallback} | |
| 588 | title="The sidebar couldn't be displayed" | |
| 589 | exit={{ label: "Open settings", onPress: handleOpenSettings }} | |
| 590 | /> | |
| 591 | )} | |
| 592 | > | |
| 593 | <AndroidHomeFabLayout sidebar onStartNewTask={handleStartNewTask}> | |
| 594 | <ThreadNavigationSidebar | |
| 595 | width={layout.listPaneWidth} | |
| 596 | visible={panes.primarySidebarVisible} | |
| 597 | onRequestVisibility={revealPrimarySidebar} | |
| 598 | selectedThreadKey={selectedThreadKey} | |
| 599 | onOpenSettings={handleOpenSettings} | |
| 600 | onOpenEnvironmentSettings={handleOpenEnvironmentSettings} | |
| 601 | onNewThreadInProject={handleNewThreadInProject} | |
| 602 | onNewThreadOnBranch={handleNewThreadOnBranch} | |
| 603 | onSelectThread={handleSelectThread} | |
| 604 | onSearchQueryChange={setPrimarySidebarSearchQuery} | |
| 605 | searchQuery={primarySidebarSearchQuery} | |
| 606 | /> | |
| 607 | </AndroidHomeFabLayout> | |
| 608 | </RenderErrorBoundary> | |
| 598 | 609 | </View> |
| 599 | 610 | </Animated.View> |
| 600 | 611 | ) : null} |
| ... | ... | @@ -625,6 +636,7 @@ function AdaptiveWorkspaceLayoutContent( |
| 625 | 636 | </View> |
| 626 | 637 | </View> |
| 627 | 638 | <WorkspaceInspectorPane |
| 639 | pathname={props.pathname} | |
| 628 | 640 | renderedInspectorWidth={renderedInspectorWidth} |
| 629 | 641 | active={workspaceInspector?.active ?? false} |
| 630 | 642 | panes={panes} |
apps/mobile/src/features/layout/workspace-inspector-pane.tsx+16-1| ... | ... | @@ -8,6 +8,7 @@ import Animated, { |
| 8 | 8 | } from "react-native-reanimated"; |
| 9 | 9 | |
| 10 | 10 | import { constrainAuxiliaryPaneWidth, type WorkspacePaneLayout } from "../../lib/layout"; |
| 11 | import { RenderErrorBoundary, RenderFailureView } from "../../components/RenderErrorBoundary"; | |
| 11 | 12 | import { WORKSPACE_PANE_TIMING } from "./workspace-pane-animation"; |
| 12 | 13 | import { WorkspacePaneDivider } from "./workspace-pane-divider"; |
| 13 | 14 | |
| ... | ... | @@ -23,6 +24,7 @@ import { WorkspacePaneDivider } from "./workspace-pane-divider"; |
| 23 | 24 | * module stays import-cycle-free with AdaptiveWorkspaceLayout. |
| 24 | 25 | */ |
| 25 | 26 | export function WorkspaceInspectorPane(props: { |
| 27 | readonly pathname: string; | |
| 26 | 28 | readonly renderedInspectorWidth: SharedValue<number>; |
| 27 | 29 | /** |
| 28 | 30 | * When false the pane animates closed but keeps its content mounted for the |
| ... | ... | @@ -139,10 +141,23 @@ export function WorkspaceInspectorPane(props: { |
| 139 | 141 | style={inspectorStyle} |
| 140 | 142 | > |
| 141 | 143 | <Animated.View className="flex-1" style={inspectorContentStyle}> |
| 142 | {props.renderInspector?.()} | |
| 144 | <RenderErrorBoundary | |
| 145 | resetKeys={[props.pathname]} | |
| 146 | renderFallback={(fallback) => ( | |
| 147 | <RenderFailureView {...fallback} title="The inspector couldn't be displayed" /> | |
| 148 | )} | |
| 149 | > | |
| 150 | <InspectorRenderer render={props.renderInspector} /> | |
| 151 | </RenderErrorBoundary> | |
| 143 | 152 | </Animated.View> |
| 144 | 153 | </Animated.View> |
| 145 | 154 | ) : null} |
| 146 | 155 | </> |
| 147 | 156 | ); |
| 148 | 157 | } |
| 158 | ||
| 159 | // The render callback must run inside the boundary's child, not while its | |
| 160 | // parent constructs the boundary element. | |
| 161 | function InspectorRenderer(props: { readonly render?: () => ReactNode }) { | |
| 162 | return <>{props.render?.()}</>; | |
| 163 | } |
apps/mobile/src/features/threads/ThreadDetailScreen.tsx+45-32| ... | ... | @@ -70,6 +70,7 @@ import { collectProviderUsageLimits } from "@t3tools/shared/usageLimits"; |
| 70 | 70 | import type { ComposerEditorHandle } from "../../components/ComposerEditor"; |
| 71 | 71 | import type { StatusTone } from "../../components/StatusPill"; |
| 72 | 72 | import type { DraftComposerAttachment } from "../../lib/composerImages"; |
| 73 | import { RenderErrorBoundary, RenderFailureView } from "../../components/RenderErrorBoundary"; | |
| 73 | 74 | import { CHAT_CONTENT_MAX_WIDTH, type LayoutVariant } from "../../lib/layout"; |
| 74 | 75 | import { IOS_NAV_BAR_HEIGHT } from "../../lib/layoutMetrics"; |
| 75 | 76 | import { editPendingThreadMessage } from "../../state/edit-pending-thread-message"; |
| ... | ... | @@ -892,39 +893,51 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread |
| 892 | 893 | : "absolute inset-0 bg-screen" |
| 893 | 894 | } |
| 894 | 895 | /> |
| 895 | <ThreadFeed | |
| 896 | <RenderErrorBoundary | |
| 896 | 897 | key={selectedThreadKey} |
| 897 | environmentId={props.environmentId} | |
| 898 | threadId={props.selectedThread.id} | |
| 899 | workspaceRoot={props.threadCwd} | |
| 900 | feed={props.selectedThreadFeed} | |
| 901 | worktreeSetup={props.worktreeSetup} | |
| 902 | setupWorkingStartedAt={props.setupWorkingStartedAt} | |
| 903 | queuedMessages={props.queuedMessages} | |
| 904 | dispatchingMessageId={props.dispatchingMessageId} | |
| 905 | onEditPendingMessage={handleEditPendingMessage} | |
| 906 | contentPresentation={props.contentPresentation} | |
| 907 | agentLabel={agentLabel} | |
| 908 | latestTurn={props.selectedThread.latestTurn} | |
| 909 | activeWorkStartedAt={props.activeWorkStartedAt} | |
| 910 | listRef={listRef} | |
| 911 | freeze={freeze} | |
| 912 | anchorMessageId={anchorMessageId} | |
| 913 | submittedMessageId={submittedMessageId} | |
| 914 | contentInsetEndAdjustment={combinedContentInsetEndAdjustment} | |
| 915 | contentTopInset={0} | |
| 916 | contentBottomInset={ | |
| 917 | estimatedOverlayHeight + (showFloatingStatus ? FLOATING_WORKING_CONTROL_COVERAGE : 0) | |
| 918 | } | |
| 919 | contentMaxWidth={contentMaxWidth} | |
| 920 | layoutVariant={layoutVariant} | |
| 921 | usesAutomaticContentInsets={props.usesAutomaticContentInsets} | |
| 922 | onHeaderMaterialVisibilityChange={props.onHeaderMaterialVisibilityChange} | |
| 923 | onEndFollowEnabledChange={setEndFollowEnabled} | |
| 924 | skills={selectedProviderSkills} | |
| 925 | onUseArtifactTemplate={handleUseArtifactTemplate} | |
| 926 | loadEarlier={props.loadEarlier ?? null} | |
| 927 | /> | |
| 898 | resetKeys={[props.threadCwd]} | |
| 899 | renderFallback={(fallback) => ( | |
| 900 | <RenderFailureView | |
| 901 | {...fallback} | |
| 902 | title="The conversation couldn't be displayed" | |
| 903 | bottomInset={estimatedOverlayHeight} | |
| 904 | /> | |
| 905 | )} | |
| 906 | > | |
| 907 | <ThreadFeed | |
| 908 | environmentId={props.environmentId} | |
| 909 | threadId={props.selectedThread.id} | |
| 910 | workspaceRoot={props.threadCwd} | |
| 911 | feed={props.selectedThreadFeed} | |
| 912 | worktreeSetup={props.worktreeSetup} | |
| 913 | setupWorkingStartedAt={props.setupWorkingStartedAt} | |
| 914 | queuedMessages={props.queuedMessages} | |
| 915 | dispatchingMessageId={props.dispatchingMessageId} | |
| 916 | onEditPendingMessage={handleEditPendingMessage} | |
| 917 | contentPresentation={props.contentPresentation} | |
| 918 | agentLabel={agentLabel} | |
| 919 | latestTurn={props.selectedThread.latestTurn} | |
| 920 | activeWorkStartedAt={props.activeWorkStartedAt} | |
| 921 | listRef={listRef} | |
| 922 | freeze={freeze} | |
| 923 | anchorMessageId={anchorMessageId} | |
| 924 | submittedMessageId={submittedMessageId} | |
| 925 | contentInsetEndAdjustment={combinedContentInsetEndAdjustment} | |
| 926 | contentTopInset={0} | |
| 927 | contentBottomInset={ | |
| 928 | estimatedOverlayHeight + | |
| 929 | (showFloatingStatus ? FLOATING_WORKING_CONTROL_COVERAGE : 0) | |
| 930 | } | |
| 931 | contentMaxWidth={contentMaxWidth} | |
| 932 | layoutVariant={layoutVariant} | |
| 933 | usesAutomaticContentInsets={props.usesAutomaticContentInsets} | |
| 934 | onHeaderMaterialVisibilityChange={props.onHeaderMaterialVisibilityChange} | |
| 935 | onEndFollowEnabledChange={setEndFollowEnabled} | |
| 936 | skills={selectedProviderSkills} | |
| 937 | onUseArtifactTemplate={handleUseArtifactTemplate} | |
| 938 | loadEarlier={props.loadEarlier ?? null} | |
| 939 | /> | |
| 940 | </RenderErrorBoundary> | |
| 928 | 941 | </View> |
| 929 | 942 | ) : ( |
| 930 | 943 | <View className="flex-1" /> |
apps/mobile/src/features/threads/ThreadRouteScreen.tsx+10-1| ... | ... | @@ -606,10 +606,19 @@ function ThreadRouteContent( |
| 606 | 606 | Files={FilesInspector} |
| 607 | 607 | Git={GitInspector} |
| 608 | 608 | mode={inspectorMode} |
| 609 | resetKeys={[routeThreadIdentity, selectedThreadCwd]} | |
| 609 | 610 | Route={props.renderInspector ? RouteInspector : undefined} |
| 610 | 611 | /> |
| 611 | 612 | ), |
| 612 | [FilesInspector, GitInspector, RouteInspector, inspectorMode, props.renderInspector], | |
| 613 | [ | |
| 614 | FilesInspector, | |
| 615 | GitInspector, | |
| 616 | RouteInspector, | |
| 617 | inspectorMode, | |
| 618 | props.renderInspector, | |
| 619 | routeThreadIdentity, | |
| 620 | selectedThreadCwd, | |
| 621 | ], | |
| 613 | 622 | ); |
| 614 | 623 | const activeInspectorRenderer = inspectorMode === null ? undefined : renderInspectorStack; |
| 615 | 624 | // Hand the inspector to the workspace so it renders beside the navigator, |
apps/mobile/src/features/threads/thread-inspector-content-stack.tsx+15-1| ... | ... | @@ -1,6 +1,8 @@ |
| 1 | 1 | import { useEffect, useState, type ComponentType, type ReactNode } from "react"; |
| 2 | 2 | import { View } from "react-native"; |
| 3 | 3 | |
| 4 | import { RenderErrorBoundary, RenderFailureView } from "../../components/RenderErrorBoundary"; | |
| 5 | ||
| 4 | 6 | export type ThreadInspectorMode = "route" | "git" | "files"; |
| 5 | 7 | |
| 6 | 8 | const INSPECTOR_PREWARM_DELAY_MS = 350; |
| ... | ... | @@ -8,6 +10,7 @@ const INSPECTOR_PREWARM_DELAY_MS = 350; |
| 8 | 10 | function InspectorContentPane(props: { |
| 9 | 11 | readonly children: ReactNode; |
| 10 | 12 | readonly mounted: boolean; |
| 13 | readonly resetKeys: readonly [string | null, string | null]; | |
| 11 | 14 | readonly visible: boolean; |
| 12 | 15 | }) { |
| 13 | 16 | if (!props.mounted) { |
| ... | ... | @@ -27,7 +30,14 @@ function InspectorContentPane(props: { |
| 27 | 30 | zIndex: props.visible ? 1 : 0, |
| 28 | 31 | }} |
| 29 | 32 | > |
| 30 | {props.children} | |
| 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> | |
| 31 | 41 | </View> |
| 32 | 42 | ); |
| 33 | 43 | } |
| ... | ... | @@ -36,6 +46,7 @@ export function ThreadInspectorContentStack(props: { |
| 36 | 46 | readonly Files: ComponentType; |
| 37 | 47 | readonly Git: ComponentType; |
| 38 | 48 | readonly mode: ThreadInspectorMode; |
| 49 | readonly resetKeys: readonly [string | null, string | null]; | |
| 39 | 50 | readonly Route?: ComponentType; |
| 40 | 51 | }) { |
| 41 | 52 | const [mountedModes, setMountedModes] = useState<ReadonlySet<ThreadInspectorMode>>( |
| ... | ... | @@ -78,12 +89,14 @@ export function ThreadInspectorContentStack(props: { |
| 78 | 89 | <View className="flex-1"> |
| 79 | 90 | <InspectorContentPane |
| 80 | 91 | mounted={mountedModes.has("files") || props.mode === "files"} |
| 92 | resetKeys={props.resetKeys} | |
| 81 | 93 | visible={props.mode === "files"} |
| 82 | 94 | > |
| 83 | 95 | <Files /> |
| 84 | 96 | </InspectorContentPane> |
| 85 | 97 | <InspectorContentPane |
| 86 | 98 | mounted={mountedModes.has("git") || props.mode === "git"} |
| 99 | resetKeys={props.resetKeys} | |
| 87 | 100 | visible={props.mode === "git"} |
| 88 | 101 | > |
| 89 | 102 | <Git /> |
| ... | ... | @@ -91,6 +104,7 @@ export function ThreadInspectorContentStack(props: { |
| 91 | 104 | {Route ? ( |
| 92 | 105 | <InspectorContentPane |
| 93 | 106 | mounted={mountedModes.has("route") || props.mode === "route"} |
| 107 | resetKeys={props.resetKeys} | |
| 94 | 108 | visible={props.mode === "route"} |
| 95 | 109 | > |
| 96 | 110 | <Route /> |