Skip to content
Source
Sign inSign up

Stack.tsx

816 lines32.8 KB

1import { isDraftThreadRoute } from "./features/threads/thread-route-state";
2import { ComponentCatalogScreen } from "./features/development/ComponentCatalogScreen";
3import { PullRequestGuideStopScreen } from "./features/pull-requests/guide/PullRequestGuideScreen";
4import { PullRequestInlineCommentScreen } from "./features/pull-requests/PullRequestInlineCommentScreen";
5import { PullRequestDiscussionScreen } from "./features/pull-requests/PullRequestDiscussionScreen";
6import { createWorkspaceStackNavigator } from "./features/layout/createWorkspaceStackNavigator";
7import {
8 createPathConfigForStaticNavigation,
9 getPathFromState,
10 NavigationState,
11 StackActions,
12 useNavigation,
13} from "@react-navigation/native";
14import { createNativeStackScreen } from "@react-navigation/native-stack";
15import type { ReactNode } from "react";
16import { Platform, Pressable, ScrollView, StyleSheet, useWindowDimensions } from "react-native";
17import { useResolveClassNames } from "uniwind";
18​
19import { AppText as Text } from "./components/ui/AppText";
20import { androidWorkspaceDestinationAnimation } from "./lib/android-workspace-navigation";
21import { getCompactBrandHeaderOptions } from "./features/layout/workspace/brand-header-options";
22import { WorkspaceConnectionStatusProvider } from "./features/home/WorkspaceConnectionTitle";
23import { WorkspaceRootScreen } from "./features/layout/workspace/WorkspaceRootScreen";
24import {
25 recordHomeFocus,
26 recordHomeTransitionEnd,
27 recordHomeTransitionStart,
28} from "./features/home/home-route-visibility";
29import { AndroidWorkspaceTabsProvider } from "./features/layout/workspace/AndroidWorkspaceTabsProvider";
30import {
31 PullRequestRouteScreen,
32 PullRequestFileRouteScreen,
33} from "./features/pull-requests/PullRequestRouteScreen";
34import { PullRequestFilesSheet } from "./features/pull-requests/PullRequestFiles";
35import { PullRequestReviewSheet } from "./features/pull-requests/PullRequestReviewSheet";
36import { PullRequestReviewersScreen } from "./features/pull-requests/PullRequestReviewersScreen";
37import { PullRequestLabelsScreen } from "./features/pull-requests/PullRequestLabelsScreen";
38import { PullRequestEditScreen } from "./features/pull-requests/PullRequestEditScreen";
39import { PullRequestFilterSearchScreen } from "./features/pull-requests/PullRequestFilterSearchScreen";
40import {
41 RenderErrorBoundary,
42 RenderFailureView,
43 type RenderFailureProps,
44} from "./components/RenderErrorBoundary";
45import { ArchivedThreadsRouteScreen } from "./features/archive/ArchivedThreadsRouteScreen";
46import { ConnectOnboardingRouteScreen } from "./features/cloud/ConnectOnboardingRouteScreen";
47import { AttachmentFileScreen } from "./features/files/AttachmentFileScreen";
48import { ThreadFilesTreeScreen, ThreadFileScreen } from "./features/files/ThreadFilesRouteScreen";
49import { AdaptiveWorkspaceLayout } from "./features/layout/AdaptiveWorkspaceLayout";
50import {
51 HardwareKeyboardCommandOverlay,
52 HardwareKeyboardCommandProvider,
53} from "./features/keyboard/HardwareKeyboardCommandProvider";
54import { ReviewCommentComposerSheet } from "./features/review/ReviewCommentComposerSheet";
55import { ReviewSheet } from "./features/review/ReviewSheet";
56import { ThreadTerminalRouteScreen } from "./features/terminal/ThreadTerminalRouteScreen";
57import { DevicePreviewRouteScreen } from "./features/devices/DevicePreviewRouteScreen";
58import { BrowserPreviewRouteScreen } from "./features/browser/BrowserPreviewRouteScreen";
59import { GitBranchesSheet } from "./features/threads/git/GitBranchesSheet";
60import { GitCommitSheet } from "./features/threads/git/GitCommitSheet";
61import { GitConfirmSheet } from "./features/threads/git/GitConfirmSheet";
62import { GitOverviewSheet } from "./features/threads/git/GitOverviewSheet";
63import { ThreadPullRequestsSheet } from "./features/threads/ThreadPullRequestsSheet";
64import { ThreadAgentsSheet } from "./features/threads/ThreadAgentsSheet";
65import { ThreadBackgroundWorkSheet } from "./features/threads/ThreadBackgroundWorkSheet";
66import { ThreadQueueSheet } from "./features/threads/ThreadQueueControl";
67import {
68 FullscreenThreadRouteScreen,
69 ThreadRouteScreen,
70} from "./features/threads/ThreadRouteScreen";
71import { McpAppFullscreenScreen } from "./features/threads/McpAppFullscreenScreen";
72import { ConnectionsRouteScreen } from "./features/connection/ConnectionsRouteScreen";
73import { ConnectionsNewRouteScreen } from "./features/connection/ConnectionsNewRouteScreen";
74import { AddProjectDestinationRoute } from "./features/projects/AddProjectDestinationRoute";
75import { AddProjectLocalRoute } from "./features/projects/AddProjectLocalRoute";
76import { AddProjectNewRoute } from "./features/projects/AddProjectNewRoute";
77import { AddProjectRepositoryRoute } from "./features/projects/AddProjectRepositoryRoute";
78import { AddProjectSourceRoute } from "./features/projects/AddProjectSourceRoute";
79import {
80 NewTaskBranchPickerRouteScreen,
81 NewTaskEnvironmentPickerRouteScreen,
82} from "./features/threads/NewTaskContextPickerScreens";
83import {
84 ExistingThreadSettingsRouteScreen,
85 NewTaskThreadSettingsRouteScreen,
86} from "./features/threads/ThreadSettingsSheet";
87import { NewTaskFlowProvider } from "./features/threads/new-task-flow-provider";
88import { newTaskFlowKey } from "./features/threads/new-task-flow-state";
89import { NewTaskRouteScreen } from "./features/threads/NewTaskRouteScreen";
90import { SettingsNavigationRouteScreen } from "./features/settings/SettingsNavigationRouteScreen";
91import { SettingsAppearanceRouteScreen } from "./features/settings/SettingsAppearanceRouteScreen";
92import { SettingsClientStorageRouteScreen } from "./features/settings/SettingsClientStorageRouteScreen";
93import { SettingsDiagnosticsRouteScreen } from "./features/diagnostics/SettingsDiagnosticsRouteScreen";
94import { SettingsProviderAccountsRouteScreen } from "./features/settings/SettingsProviderAccountsRouteScreen";
95import { SettingsAuthRouteScreen } from "./features/settings/SettingsAuthRouteScreen";
96import { SettingsEnvironmentDetailRouteScreen } from "./features/settings/SettingsEnvironmentDetailRouteScreen";
97import { SettingsEnvironmentsRouteScreen } from "./features/settings/SettingsEnvironmentsRouteScreen";
98import { SettingsFollowUpRouteScreen } from "./features/settings/SettingsFollowUpRouteScreen";
99import { SettingsLanguageRouteScreen } from "./features/settings/SettingsLanguageRouteScreen";
100import {
101 SettingsEnvironmentMaintenanceRouteScreen,
102 SettingsThreadsRouteScreen,
103 SettingsWorktreesRouteScreen,
104 SettingsEnvironmentSourceControlRouteScreen,
105} from "./features/settings/SettingsServerControlsRouteScreen";
106import {
107 SettingsScheduledTasksRouteScreen,
108 SettingsScheduledTaskNewRouteScreen,
109 SettingsScheduledTaskEditRouteScreen,
110} from "./features/settings/SettingsScheduledTasksRouteScreen";
111import {
112 ScheduledTaskModelPickerRouteScreen,
113 ScheduledTaskBranchPickerRouteScreen,
114 ScheduledTaskThreadPickerRouteScreen,
115} from "./features/settings/ScheduledTaskPickerScreens";
116import { SettingsKeyboardRouteScreen } from "./features/settings/SettingsKeyboardRouteScreen";
117import { SettingsLegalRouteScreen } from "./features/settings/SettingsLegalRouteScreen";
118import { SettingsVoiceRouteScreen } from "./features/settings/SettingsVoiceRouteScreen";
119import { SettingsVoiceModelsRouteScreen } from "./features/settings/SettingsVoiceModelsRouteScreen";
120import {
121 SettingsOpenSourceLicenseRouteScreen,
122 SettingsOpenSourceLicensesRouteScreen,
123} from "./features/settings/SettingsOpenSourceLicensesRouteScreen";
124import { SettingsProjectGroupingRouteScreen } from "./features/settings/SettingsProjectGroupingRouteScreen";
125import { SettingsProjectOverviewRouteScreen } from "./features/settings/SettingsProjectOverviewRouteScreen";
126import { UsageLimitAccountScreen } from "./features/usage/UsageLimitsPooled";
127import { UsageRouteScreen } from "./features/usage/UsageRouteScreen";
128import { SettingsAboutRouteScreen } from "./features/settings/SettingsAboutRouteScreen";
129import { SettingsNotificationsRouteScreen } from "./features/settings/SettingsNotificationsRouteScreen";
130import { SettingsRouteScreen } from "./features/settings/SettingsRouteScreen";
131import { SettingsLabsRouteScreen } from "./features/settings/ThreadSettingsSections";
132import { SettingsEnvironmentFilterProvider } from "./features/settings/settings-environment-filter";
133import { openSettings, settingsOpenIn } from "./features/settings/settings-navigation";
134import { DEFAULT_SETTINGS_PAGE } from "./features/layout/workspace/workspace-tab-navigation";
135import {
136 isWorkspaceOverlayRoute,
137 registerPresentedRoutes,
138} from "./features/layout/workspace-overlay-routes";
139import { ShowcaseReadyMarker } from "./features/showcase/ShowcaseCaptureCoordinator";
140import { immersiveRoute, modalRoute, pageRoute, sheetRoute } from "./features/layout/route-options";
141import { deriveLayout } from "./lib/layout";
142import { useNativeLayoutMetrics } from "./native/native-layout-metrics";
143import { isPresentedRoute } from "./native/workspace-stack-projection";
144import { workspaceDetailRouter } from "./features/layout/workspace/workspace-detail-selection";
145​
146const settingsPage = (title: string) => pageRoute({ title, itemStyle: "standard" });
147​
148// Thread settings keep the Duo's vertical bar so cancel, search, filter and confirm sit in the
149// rail. The picker stack spans the sheet so its header reaches the rail, and insets its content.
150const THREAD_SETTINGS_ROUTE = sheetRoute({
151 detent: "full",
152 header: "none",
153 android: "card",
154 spansRail: true,
155});
156​
157// Search sheets push on Android like the branch picker; a formSheet there leaves the list's
158// header peeking above it once the keyboard opens.
159const SEARCH_SHEET_ROUTE = sheetRoute({ detent: 0.75, android: "card", itemStyle: "editor" });
160​
161const SETTINGS_SCREENS = {
162 Settings: createNativeStackScreen({
163 screen: SettingsRouteScreen,
164 linking: "settings",
165 options: {
166 ...pageRoute({ title: "Settings", itemStyle: "standard", root: true }),
167 ...(Platform.OS === "android" ? { animation: "none" as const } : {}),
168 },
169 }),
170 SettingsEnvironments: createNativeStackScreen({
171 screen: SettingsEnvironmentsRouteScreen,
172 linking: "settings/environments",
173 options: settingsPage("Environments"),
174 }),
175 SettingsEnvironmentDetail: createNativeStackScreen({
176 screen: SettingsEnvironmentDetailRouteScreen,
177 linking: "settings/environments/:environmentId",
178 options: settingsPage("Environment"),
179 }),
180 // Old links to New threads and Agent behavior open Threads, which absorbed them.
181 SettingsEnvironmentNewThreads: createNativeStackScreen({
182 screen: SettingsThreadsRouteScreen,
183 linking: "settings/new-threads",
184 options: settingsPage("Threads"),
185 }),
186 SettingsEnvironmentSourceControl: createNativeStackScreen({
187 screen: SettingsEnvironmentSourceControlRouteScreen,
188 linking: "settings/source-control",
189 options: settingsPage("Source control"),
190 }),
191 SettingsEnvironmentAgentBehavior: createNativeStackScreen({
192 screen: SettingsThreadsRouteScreen,
193 linking: "settings/agent-behavior",
194 options: settingsPage("Threads"),
195 }),
196 SettingsWorktrees: createNativeStackScreen({
197 screen: SettingsWorktreesRouteScreen,
198 linking: "settings/worktrees",
199 options: settingsPage("Worktrees"),
200 }),
201 SettingsLabs: createNativeStackScreen({
202 screen: SettingsLabsRouteScreen,
203 linking: "settings/labs",
204 options: settingsPage("Labs"),
205 }),
206 SettingsProviderAccounts: createNativeStackScreen({
207 screen: SettingsProviderAccountsRouteScreen,
208 linking: "settings/provider-accounts",
209 options: settingsPage("Providers"),
210 }),
211 SettingsEnvironmentMaintenance: createNativeStackScreen({
212 screen: SettingsEnvironmentMaintenanceRouteScreen,
213 linking: "settings/maintenance",
214 options: settingsPage("Maintenance"),
215 }),
216 SettingsNotifications: createNativeStackScreen({
217 screen: SettingsNotificationsRouteScreen,
218 linking: "settings/notifications",
219 options: settingsPage("Notifications"),
220 }),
221 SettingsThreads: createNativeStackScreen({
222 screen: SettingsThreadsRouteScreen,
223 linking: "settings/thread-preferences",
224 options: settingsPage("Threads"),
225 }),
226 SettingsAbout: createNativeStackScreen({
227 screen: SettingsAboutRouteScreen,
228 linking: "settings/about",
229 options: settingsPage("About"),
230 }),
231 SettingsEnvironmentNew: createNativeStackScreen({
232 screen: ConnectionsNewRouteScreen,
233 linking: "settings/environment-new",
234 options: settingsPage("Add Environment"),
235 }),
236 SettingsArchive: createNativeStackScreen({
237 screen: ArchivedThreadsRouteScreen,
238 linking: "settings/archive",
239 options: settingsPage("Archive"),
240 }),
241 SettingsNavigation: createNativeStackScreen({
242 screen: SettingsNavigationRouteScreen,
243 linking: "settings/navigation-tabs",
244 options: settingsPage("Navigation tabs"),
245 }),
246 SettingsAppearance: createNativeStackScreen({
247 screen: SettingsAppearanceRouteScreen,
248 linking: "settings/appearance",
249 options: settingsPage("Appearance"),
250 }),
251 SettingsProjectGrouping: createNativeStackScreen({
252 screen: SettingsProjectGroupingRouteScreen,
253 linking: "settings/project-grouping",
254 options: settingsPage("General"),
255 }),
256 SettingsOrganization: createNativeStackScreen({
257 screen: SettingsProjectGroupingRouteScreen,
258 linking: "settings/organization",
259 options: settingsPage("General"),
260 }),
261 SettingsLanguage: createNativeStackScreen({
262 screen: SettingsLanguageRouteScreen,
263 linking: "settings/language",
264 options: settingsPage("Language"),
265 }),
266 SettingsProjectOverview: createNativeStackScreen({
267 screen: SettingsProjectOverviewRouteScreen,
268 linking: "settings/project",
269 options: settingsPage("Projects"),
270 }),
271 SettingsKeyboard: createNativeStackScreen({
272 screen: SettingsKeyboardRouteScreen,
273 linking: "settings/keyboard",
274 options: settingsPage("Keyboard"),
275 }),
276 SettingsVoice: createNativeStackScreen({
277 screen: SettingsVoiceRouteScreen,
278 linking: { path: "settings/voice", alias: ["settings/microphone"] },
279 options: settingsPage("Voice"),
280 }),
281 SettingsVoiceModels: createNativeStackScreen({
282 screen: SettingsVoiceModelsRouteScreen,
283 options: SEARCH_SHEET_ROUTE,
284 }),
285 SettingsFollowUp: createNativeStackScreen({
286 screen: SettingsFollowUpRouteScreen,
287 linking: "settings/follow-ups",
288 options: settingsPage("Chat"),
289 }),
290 SettingsScheduledTasks: createNativeStackScreen({
291 screen: SettingsScheduledTasksRouteScreen,
292 linking: "settings/scheduled-tasks",
293 options: {
294 ...settingsPage("Scheduled tasks"),
295 headerTitleStyle: { fontSize: 16, fontWeight: "800" },
296 },
297 }),
298 SettingsScheduledTaskNew: createNativeStackScreen({
299 screen: SettingsScheduledTaskNewRouteScreen,
300 linking: "settings/scheduled-tasks/new",
301 options: settingsPage("New scheduled task"),
302 }),
303 SettingsScheduledTaskEdit: createNativeStackScreen({
304 screen: SettingsScheduledTaskEditRouteScreen,
305 options: settingsPage("Edit scheduled task"),
306 }),
307 SettingsScheduledTaskBranch: createNativeStackScreen({
308 screen: ScheduledTaskBranchPickerRouteScreen,
309 options: settingsPage("Base branch"),
310 }),
311 SettingsScheduledTaskThread: createNativeStackScreen({
312 screen: ScheduledTaskThreadPickerRouteScreen,
313 options: settingsPage("Destination"),
314 }),
315 SettingsScheduledTaskModel: createNativeStackScreen({
316 screen: ScheduledTaskModelPickerRouteScreen,
317 options: THREAD_SETTINGS_ROUTE,
318 }),
319 SettingsClientStorage: createNativeStackScreen({
320 screen: SettingsClientStorageRouteScreen,
321 linking: "settings/client-storage",
322 options: settingsPage("Client Storage"),
323 }),
324 SettingsDiagnostics: createNativeStackScreen({
325 screen: SettingsDiagnosticsRouteScreen,
326 linking: "settings/diagnostics",
327 options: settingsPage("Diagnostics"),
328 }),
329 // These params snapshot the tap time, local filters, and freshly aggregated pools.
330 // A linking path would round-trip them as URL query strings and preserve stale values.
331 SettingsUsageAccount: createNativeStackScreen({
332 screen: UsageLimitAccountScreen,
333 options: settingsPage("Account"),
334 }),
335 SettingsOpenSourceLicenses: createNativeStackScreen({
336 screen: SettingsOpenSourceLicensesRouteScreen,
337 linking: "settings/open-source-licenses",
338 options: settingsPage("Open source licenses"),
339 }),
340 SettingsOpenSourceLicense: createNativeStackScreen({
341 screen: SettingsOpenSourceLicenseRouteScreen,
342 linking: "settings/open-source-licenses/:entryKey",
343 options: settingsPage("License notice"),
344 }),
345 SettingsUsage: createNativeStackScreen({
346 screen: UsageRouteScreen,
347 linking: "settings/usage",
348 options: settingsPage("Usage"),
349 }),
350 SettingsAuth: createNativeStackScreen({
351 screen: SettingsAuthRouteScreen,
352 linking: "settings/auth",
353 options: { headerShown: false, presentation: "modal" },
354 }),
355 SettingsWaitlist: createNativeStackScreen({
356 screen: SettingsAuthRouteScreen,
357 linking: "settings/waitlist",
358 options: { headerShown: false, presentation: "modal" },
359 }),
360};
361​
362// iOS 26 header morphing requires one UINavigationBar; nested stacks make the whole screen slide.
363const THREAD_LINKING_PREFIX = "threads/:environmentId/:threadId";
364​
365export function workspaceLocationFromState(state: NavigationState) {
366 const routes = state.routes.filter((route) => !isWorkspaceOverlayRoute(route.name));
367 const effectiveState =
368 routes.length > 0 && routes.length !== state.routes.length
369 ? ({ ...state, routes, index: routes.length - 1 } as NavigationState)
370 : state;
371 const path = getPathFromState(effectiveState, navigationPathConfig);
372 const route = effectiveState.routes[effectiveState.index];
373 return {
374 pathname: path.startsWith("/") ? path : `/${path}`,
375 routeKey: route?.key,
376 routeName: route?.name,
377 };
379​
380export function rootPathnameFromState(state: NavigationState): string {
381 const path = getPathFromState(state, navigationPathConfig);
382 return path.startsWith("/") ? path : `/${path}`;
384​
385function RootStackLayout(props: {
386 readonly children: React.ReactNode;
387 readonly state: NavigationState;
388}) {
389 const pathname = rootPathnameFromState(props.state);
390 const workspaceLocation = workspaceLocationFromState(props.state);
391 const settingsOpen = settingsOpenIn(
392 props.state.routes
393 .slice(0, props.state.index + 1)
394 .filter((route) => !isWorkspaceOverlayRoute(route.name)),
395 );
396 // The flow follows the draft route even while a pushed Thread covers it, so
397 // its state lives exactly as long as the route.
398 const draftRoute = props.state.routes.findLast(isDraftThreadRoute);
399 const draftRouteOnTop =
400 draftRoute !== undefined &&
401 props.state.routes.findLast((route) => route.name === "Thread") === draftRoute;
402 const draftParams = (
403 draftRoute?.params as { draft?: { draftId?: string | string[] } } | undefined
404 )?.draft;
405 const routeDraftId = Array.isArray(draftParams?.draftId)
406 ? draftParams.draftId[0]
407 : draftParams?.draftId;
408​
409 return (
410 <NewTaskFlowProvider
411 active={draftRouteOnTop}
412 flowKey={draftRoute === undefined ? undefined : newTaskFlowKey(draftRoute)}
413 routeDraftId={routeDraftId}
414 >
415 <HardwareKeyboardCommandProvider pathname={pathname}>
416 <ShowcaseReadyMarker />
417 <SettingsEnvironmentFilterProvider open={settingsOpen}>
418 <WorkspaceConnectionStatusProvider>
419 <AndroidWorkspaceTabsProvider state={props.state}>
420 <AdaptiveWorkspaceLayout
421 pathname={workspaceLocation.pathname}
422 workspaceRouteKey={workspaceLocation.routeKey}
423 workspaceRouteName={workspaceLocation.routeName}
424 >
425 {props.children}
426 <HardwareKeyboardCommandOverlay />
427 </AdaptiveWorkspaceLayout>
428 </AndroidWorkspaceTabsProvider>
429 </WorkspaceConnectionStatusProvider>
430 </SettingsEnvironmentFilterProvider>
431 </HardwareKeyboardCommandProvider>
432 </NewTaskFlowProvider>
433 );
435​
436function NotFoundScreen() {
437 const navigation = useNavigation();
438 const screenBgStyle = StyleSheet.flatten(useResolveClassNames("bg-screen"));
439 const primaryBgStyle = StyleSheet.flatten(useResolveClassNames("bg-primary"));
440 const returnHomeButtonStyle = StyleSheet.flatten([
441 {
442 borderRadius: 999,
443 paddingHorizontal: 20,
444 paddingVertical: 14,
445 },
446 primaryBgStyle,
447 ]);
448​
449 return (
450 <ScrollView
451 contentInsetAdjustmentBehavior="automatic"
452 contentContainerStyle={{
453 flexGrow: 1,
454 alignItems: "center",
455 justifyContent: "center",
456 gap: 16,
457 paddingHorizontal: 24,
458 paddingVertical: 32,
459 }}
460 style={[{ flex: 1 }, screenBgStyle]}
461 >
462 <Text className="text-3xl font-t3-bold text-foreground" selectable>
463 Route not found
464 </Text>
465 <Pressable
466 style={returnHomeButtonStyle}
467 onPress={() => navigation.dispatch(StackActions.replace("Home"))}
468 >
469 <Text className="text-base font-t3-bold text-primary-foreground">Return home</Text>
470 </Pressable>
471 </ScrollView>
472 );
474​
475const RootStackConfig = createWorkspaceStackNavigator({
476 initialRouteName: "Home",
477 UNSTABLE_router: workspaceDetailRouter,
478 // React Navigation calls layouts inline; compiled components would share the navigator's memo cache.
479 layout: (props) => <RootStackLayout {...props} />,
480 screenOptions: {
481 headerShown: false,
482 },
483 screens: {
484 ...(__DEV__
485 ? {
486 ComponentCatalog: createNativeStackScreen({
487 screen: ComponentCatalogScreen,
488 linking: "dev/components",
489 options: { title: "UI components" },
490 }),
491 }
492 : {}),
493 Home: createNativeStackScreen({
494 screen: WorkspaceRootScreen,
495 linking: "",
496 listeners: ({ navigation }) => ({
497 transitionStart: ({ data }) => recordHomeTransitionStart(data.closing),
498 transitionEnd: ({ data }) => recordHomeTransitionEnd(data.closing, navigation.isFocused()),
499 focus: recordHomeFocus,
500 }),
501 options: {
502 ...pageRoute({ duoMinimize: "never", root: true }),
503 contentStyle: { backgroundColor: "transparent" },
504 ...getCompactBrandHeaderOptions(),
505 },
506 }),
507 PullRequest: createNativeStackScreen({
508 screen: PullRequestRouteScreen,
509 linking: {
510 path: "pull-requests/:environmentId/:projectId/:repository/:number",
511 parse: { number: Number },
512 },
513 options: pageRoute({ duoMinimize: "never" }),
514 }),
515 PullRequestGuideStop: createNativeStackScreen({
516 screen: PullRequestGuideStopScreen,
517 options: pageRoute(),
518 }),
519 PullRequestFile: createNativeStackScreen({
520 screen: PullRequestFileRouteScreen,
521 options: pageRoute(),
522 }),
523 PullRequestFiles: createNativeStackScreen({
524 screen: PullRequestFilesSheet,
525 options: sheetRoute({ detent: 0.6 }),
526 }),
527 PullRequestReviewers: createNativeStackScreen({
528 screen: PullRequestReviewersScreen,
529 options: sheetRoute({ detent: 0.75, itemStyle: "editor", duoMinimize: "never" }),
530 }),
531 PullRequestLabels: createNativeStackScreen({
532 screen: PullRequestLabelsScreen,
533 options: SEARCH_SHEET_ROUTE,
534 }),
535 PullRequestFilterSearch: createNativeStackScreen({
536 screen: PullRequestFilterSearchScreen,
537 options: SEARCH_SHEET_ROUTE,
538 }),
539 PullRequestDiscussion: createNativeStackScreen({
540 screen: PullRequestDiscussionScreen,
541 options: pageRoute(),
542 }),
543 PullRequestInlineComment: createNativeStackScreen({
544 screen: PullRequestInlineCommentScreen,
545 options: sheetRoute({ detent: 0.75, android: "fullScreen" }),
546 }),
547 PullRequestReview: createNativeStackScreen({
548 screen: PullRequestReviewSheet,
549 options: sheetRoute({ detent: 0.75, androidFit: true }),
550 }),
551 PullRequestEdit: createNativeStackScreen({
552 screen: PullRequestEditScreen,
553 options: sheetRoute({ detent: 0.75, androidFit: true }),
554 }),
555 Thread: createNativeStackScreen({
556 screen: ThreadRouteScreen,
557 linking: { path: "threads/:environmentId?/:threadId?", alias: ["new/draft"] },
558 options: pageRoute({ duoMinimize: "onScrollUp" }),
559 }),
560 NewTaskProject: createNativeStackScreen({
561 screen: NewTaskRouteScreen,
562 linking: { path: "new", alias: ["new/project"] },
563 options: pageRoute({ title: "Choose project" }),
564 }),
565 NewTaskEnvironment: createNativeStackScreen({
566 linking: "new/draft/environment",
567 screen: NewTaskEnvironmentPickerRouteScreen,
568 options: pageRoute({ title: "Environment" }),
569 }),
570 NewTaskBranch: createNativeStackScreen({
571 linking: "new/draft/branch",
572 screen: NewTaskBranchPickerRouteScreen,
573 options: pageRoute({ title: "Branch" }),
574 }),
575 AddProject: createNativeStackScreen({
576 linking: "new/add-project",
577 screen: AddProjectSourceRoute,
578​
579 options: pageRoute({ title: "Add project" }),
580 }),
581 AddProjectRepository: createNativeStackScreen({
582 linking: "new/add-project/repository",
583 screen: AddProjectRepositoryRoute,
584 options: pageRoute(),
585 }),
586 AddProjectDestination: createNativeStackScreen({
587 linking: "new/add-project/destination",
588 screen: AddProjectDestinationRoute,
589 options: pageRoute(),
590 }),
591 AddProjectLocal: createNativeStackScreen({
592 linking: "new/add-project/local",
593 screen: AddProjectLocalRoute,
594 options: pageRoute(),
595 }),
596 AddProjectNew: createNativeStackScreen({
597 linking: "new/add-project/new",
598 screen: AddProjectNewRoute,
599 options: pageRoute(),
600 }),
601 NewTaskFiles: createNativeStackScreen({
602 linking: "new/draft/files",
603 screen: ThreadFilesTreeScreen,
604 options: pageRoute({ header: "solid" }),
605 }),
606 NewTaskFile: createNativeStackScreen({
607 linking: "new/draft/files/:path*",
608 screen: ThreadFileScreen,
609 options: pageRoute({ header: "solid" }),
610 }),
611 NewTaskAttachment: createNativeStackScreen({
612 screen: AttachmentFileScreen,
613 linking: "new/draft/attachments/:attachmentId",
614 options: pageRoute({ header: "solid" }),
615 }),
616 NewTaskThreadSettings: createNativeStackScreen({
617 screen: NewTaskThreadSettingsRouteScreen,
618 linking: "new/draft/settings",
619 options: THREAD_SETTINGS_ROUTE,
620 }),
621 ThreadTerminal: createNativeStackScreen({
622 screen: ThreadTerminalRouteScreen,
623 linking: `${THREAD_LINKING_PREFIX}/terminal`,
624 options: pageRoute({ header: "solid" }),
625 }),
626 ThreadInspectorFullscreen: createNativeStackScreen({
627 screen: FullscreenThreadRouteScreen,
628 options: modalRoute({ header: "glass", itemStyle: "editor", duoMinimize: "onScrollUp" }),
629 }),
630 ThreadDevicePreview: createNativeStackScreen({
631 screen: DevicePreviewRouteScreen,
632 linking: `${THREAD_LINKING_PREFIX}/devices`,
633 options: immersiveRoute({ hideSystemBars: true }),
634 }),
635 ThreadBrowserPreview: createNativeStackScreen({
636 screen: BrowserPreviewRouteScreen,
637 linking: `${THREAD_LINKING_PREFIX}/browser`,
638 options: { ...modalRoute({ header: "solid" }), gestureEnabled: false },
639 }),
640 ThreadReview: createNativeStackScreen({
641 screen: ReviewSheet,
642 linking: `${THREAD_LINKING_PREFIX}/review`,
643 options: pageRoute(),
644 }),
645 ThreadReviewComment: createNativeStackScreen({
646 screen: ReviewCommentComposerSheet,
647 linking: `${THREAD_LINKING_PREFIX}/review-comment`,
648 options: sheetRoute({ detent: 0.55, android: "fullScreen" }),
649 }),
650 ThreadFiles: createNativeStackScreen({
651 screen: ThreadFilesTreeScreen,
652 linking: `${THREAD_LINKING_PREFIX}/files`,
653 options: pageRoute({ title: "Files" }),
654 }),
655 ThreadFile: createNativeStackScreen({
656 screen: ThreadFileScreen,
657 linking: `${THREAD_LINKING_PREFIX}/files/:path*`,
658 options: pageRoute(),
659 }),
660 ThreadMcpApp: createNativeStackScreen({
661 screen: McpAppFullscreenScreen,
662 linking: `${THREAD_LINKING_PREFIX}/apps/:itemId`,
663 options: { ...modalRoute({ header: "solid" }), gestureEnabled: false },
664 }),
665 ThreadAttachment: createNativeStackScreen({
666 screen: AttachmentFileScreen,
667 linking: `${THREAD_LINKING_PREFIX}/attachments/:attachmentId`,
668 options: pageRoute({ header: "solid" }),
669 }),
670 ThreadSettingsSheet: createNativeStackScreen({
671 screen: ExistingThreadSettingsRouteScreen,
672 options: THREAD_SETTINGS_ROUTE,
673 }),
674 ThreadQueue: createNativeStackScreen({
675 screen: ThreadQueueSheet,
676 options: sheetRoute({ detent: 0.65, androidMaxDetent: 0.95 }),
677 }),
678 ThreadBackgroundWork: createNativeStackScreen({
679 screen: ThreadBackgroundWorkSheet,
680 options: sheetRoute({ detent: 0.5, androidMaxDetent: 0.9 }),
681 }),
682 ThreadAgents: createNativeStackScreen({
683 screen: ThreadAgentsSheet,
684 options: sheetRoute({ detent: 0.5, androidMaxDetent: 0.9 }),
685 }),
686 GitOverview: createNativeStackScreen({
687 screen: GitOverviewSheet,
688 linking: `${THREAD_LINKING_PREFIX}/git`,
689 options: sheetRoute({ detent: 0.55 }),
690 }),
691 ThreadPullRequests: createNativeStackScreen({
692 screen: ThreadPullRequestsSheet,
693 linking: `${THREAD_LINKING_PREFIX}/pull-requests`,
694 options: sheetRoute({ detent: 0.55 }),
695 }),
696 GitCommit: createNativeStackScreen({
697 screen: GitCommitSheet,
698 linking: `${THREAD_LINKING_PREFIX}/git/commit`,
699 options: sheetRoute({ detent: 0.8, androidFit: true }),
700 }),
701 GitBranches: createNativeStackScreen({
702 screen: GitBranchesSheet,
703 linking: `${THREAD_LINKING_PREFIX}/git/branches`,
704 options: sheetRoute({ detent: 0.55, androidFit: true }),
705 }),
706 GitConfirm: createNativeStackScreen({
707 screen: GitConfirmSheet,
708 linking: `${THREAD_LINKING_PREFIX}/git-confirm`,
709 options: sheetRoute({ detent: 0.55, maxDetent: 0.7, androidFit: true }),
710 }),
711 ...SETTINGS_SCREENS,
712 SettingsLegal: createNativeStackScreen({
713 screen: SettingsLegalRouteScreen,
714 linking: "settings/legal",
715 options: modalRoute({ header: "solid", title: "Legal" }),
716 }),
717 ConnectOnboarding: createNativeStackScreen({
718 screen: ConnectOnboardingRouteScreen,
719 linking: "connect-onboarding",
720 options: sheetRoute({
721 title: "Set up T3 Connect",
722 header: "solid",
723 detent: 0.6,
724 androidMaxDetent: 0.95,
725 }),
726 }),
727 Connections: createNativeStackScreen({
728 screen: ConnectionsRouteScreen,
729 linking: "connections",
730 options: sheetRoute({ title: "Environments", detent: 0.55, maxDetent: 0.7, android: "card" }),
731 }),
732 ConnectionsNew: createNativeStackScreen({
733 screen: ConnectionsNewRouteScreen,
734 linking: "connections/new",
735 options: sheetRoute({ title: "Add Environment", detent: 0.55, maxDetent: 0.7 }),
736 }),
737 NotFound: createNativeStackScreen({
738 screen: NotFoundScreen,
739 linking: "*",
740 }),
741 },
742});
743​
744function GuardedScreenLayout(props: {
745 readonly children: ReactNode;
746 readonly route: { readonly name: string; readonly params?: object | undefined };
747}) {
748 return (
749 <RenderErrorBoundary
750 resetKeys={[props.route.params]}
751 renderFallback={(fallback) => (
752 <ScreenRenderFallback {...fallback} routeName={props.route.name} />
753 )}
754 >
755 {props.children}
756 </RenderErrorBoundary>
757 );
759​
760function ScreenRenderFallback(props: RenderFailureProps & { readonly routeName: string }) {
761 const navigation = useNavigation();
762 const exit = navigation.canGoBack()
763 ? { label: "Go back", onPress: () => navigation.goBack() }
764 : props.routeName === "Home"
765 ? // Home itself failed, and the Settings list renders inside it; open a Settings page.
766 {
767 label: "Open settings",
768 onPress: () => openSettings(navigation, DEFAULT_SETTINGS_PAGE),
769 }
770 : { label: "Return home", onPress: () => navigation.dispatch(StackActions.replace("Home")) };
771​
772 return <RenderFailureView {...props} exit={exit} />;
774​
775registerPresentedRoutes(
776 Object.entries(RootStackConfig.config.screens ?? {}).flatMap(([name, screen]) => {
777 const options = (screen as { readonly options?: unknown }).options;
778 return options && typeof options === "object" && isPresentedRoute(options) ? [name] : [];
779 }),
780);
781​
782export const RootStack = RootStackConfig.with(function AdaptiveRootStack({ Navigator }) {
783 const { width, height } = useWindowDimensions();
784 const nativeMetrics = useNativeLayoutMetrics();
785 const usesSplitView = deriveLayout({ width, height, nativeMetrics }).usesSplitView;
786​
787 /* oxlint-disable react/no-unstable-nested-components -- React Navigation invokes screenLayout as a render callback. */
788 return (
789 <Navigator
790 screenLayout={(props) => <GuardedScreenLayout {...props} />}
791 screenOptions={({ route, navigation }) =>
792 Platform.OS === "android"
793 ? {
794 animation: androidWorkspaceDestinationAnimation(
795 route,
796 navigation.getState().routes,
797 usesSplitView,
798 ),
799 }
800 : {}
801 }
802 />
803 );
804​
805 /* oxlint-enable react/no-unstable-nested-components */
806});
807​
808type RootStackType = typeof RootStack;
809​
810const navigationPathConfig = {
811 screens: createPathConfigForStaticNavigation(RootStack) ?? {},
812};
813​
814declare module "@react-navigation/native" {
815 interface RootNavigator extends RootStackType {}