1import { PullRequestGuideStopScreen } from "./features/pull-requests/guide/PullRequestGuideScreen";
2import { PullRequestInlineCommentScreen } from "./features/pull-requests/PullRequestInlineCommentScreen";
3import { PullRequestDiscussionScreen } from "./features/pull-requests/PullRequestDiscussionScreen";
4import {
5 createV5StackNavigator as createNativeStackNavigator,
6 createV5SheetStackNavigator,
7} from "./native/createV5StackNavigator";
8import { createWorkspaceStackNavigator } from "./features/layout/createWorkspaceStackNavigator";
9import {
10 createPathConfigForStaticNavigation,
11 getPathFromState,
12 NavigationState,
13 StackActions,
14 useNavigation,
15} from "@react-navigation/native";
16import {
17 createNativeStackScreen,
18 type NativeStackNavigationOptions,
19} from "@react-navigation/native-stack";
20import { useEffect, useRef, type ReactNode } from "react";
21import {
22 Platform,
23 Pressable,
24 ScrollView,
25 StyleSheet,
26 View,
27 useWindowDimensions,
28} from "react-native";
29import { useResolveClassNames } from "uniwind";
30
31import { AppText as Text } from "./components/AppText";
32import { getCompactBrandHeaderOptions } from "./components/CompactBrandTitle";
33import { WorkspaceRootScreen } from "./features/workspace/WorkspaceRootScreen";
34import { AndroidWorkspaceTabsProvider } from "./features/workspace/AndroidWorkspaceTabsProvider";
35import {
36 PullRequestRouteScreen,
37 PullRequestFileRouteScreen,
38 PullRequestFilesRouteScreen,
39} from "./features/pull-requests/PullRequestRouteScreen";
40import { PullRequestReviewSheet } from "./features/pull-requests/PullRequestReviewSheet";
41import { PullRequestReviewersScreen } from "./features/pull-requests/PullRequestReviewersScreen";
42import {
43 RenderErrorBoundary,
44 RenderFailureView,
45 type RenderFailureProps,
46} from "./components/RenderErrorBoundary";
47import { ArchivedThreadsRouteScreen } from "./features/archive/ArchivedThreadsRouteScreen";
48import { useAgentNotificationNavigation } from "./features/agent-awareness/notificationNavigation";
49import { ConnectOnboardingRouteScreen } from "./features/cloud/ConnectOnboardingRouteScreen";
50import { useConnectOnboardingNavigation } from "./features/cloud/connectOnboardingNavigation";
51import { AttachmentFileScreen } from "./features/files/AttachmentFileScreen";
52import { ThreadFilesTreeScreen, ThreadFileScreen } from "./features/files/ThreadFilesRouteScreen";
53import { AdaptiveWorkspaceLayout } from "./features/layout/AdaptiveWorkspaceLayout";
54import { selectedPullRequestKey } from "./features/pull-requests/pullRequestSelection";
55import {
56 HardwareKeyboardCommandOverlay,
57 HardwareKeyboardCommandProvider,
58} from "./features/keyboard/HardwareKeyboardCommandProvider";
59import { ReviewCommentComposerSheet } from "./features/review/ReviewCommentComposerSheet";
60import { ReviewSheet } from "./features/review/ReviewSheet";
61import { ThreadTerminalRouteScreen } from "./features/terminal/ThreadTerminalRouteScreen";
62import { DevicePreviewRouteScreen } from "./features/devices/DevicePreviewRouteScreen";
63import { BrowserPreviewRouteScreen } from "./features/browser/BrowserPreviewRouteScreen";
64import { GitBranchesSheet } from "./features/threads/git/GitBranchesSheet";
65import { GitCommitSheet } from "./features/threads/git/GitCommitSheet";
66import { GitConfirmSheet } from "./features/threads/git/GitConfirmSheet";
67import { GitOverviewSheet } from "./features/threads/git/GitOverviewSheet";
68import { ThreadAgentsSheet } from "./features/threads/ThreadAgentsSheet";
69import { ThreadQueueSheet } from "./features/threads/ThreadQueueControl";
70import { ThreadRouteScreen } 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 { NewTaskDraftRouteScreen } from "./features/threads/NewTaskDraftRouteScreen";
80import {
81 NewTaskBranchPickerRouteScreen,
82 NewTaskEnvironmentPickerRouteScreen,
83} from "./features/threads/NewTaskContextPickerScreens";
84import {
85 ExistingThreadSettingsRouteProvider,
86 ExistingThreadSettingsRouteScreen,
87 NewTaskThreadSettingsRouteScreen,
88} from "./features/threads/ThreadSettingsSheet";
89import { NewTaskFlowProvider } from "./features/threads/new-task-flow-provider";
90import { NewTaskRouteScreen } from "./features/threads/NewTaskRouteScreen";
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 {
100 SettingsEnvironmentAgentBehaviorRouteScreen,
101 SettingsEnvironmentMaintenanceRouteScreen,
102 SettingsEnvironmentNewThreadsRouteScreen,
103 SettingsEnvironmentSourceControlRouteScreen,
104} from "./features/settings/SettingsServerControlsRouteScreen";
105import {
106 SettingsScheduledTasksRouteScreen,
107 SettingsScheduledTaskNewRouteScreen,
108 SettingsScheduledTaskEditRouteScreen,
109} from "./features/settings/SettingsScheduledTasksRouteScreen";
110import {
111 ScheduledTaskModelPickerRouteScreen,
112 ScheduledTaskBranchPickerRouteScreen,
113} from "./features/settings/ScheduledTaskPickerScreens";
114import { ScheduledTaskEditorProvider } from "./features/settings/scheduled-task-editor";
115import { SettingsKeyboardRouteScreen } from "./features/settings/SettingsKeyboardRouteScreen";
116import { SettingsLegalRouteScreen } from "./features/settings/SettingsLegalRouteScreen";
117import { SettingsMicrophoneRouteScreen } from "./features/settings/SettingsMicrophoneRouteScreen";
118import {
119 SettingsOpenSourceLicenseRouteScreen,
120 SettingsOpenSourceLicensesRouteScreen,
121} from "./features/settings/SettingsOpenSourceLicensesRouteScreen";
122import { SettingsProjectGroupingRouteScreen } from "./features/settings/SettingsProjectGroupingRouteScreen";
123import { SettingsProjectOverviewRouteScreen } from "./features/settings/SettingsProjectOverviewRouteScreen";
124import { UsageLimitAccountScreen } from "./features/usage/UsageLimitsPooled";
125import { UsageRouteScreen } from "./features/usage/UsageRouteScreen";
126import { SettingsAboutRouteScreen } from "./features/settings/SettingsAboutRouteScreen";
127import { SettingsNotificationsRouteScreen } from "./features/settings/SettingsNotificationsRouteScreen";
128import { SettingsRouteScreen } from "./features/settings/SettingsRouteScreen";
129import { SettingsThreadsRouteScreen } from "./features/settings/SettingsThreadsRouteScreen";
130import { SettingsEnvironmentFilterProvider } from "./features/settings/settings-environment-filter";
131import { ShowcaseCaptureCoordinator } from "./features/showcase/ShowcaseCaptureCoordinator";
132import {
133 SettingsLegalDocumentCloseHeaderButton,
134 SettingsLegalDocumentExternalHeaderButton,
135} from "./features/settings/components/SettingsLegalDocumentRouteScreen";
136import { useAppShortcuts } from "./features/shortcuts/useAppShortcuts";
137import { useIncomingShare } from "./features/sharing/IncomingShareProvider";
138import {
139 EMPTY_INCOMING_SHARE_PRESENTATION_STATE,
140 transitionIncomingSharePresentation,
141} from "./features/sharing/incoming-share-presentation";
142import { NATIVE_LIQUID_GLASS_SUPPORTED } from "./native/native-glass";
143import { deriveLayout } from "./lib/layout";
144import { useNativeLayoutMetrics } from "./native/native-layout-metrics";
145import { nativeHeaderScrollEdgeEffects } from "./native/StackHeader";
146import { FORM_SHEET_PRESENTATION_OPTIONS } from "./native/sheet-surface";
147import { useThreadOutboxDrain } from "./state/use-thread-outbox-drain";
148import { useComposerAttachmentUploadWorker } from "./state/composer-attachment-uploads";
149
150const HEADER_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version);
151
152type AppScreenOptions = NativeStackNavigationOptions & {
153 readonly unstable_navigationItemStyle?: "editor";
154};
155
156// Shared header presets. Screens only override genuinely dynamic values (titles,
157// subtitles, toolbar items, search callbacks) via NativeStackScreenOptions.
158//
159// GLASS: transparent header over the screen's primary scroll view on supported
160// iOS versions. Pre-glass iOS gets the same solid material as internal-scroll
161// surfaces so content is laid out below the bar instead of underlapping it.
162const GLASS_HEADER_OPTIONS: AppScreenOptions = {
163 headerBackButtonDisplayMode: "minimal",
164 headerBackTitle: "",
165 headerLargeTitle: false,
166 headerShadowVisible: false,
167 headerShown: true,
168 headerStyle: NATIVE_LIQUID_GLASS_SUPPORTED ? { backgroundColor: "transparent" } : undefined,
169 headerTitleStyle: { fontSize: 18, fontWeight: "800" },
170 headerTransparent: NATIVE_LIQUID_GLASS_SUPPORTED,
171 scrollEdgeEffects: NATIVE_LIQUID_GLASS_SUPPORTED ? HEADER_SCROLL_EDGE_EFFECTS : undefined,
172 unstable_navigationItemStyle: NATIVE_LIQUID_GLASS_SUPPORTED ? "editor" : undefined,
173};
174
175// Android PR screens render their own Material toolbar.
176const PULL_REQUEST_HEADER_OPTIONS: AppScreenOptions = {
177 ...GLASS_HEADER_OPTIONS,
178 headerShown: Platform.OS !== "android",
179};
180
181// SOLID: opaque sheet-colored header for surfaces that manage their own viewport.
182const SOLID_HEADER_OPTIONS: AppScreenOptions = {
183 headerBackButtonDisplayMode: "minimal",
184 headerBackTitle: "",
185 headerLargeTitle: false,
186 headerShadowVisible: false,
187 headerShown: true,
188 headerTitleStyle: { fontSize: 18, fontWeight: "800" },
189 headerTransparent: false,
190 unstable_navigationItemStyle: Platform.OS === "ios" ? "editor" : undefined,
191};
192
193// Solid header variant for screens inside sheets (centered title, no editor style).
194const SHEET_SOLID_HEADER_OPTIONS: AppScreenOptions = {
195 ...SOLID_HEADER_OPTIONS,
196 unstable_navigationItemStyle: undefined,
197};
198
199// A native glass header for a sheet screen whose primary child is a scroll
200// view. The centered sheet title stays stable while UIKit supplies scroll-edge
201// fading from that child.
202const SHEET_GLASS_HEADER_OPTIONS: AppScreenOptions = {
203 ...GLASS_HEADER_OPTIONS,
204 unstable_navigationItemStyle: undefined,
205};
206
207const LEGAL_DOCUMENT_HEADER_OPTIONS: AppScreenOptions = {
208 ...SHEET_SOLID_HEADER_OPTIONS,
209 headerBackVisible: false,
210 headerLeft: SettingsLegalDocumentCloseHeaderButton,
211 headerRight: () => <SettingsLegalDocumentExternalHeaderButton />,
212 presentation: "fullScreenModal",
213};
214
215// A navigator container leaves horizontal safe-area handling to its leaf screens.
216const NESTED_NAVIGATOR_OPTIONS = { nativeContentInsetHorizontally: false, headerShown: false };
217
218const SettingsContentStack = createV5SheetStackNavigator({
219 initialRouteName: "Settings",
220 screenOptions: {
221 ...GLASS_HEADER_OPTIONS,
222 // Sheets read better with the iOS-default centered title (no editor style).
223 unstable_navigationItemStyle: undefined,
224 },
225 screens: {
226 Settings: createNativeStackScreen({
227 screen: SettingsRouteScreen,
228 linking: "",
229 options: {
230 title: "Settings",
231 },
232 }),
233 SettingsEnvironments: createNativeStackScreen({
234 screen: SettingsEnvironmentsRouteScreen,
235 linking: "environments",
236 options: {
237 title: "Environments",
238 },
239 }),
240 SettingsEnvironmentDetail: createNativeStackScreen({
241 screen: SettingsEnvironmentDetailRouteScreen,
242 linking: "environments/:environmentId",
243 options: { title: "Environment" },
244 }),
245 SettingsEnvironmentNewThreads: createNativeStackScreen({
246 screen: SettingsEnvironmentNewThreadsRouteScreen,
247 linking: "new-threads",
248 options: { title: "New threads" },
249 }),
250 SettingsEnvironmentSourceControl: createNativeStackScreen({
251 screen: SettingsEnvironmentSourceControlRouteScreen,
252 linking: "source-control",
253 options: { title: "Source control" },
254 }),
255 SettingsEnvironmentAgentBehavior: createNativeStackScreen({
256 screen: SettingsEnvironmentAgentBehaviorRouteScreen,
257 linking: "agent-behavior",
258 options: { title: "Agent behavior" },
259 }),
260 SettingsProviderAccounts: createNativeStackScreen({
261 screen: SettingsProviderAccountsRouteScreen,
262 linking: "provider-accounts",
263 options: { title: "Provider accounts" },
264 }),
265 SettingsEnvironmentMaintenance: createNativeStackScreen({
266 screen: SettingsEnvironmentMaintenanceRouteScreen,
267 linking: "maintenance",
268 options: { title: "Maintenance" },
269 }),
270 SettingsNotifications: createNativeStackScreen({
271 screen: SettingsNotificationsRouteScreen,
272 linking: "notifications",
273 options: { title: "Notifications" },
274 }),
275 SettingsThreads: createNativeStackScreen({
276 screen: SettingsThreadsRouteScreen,
277 linking: "thread-preferences",
278 options: { title: "Thread behavior" },
279 }),
280 SettingsAbout: createNativeStackScreen({
281 screen: SettingsAboutRouteScreen,
282 linking: "about",
283 options: { title: "About The Knife" },
284 }),
285 SettingsEnvironmentNew: createNativeStackScreen({
286 screen: ConnectionsNewRouteScreen,
287 linking: "environment-new",
288 options: {
289 title: "Add Environment",
290 },
291 }),
292 SettingsArchive: createNativeStackScreen({
293 screen: ArchivedThreadsRouteScreen,
294 linking: "archive",
295 options: {
296 title: "Archived Threads",
297 },
298 }),
299 SettingsAppearance: createNativeStackScreen({
300 screen: SettingsAppearanceRouteScreen,
301 linking: "appearance",
302 options: {
303 title: "Appearance",
304 },
305 }),
306 SettingsProjectGrouping: createNativeStackScreen({
307 screen: SettingsProjectGroupingRouteScreen,
308 linking: "project-grouping",
309 options: {
310 title: "Organization",
311 },
312 }),
313 SettingsOrganization: createNativeStackScreen({
314 screen: SettingsProjectGroupingRouteScreen,
315 linking: "organization",
316 options: {
317 title: "Organization",
318 },
319 }),
320 SettingsProjectOverview: createNativeStackScreen({
321 screen: SettingsProjectOverviewRouteScreen,
322 linking: "project",
323 options: { title: "Project overview" },
324 }),
325 SettingsKeyboard: createNativeStackScreen({
326 screen: SettingsKeyboardRouteScreen,
327 linking: "keyboard",
328 options: {
329 title: "Keyboard",
330 },
331 }),
332 SettingsMicrophone: createNativeStackScreen({
333 screen: SettingsMicrophoneRouteScreen,
334 linking: "microphone",
335 options: {
336 title: "Microphone",
337 },
338 }),
339 SettingsFollowUp: createNativeStackScreen({
340 screen: SettingsFollowUpRouteScreen,
341 linking: "follow-ups",
342 options: {
343 title: "Follow-ups",
344 },
345 }),
346 SettingsScheduledTasks: createNativeStackScreen({
347 screen: SettingsScheduledTasksRouteScreen,
348 linking: "scheduled-tasks",
349 options: {
350 title: "Scheduled Tasks",
351 // Leave room to center UIKit's title beside the two trailing actions.
352 headerTitleStyle: { fontSize: 16, fontWeight: "800" },
353 },
354 }),
355 SettingsScheduledTaskNew: createNativeStackScreen({
356 screen: SettingsScheduledTaskNewRouteScreen,
357 linking: "scheduled-tasks/new",
358 options: { title: "New scheduled task" },
359 }),
360 SettingsScheduledTaskEdit: createNativeStackScreen({
361 screen: SettingsScheduledTaskEditRouteScreen,
362 options: { title: "Edit scheduled task" },
363 }),
364 SettingsScheduledTaskBranch: createNativeStackScreen({
365 screen: ScheduledTaskBranchPickerRouteScreen,
366 options: { title: "Base branch" },
367 }),
368 SettingsScheduledTaskModel: createNativeStackScreen({
369 screen: ScheduledTaskModelPickerRouteScreen,
370 options: {
371 headerShown: false,
372 ...(Platform.OS === "android"
373 ? { presentation: "card" as const }
374 : {
375 ...FORM_SHEET_PRESENTATION_OPTIONS,
376 sheetAllowedDetents: [1],
377 sheetGrabberVisible: true,
378 }),
379 },
380 }),
381 SettingsClientStorage: createNativeStackScreen({
382 screen: SettingsClientStorageRouteScreen,
383 linking: "client-storage",
384 options: {
385 title: "Client Storage",
386 },
387 }),
388 SettingsDiagnostics: createNativeStackScreen({
389 screen: SettingsDiagnosticsRouteScreen,
390 linking: "diagnostics",
391 options: {
392 title: "Diagnostics",
393 },
394 }),
395 // Deliberately the one settings screen with no `linking:` path. Its params
396 // are a tap-time snapshot, not a stable resource address: `now` is the
397 // wall-clock of the tap, `environmentIds` is the usage screen's local
398 // filter selection, and the window id/kind identify freshly aggregated
399 // pools. React Navigation round-trips non-path params through the URL as
400 // query strings, so a path here would bake in a permanently stale
401 // timestamp and filter. The deep linkable surface is the list at
402 // `settings/usage`, which rebuilds this state and pushes the detail from a
403 // tapped account segment.
404 SettingsUsageAccount: createNativeStackScreen({
405 screen: UsageLimitAccountScreen,
406 options: { title: "Account" },
407 }),
408 SettingsOpenSourceLicenses: createNativeStackScreen({
409 screen: SettingsOpenSourceLicensesRouteScreen,
410 linking: "open-source-licenses",
411 options: {
412 title: "Open source licenses",
413 },
414 }),
415 SettingsOpenSourceLicense: createNativeStackScreen({
416 screen: SettingsOpenSourceLicenseRouteScreen,
417 linking: "open-source-licenses/:entryKey",
418 options: {
419 title: "License notice",
420 },
421 }),
422 SettingsUsage: createNativeStackScreen({
423 screen: UsageRouteScreen,
424 linking: "usage",
425 options: {
426 title: "Usage",
427 },
428 }),
429 },
430});
431
432// The outer stack never owns visible chrome. Settings routes render inside a
433// nested stack whose native header remains mounted, while Clerk owns auth chrome.
434// Keeping bar visibility invariant avoids iOS 26's headerless-to-headered jump.
435const SettingsSheetStack = createV5SheetStackNavigator({
436 initialRouteName: "SettingsContent",
437 screenOptions: {
438 headerShown: false,
439 },
440 screens: {
441 SettingsContent: createNativeStackScreen({
442 screen: SettingsContentStack,
443 options: NESTED_NAVIGATOR_OPTIONS,
444 linking: "",
445 layout: ({ children }) => (
446 <SettingsEnvironmentFilterProvider>
447 <ScheduledTaskEditorProvider>{children}</ScheduledTaskEditorProvider>
448 </SettingsEnvironmentFilterProvider>
449 ),
450 }),
451 SettingsAuth: createNativeStackScreen({
452 screen: SettingsAuthRouteScreen,
453 linking: "auth",
454 }),
455 SettingsWaitlist: createNativeStackScreen({
456 // Keep the old deep link working after the Connect GA launch.
457 screen: SettingsAuthRouteScreen,
458 linking: "waitlist",
459 }),
460 },
461});
462
463// Thread routes live FLAT in the root stack (not in a nested navigator). A nested
464// stack means a second UINavigationController with its own UINavigationBar, which
465// breaks iOS 26's shared-header morphing between Home and Thread (each pair inside
466// one bar morphs; across two bars the whole screen slides). Flat linking paths keep
467// the same deep-link URLs the nested config produced.
468const THREAD_LINKING_PREFIX = "threads/:environmentId/:threadId";
469
470// New-task / add-project flow: the nested navigator owns the header and pushes
471// whether the flow opens in the workspace or in a compact form sheet.
472const NewTaskSheetStack = createNativeStackNavigator({
473 initialRouteName: "NewTask",
474 screenOptions: {
475 ...SHEET_GLASS_HEADER_OPTIONS,
476 // The form-sheet host owns the one opaque adaptive surface. Child screens
477 // and the navigation bar stay transparent over it, avoiding visible color
478 // slabs as view controllers move horizontally.
479 contentStyle: Platform.OS === "ios" ? { backgroundColor: "transparent" } : undefined,
480 // UIKit's default push adds a dimming shadow and independently transitions
481 // the navigation bar. Both read as mismatched sheet backgrounds here.
482 // simple_push retains native push/pop gestures without either artifact.
483 animation: Platform.OS === "ios" ? "simple_push" : undefined,
484 animationDuration: Platform.OS === "ios" ? 350 : undefined,
485 },
486 screens: {
487 NewTask: createNativeStackScreen({
488 screen: NewTaskRouteScreen,
489 linking: "",
490 options: {
491 title: "Choose project",
492 },
493 }),
494 NewTaskDraft: createNativeStackScreen({
495 screen: NewTaskDraftRouteScreen,
496 linking: "draft",
497 options: {
498 headerBackVisible: false,
499 title: "",
500 },
501 }),
502 NewTaskEnvironment: createNativeStackScreen({
503 screen: NewTaskEnvironmentPickerRouteScreen,
504 linking: "draft/environment",
505 options: {
506 title: "Environment",
507 },
508 }),
509 NewTaskBranch: createNativeStackScreen({
510 screen: NewTaskBranchPickerRouteScreen,
511 linking: "draft/branch",
512 options: {
513 title: "Branch",
514 },
515 }),
516 // The same file view the thread composer pushes. A draft has no thread, so it names its
517 // own workspace through route params instead of resolving one from a selected thread.
518 NewTaskFile: createNativeStackScreen({
519 screen: ThreadFileScreen,
520 linking: "draft/files/:path*",
521 options: SOLID_HEADER_OPTIONS,
522 }),
523 NewTaskAttachment: createNativeStackScreen({
524 screen: AttachmentFileScreen,
525 linking: "draft/attachments/:attachmentId",
526 options: SOLID_HEADER_OPTIONS,
527 }),
528 ThreadSettings: createNativeStackScreen({
529 screen: NewTaskThreadSettingsRouteScreen,
530 linking: "draft/settings",
531 options: {
532 gestureEnabled: true,
533 headerShown: false,
534 ...(Platform.OS === "android"
535 ? { presentation: "card" as const }
536 : {
537 ...FORM_SHEET_PRESENTATION_OPTIONS,
538 sheetAllowedDetents: [1],
539 sheetGrabberVisible: true,
540 }),
541 },
542 }),
543 AddProject: createNativeStackScreen({
544 screen: AddProjectSourceRoute,
545 linking: "add-project",
546 options: {
547 title: "Add Project",
548 },
549 }),
550 AddProjectRepository: createNativeStackScreen({
551 screen: AddProjectRepositoryRoute,
552 linking: "add-project/repository",
553 }),
554 AddProjectDestination: createNativeStackScreen({
555 screen: AddProjectDestinationRoute,
556 linking: "add-project/destination",
557 }),
558 AddProjectLocal: createNativeStackScreen({
559 screen: AddProjectLocalRoute,
560 linking: "add-project/local",
561 }),
562 AddProjectNew: createNativeStackScreen({
563 screen: AddProjectNewRoute,
564 linking: "add-project/new",
565 }),
566 },
567});
568
569// Routes presented as sheets/overlays ON TOP of the workspace. They must not
570// influence the adaptive workspace layout: opening Settings over Home should
571// not flip the sidebar in or change the active thread.
572const WORKSPACE_OVERLAY_ROUTES = new Set([
573 "PullRequestReview",
574 "PullRequestInlineComment",
575 "ConnectOnboarding",
576 "Connections",
577 "ConnectionsNew",
578 "GitBranches",
579 "GitCommit",
580 "GitConfirm",
581 "GitOverview",
582 "NewTaskSheet",
583 "SettingsLegal",
584 "SettingsSheet",
585 "ThreadAgents",
586 "ThreadQueue",
587 "ThreadReviewComment",
588 "ThreadDevicePreview",
589 "ThreadBrowserPreview",
590 "ThreadSettingsSheet",
591]);
592
593/**
594 * Location of the topmost non-overlay route, including its key so thread
595 * selection can dismiss sheets without replacing the wrong destination.
596 */
597function workspaceLocationFromState(state: NavigationState) {
598 const routes = state.routes.filter((route) => !WORKSPACE_OVERLAY_ROUTES.has(route.name));
599 const effectiveState =
600 routes.length > 0 && routes.length !== state.routes.length
601 ? ({ ...state, routes, index: routes.length - 1 } as NavigationState)
602 : state;
603 const path = getPathFromState(effectiveState, navigationPathConfig);
604 return {
605 pathname: path.startsWith("/") ? path : `/${path}`,
606 routeKey: effectiveState.routes[effectiveState.index]?.key,
607 };
608}
609
610// The drain hook subscribes to the outbox, all thread shells, projects, and
611// connection statuses. Hosting it in a null-rendering leaf keeps those
612// updates from re-rendering RootStackLayout (and with it every screen) on
613// each enqueue, shell change, or reconnect.
614function ThreadOutboxDrainWorker() {
615 useThreadOutboxDrain();
616 useComposerAttachmentUploadWorker();
617 return null;
618}
619
620function RootStackLayout(props: {
621 readonly children: React.ReactNode;
622 readonly state: NavigationState;
623}) {
624 const navigation = useNavigation();
625 const { pendingShare } = useIncomingShare();
626 const sharePresentationRef = useRef(EMPTY_INCOMING_SHARE_PRESENTATION_STATE);
627 // Keyboard commands follow the top route; notification suppression follows
628 // the thread beneath overlay sheets.
629 const path = getPathFromState(props.state, navigationPathConfig);
630 const pathname = path.startsWith("/") ? path : `/${path}`;
631 const workspaceLocation = workspaceLocationFromState(props.state);
632 useAgentNotificationNavigation(workspaceLocation.pathname);
633 // Presents the T3 Connect onboarding sheet after an in-session sign-in.
634 useConnectOnboardingNavigation();
635 // Launcher app shortcuts: routes shortcut taps and tracks opened threads.
636 useAppShortcuts(props.state);
637 useEffect(() => {
638 const topRouteName = props.state.routes[props.state.index]?.name;
639 const transition = transitionIncomingSharePresentation(sharePresentationRef.current, {
640 isShareSheetPresented: topRouteName === "NewTaskSheet",
641 pendingShareId: pendingShare?.id ?? null,
642 });
643 sharePresentationRef.current = transition.state;
644 if (!transition.shareIdToPresent) {
645 return;
646 }
647 navigation.navigate("NewTaskSheet", {
648 screen: "NewTask",
649 params: { incomingShareId: transition.shareIdToPresent },
650 });
651 }, [navigation, pendingShare, props.state]);
652
653 return (
654 <HardwareKeyboardCommandProvider pathname={pathname}>
655 <ThreadOutboxDrainWorker />
656 <ShowcaseCaptureCoordinator pathname={pathname} />
657 <ExistingThreadSettingsRouteProvider>
658 <AndroidWorkspaceTabsProvider>
659 <AdaptiveWorkspaceLayout
660 pathname={workspaceLocation.pathname}
661 workspaceRouteKey={workspaceLocation.routeKey}
662 selectedPullRequestKey={selectedPullRequestKey(props.state)}
663 >
664 {props.children}
665 <HardwareKeyboardCommandOverlay />
666 </AdaptiveWorkspaceLayout>
667 </AndroidWorkspaceTabsProvider>
668 </ExistingThreadSettingsRouteProvider>
669 </HardwareKeyboardCommandProvider>
670 );
671}
672
673function NotFoundScreen() {
674 const navigation = useNavigation();
675 const screenBgStyle = StyleSheet.flatten(useResolveClassNames("bg-screen"));
676 const primaryBgStyle = StyleSheet.flatten(useResolveClassNames("bg-primary"));
677 const returnHomeButtonStyle = StyleSheet.flatten([
678 {
679 borderRadius: 999,
680 paddingHorizontal: 20,
681 paddingVertical: 14,
682 },
683 primaryBgStyle,
684 ]);
685
686 return (
687 <ScrollView
688 contentInsetAdjustmentBehavior="automatic"
689 contentContainerStyle={{
690 flexGrow: 1,
691 alignItems: "center",
692 justifyContent: "center",
693 gap: 16,
694 paddingHorizontal: 24,
695 paddingVertical: 32,
696 }}
697 style={[{ flex: 1 }, screenBgStyle]}
698 >
699 <Text className="text-3xl font-t3-bold text-foreground" selectable>
700 Route not found
701 </Text>
702 <Pressable
703 style={returnHomeButtonStyle}
704 onPress={() => navigation.dispatch(StackActions.replace("Home"))}
705 >
706 <Text className="text-base font-t3-bold text-primary-foreground">Return home</Text>
707 </Pressable>
708 </ScrollView>
709 );
710}
711
712const RootStackConfig = createWorkspaceStackNavigator({
713 initialRouteName: "Home",
714 layout: RootStackLayout,
715 screenOptions: {
716 headerShown: false,
717 },
718 screens: {
719 Home: createNativeStackScreen({
720 screen: WorkspaceRootScreen,
721 linking: "",
722 options: {
723 ...GLASS_HEADER_OPTIONS,
724 contentStyle: { backgroundColor: "transparent" },
725 headerBackVisible: false,
726 ...getCompactBrandHeaderOptions(),
727 },
728 }),
729 PullRequest: createNativeStackScreen({
730 screen: PullRequestRouteScreen,
731 linking: {
732 path: "pull-requests/:environmentId/:projectId/:repository/:number",
733 parse: { number: Number },
734 },
735 options: PULL_REQUEST_HEADER_OPTIONS,
736 }),
737 PullRequestGuideStop: createNativeStackScreen({
738 screen: PullRequestGuideStopScreen,
739 options: PULL_REQUEST_HEADER_OPTIONS,
740 }),
741 PullRequestFile: createNativeStackScreen({
742 screen: PullRequestFileRouteScreen,
743 options: PULL_REQUEST_HEADER_OPTIONS,
744 }),
745 PullRequestFiles: createNativeStackScreen({
746 screen: PullRequestFilesRouteScreen,
747 options: {
748 ...FORM_SHEET_PRESENTATION_OPTIONS,
749 sheetAllowedDetents: [0.6, 0.92],
750 sheetGrabberVisible: true,
751 headerShown: false,
752 },
753 }),
754 PullRequestReviewers: createNativeStackScreen({
755 screen: PullRequestReviewersScreen,
756 options: {
757 ...PULL_REQUEST_HEADER_OPTIONS,
758 ...FORM_SHEET_PRESENTATION_OPTIONS,
759 sheetAllowedDetents: [0.75, 0.92],
760 sheetGrabberVisible: true,
761 headerBackVisible: false,
762 },
763 }),
764 PullRequestDiscussion: createNativeStackScreen({
765 screen: PullRequestDiscussionScreen,
766 options: PULL_REQUEST_HEADER_OPTIONS,
767 }),
768 PullRequestInlineComment: createNativeStackScreen({
769 screen: PullRequestInlineCommentScreen,
770 options: {
771 ...(Platform.OS === "android"
772 ? { presentation: "fullScreenModal" as const }
773 : FORM_SHEET_PRESENTATION_OPTIONS),
774 sheetAllowedDetents: Platform.OS === "android" ? undefined : [0.75, 0.92],
775 sheetGrabberVisible: Platform.OS !== "android",
776 headerShown: false,
777 },
778 }),
779 PullRequestReview: createNativeStackScreen({
780 screen: PullRequestReviewSheet,
781 options: {
782 ...FORM_SHEET_PRESENTATION_OPTIONS,
783 sheetAllowedDetents: [0.75, 0.92],
784 sheetGrabberVisible: true,
785 headerShown: false,
786 },
787 }),
788 Thread: createNativeStackScreen({
789 screen: ThreadRouteScreen,
790 linking: THREAD_LINKING_PREFIX,
791 options: GLASS_HEADER_OPTIONS,
792 }),
793 ThreadTerminal: createNativeStackScreen({
794 screen: ThreadTerminalRouteScreen,
795 linking: `${THREAD_LINKING_PREFIX}/terminal`,
796 options: SOLID_HEADER_OPTIONS,
797 }),
798 ThreadDevicePreview: createNativeStackScreen({
799 screen: DevicePreviewRouteScreen,
800 linking: `${THREAD_LINKING_PREFIX}/devices`,
801 options: {
802 presentation: "fullScreenModal",
803 headerShown: false,
804 gestureEnabled: false,
805 autoHideHomeIndicator: true,
806 navigationBarHidden: true,
807 },
808 }),
809 ThreadBrowserPreview: createNativeStackScreen({
810 screen: BrowserPreviewRouteScreen,
811 linking: `${THREAD_LINKING_PREFIX}/browser`,
812 options: {
813 presentation: "fullScreenModal",
814 headerShown: false,
815 gestureEnabled: false,
816 },
817 }),
818 ThreadReview: createNativeStackScreen({
819 screen: ReviewSheet,
820 linking: `${THREAD_LINKING_PREFIX}/review`,
821 options: GLASS_HEADER_OPTIONS,
822 }),
823 ThreadReviewComment: createNativeStackScreen({
824 screen: ReviewCommentComposerSheet,
825 linking: `${THREAD_LINKING_PREFIX}/review-comment`,
826 options: {
827 // Android cannot host the keyboard-driven comment composer inside a
828 // formSheet; use a full-screen modal there instead.
829 ...(Platform.OS === "android"
830 ? { presentation: "fullScreenModal" as const }
831 : FORM_SHEET_PRESENTATION_OPTIONS),
832 sheetAllowedDetents: Platform.OS === "android" ? undefined : [0.55, 0.92],
833 sheetGrabberVisible: Platform.OS !== "android",
834 },
835 }),
836 ThreadFiles: createNativeStackScreen({
837 screen: ThreadFilesTreeScreen,
838 linking: `${THREAD_LINKING_PREFIX}/files`,
839 options: {
840 ...GLASS_HEADER_OPTIONS,
841 title: "Files",
842 },
843 }),
844 ThreadFile: createNativeStackScreen({
845 screen: ThreadFileScreen,
846 linking: `${THREAD_LINKING_PREFIX}/files/:path*`,
847 options: GLASS_HEADER_OPTIONS,
848 }),
849 ThreadMcpApp: createNativeStackScreen({
850 screen: McpAppFullscreenScreen,
851 linking: `${THREAD_LINKING_PREFIX}/apps/:itemId`,
852 options: {
853 presentation: "fullScreenModal",
854 headerShown: false,
855 gestureEnabled: false,
856 },
857 }),
858 ThreadAttachment: createNativeStackScreen({
859 screen: AttachmentFileScreen,
860 linking: `${THREAD_LINKING_PREFIX}/attachments/:attachmentId`,
861 options: SOLID_HEADER_OPTIONS,
862 }),
863 // Deliberately the one root route with no `linking:` path. The route
864 // carries zero params: its content is a session object (staged model,
865 // provider groups, and live update callbacks) that the active
866 // ThreadComposer presents into ExistingThreadSettingsRouteProvider before
867 // pushing this screen — state no URL can reconstruct. Reached without a
868 // presented session the screen navigates straight back, so a path would
869 // only produce a flash-and-dismiss link. Deep links to a thread land on
870 // `threads/:environmentId/:threadId`, where this sheet is one tap away.
871 ThreadSettingsSheet: createNativeStackScreen({
872 screen: ExistingThreadSettingsRouteScreen,
873 options: {
874 gestureEnabled: true,
875 headerShown: false,
876 ...(Platform.OS === "android"
877 ? { presentation: "card" as const }
878 : {
879 ...FORM_SHEET_PRESENTATION_OPTIONS,
880 sheetAllowedDetents: [1],
881 sheetGrabberVisible: true,
882 }),
883 },
884 }),
885 ThreadQueue: createNativeStackScreen({
886 screen: ThreadQueueSheet,
887 options: {
888 ...FORM_SHEET_PRESENTATION_OPTIONS,
889 headerShown: false,
890 sheetAllowedDetents: [0.65, 0.95],
891 sheetGrabberVisible: true,
892 },
893 }),
894 ThreadAgents: createNativeStackScreen({
895 screen: ThreadAgentsSheet,
896 options: {
897 ...FORM_SHEET_PRESENTATION_OPTIONS,
898 headerShown: false,
899 sheetAllowedDetents: [0.5, 0.9],
900 sheetGrabberVisible: true,
901 },
902 }),
903 GitOverview: createNativeStackScreen({
904 screen: GitOverviewSheet,
905 linking: `${THREAD_LINKING_PREFIX}/git`,
906 options: {
907 ...FORM_SHEET_PRESENTATION_OPTIONS,
908 sheetAllowedDetents: [0.55, 0.92],
909 sheetGrabberVisible: true,
910 },
911 }),
912 GitCommit: createNativeStackScreen({
913 screen: GitCommitSheet,
914 linking: `${THREAD_LINKING_PREFIX}/git/commit`,
915 options: {
916 ...FORM_SHEET_PRESENTATION_OPTIONS,
917 sheetAllowedDetents: [0.55, 0.92],
918 sheetGrabberVisible: true,
919 },
920 }),
921 GitBranches: createNativeStackScreen({
922 screen: GitBranchesSheet,
923 linking: `${THREAD_LINKING_PREFIX}/git/branches`,
924 options: {
925 ...FORM_SHEET_PRESENTATION_OPTIONS,
926 sheetAllowedDetents: [0.55, 0.92],
927 sheetGrabberVisible: true,
928 },
929 }),
930 GitConfirm: createNativeStackScreen({
931 screen: GitConfirmSheet,
932 linking: `${THREAD_LINKING_PREFIX}/git-confirm`,
933 options: {
934 ...FORM_SHEET_PRESENTATION_OPTIONS,
935 sheetAllowedDetents: [0.45, 0.7],
936 sheetGrabberVisible: true,
937 },
938 }),
939 SettingsSheet: createNativeStackScreen({
940 screen: SettingsSheetStack,
941 linking: "settings",
942 options: {
943 ...NESTED_NAVIGATOR_OPTIONS,
944 gestureEnabled: true,
945 headerShown: false,
946 },
947 }),
948 SettingsLegal: createNativeStackScreen({
949 screen: SettingsLegalRouteScreen,
950 linking: "settings/legal",
951 options: {
952 ...LEGAL_DOCUMENT_HEADER_OPTIONS,
953 title: "Legal",
954 },
955 }),
956 ConnectOnboarding: createNativeStackScreen({
957 screen: ConnectOnboardingRouteScreen,
958 linking: "connect-onboarding",
959 options: {
960 // A root-level Android formSheet does not host the native stack bar;
961 // the route renders an embedded AndroidSheetHeader instead.
962 ...(Platform.OS === "android" ? { headerShown: false } : SHEET_SOLID_HEADER_OPTIONS),
963 title: "Set up T3 Connect",
964 gestureEnabled: true,
965 ...FORM_SHEET_PRESENTATION_OPTIONS,
966 sheetAllowedDetents: [0.6, 0.95],
967 sheetGrabberVisible: true,
968 },
969 }),
970 Connections: createNativeStackScreen({
971 screen: ConnectionsRouteScreen,
972 linking: "connections",
973 options: {
974 title: "Environments",
975 // Android: full page; the screen renders its own AndroidScreenHeader,
976 // so the native bar stays hidden. iOS keeps the sheet.
977 ...(Platform.OS === "android"
978 ? { presentation: "card" as const, headerShown: false }
979 : {
980 ...FORM_SHEET_PRESENTATION_OPTIONS,
981 sheetAllowedDetents: [0.55, 0.7],
982 sheetGrabberVisible: true,
983 }),
984 },
985 }),
986 ConnectionsNew: createNativeStackScreen({
987 screen: ConnectionsNewRouteScreen,
988 linking: "connections/new",
989 options: {
990 // Keep the iOS header visible from the first render so the sheet's
991 // native header stack mounts without remounting the pairing form.
992 ...(Platform.OS === "ios" ? SHEET_GLASS_HEADER_OPTIONS : undefined),
993 title: "Add Environment",
994 ...FORM_SHEET_PRESENTATION_OPTIONS,
995 sheetAllowedDetents: [0.55, 0.7],
996 sheetGrabberVisible: true,
997 },
998 }),
999 NewTaskSheet: createNativeStackScreen({
1000 screen: NewTaskSheetStack,
1001 linking: "new",
1002 // The whole new-task flow (choose project → draft → add project) shares
1003 // draft state via NewTaskFlowProvider. The expo-router era mounted it in
1004 // app/new/_layout.tsx; this layout wrapper is the native-stack equivalent.
1005 // A screen's layout replaces the navigator's screenLayout.
1006 layout: ({ children, route }) => (
1007 <GuardedScreenLayout route={route}>
1008 <NewTaskFlowProvider>
1009 <View className="flex-1 bg-sheet-solid">{children}</View>
1010 </NewTaskFlowProvider>
1011 </GuardedScreenLayout>
1012 ),
1013 options: {
1014 ...NESTED_NAVIGATOR_OPTIONS,
1015 gestureEnabled: true,
1016 headerShown: false,
1017 },
1018 }),
1019 NotFound: createNativeStackScreen({
1020 screen: NotFoundScreen,
1021 linking: "*",
1022 }),
1023 },
1024});
1025
1026function GuardedScreenLayout(props: {
1027 readonly children: ReactNode;
1028 readonly route: { readonly name: string; readonly params?: object | undefined };
1029}) {
1030 return (
1031 <RenderErrorBoundary
1032 resetKeys={[props.route.params]}
1033 renderFallback={(fallback) => (
1034 <ScreenRenderFallback {...fallback} routeName={props.route.name} />
1035 )}
1036 >
1037 {props.children}
1038 </RenderErrorBoundary>
1039 );
1040}
1041
1042function ScreenRenderFallback(props: RenderFailureProps & { readonly routeName: string }) {
1043 const navigation = useNavigation();
1044 const exit = navigation.canGoBack()
1045 ? { label: "Go back", onPress: () => navigation.goBack() }
1046 : props.routeName === "Home"
1047 ? { label: "Open settings", onPress: () => navigation.navigate("SettingsSheet") }
1048 : { label: "Return home", onPress: () => navigation.dispatch(StackActions.replace("Home")) };
1049
1050 return <RenderFailureView {...props} exit={exit} />;
1051}
1052
1053export const RootStack = RootStackConfig.with(function AdaptiveRootStack({ Navigator }) {
1054 const { width, height } = useWindowDimensions();
1055 const nativeMetrics = useNativeLayoutMetrics();
1056 const usesWorkspaceFlowScreens =
1057 Platform.OS === "android" || deriveLayout({ width, height, nativeMetrics }).usesSplitView;
1058
1059 return (
1060 <Navigator
1061 screenLayout={GuardedScreenLayout}
1062 screenOptions={({ route }) => {
1063 if (route.name !== "SettingsSheet" && route.name !== "NewTaskSheet") {
1064 return {};
1065 }
1066
1067 // Follow the workspace viewport as it resizes; compact iOS keeps sheets.
1068 return usesWorkspaceFlowScreens
1069 ? { presentation: "card" }
1070 : {
1071 ...FORM_SHEET_PRESENTATION_OPTIONS,
1072 sheetAllowedDetents: [0.92],
1073 sheetGrabberVisible: true,
1074 };
1075 }}
1076 />
1077 );
1078});
1079
1080type RootStackType = typeof RootStack;
1081
1082const navigationPathConfig = {
1083 screens: createPathConfigForStaticNavigation(RootStack) ?? {},
1084};
1085
1086declare module "@react-navigation/native" {
1087 interface RootNavigator extends RootStackType {}
1088}