| ... | ... | @@ -6,7 +6,10 @@ import { |
| 6 | 6 | } from "../../state/use-composer-drafts"; |
| 7 | 7 | import { useWorktreeSetup } from "./use-worktree-setup"; |
| 8 | 8 | import { worktreeSetupAgentStarted } from "@t3tools/client-runtime/worktree-setup"; |
| 9 | | import { NativeStackScreenOptions } from "../../native/StackHeader"; |
| 9 | import { ScreenHeader } from "../../components/ScreenHeader"; |
| 10 | import { ScreenHeaderButton } from "../../components/ScreenHeaderButton"; |
| 11 | import type { ScreenHeaderAction } from "../../components/ScreenHeader.types"; |
| 12 | import { useThreadHeaderOptions } from "./useThreadHeaderOptions"; |
| 10 | 13 | import { |
| 11 | 14 | StackActions, |
| 12 | 15 | useFocusEffect, |
| ... | ... | @@ -43,12 +46,6 @@ import { useEnvironmentQuery } from "../../state/query"; |
| 43 | 46 | import { dismissGitActionResult, useGitActionProgress } from "../../state/use-vcs-action-state"; |
| 44 | 47 | import { vcsEnvironment } from "../../state/vcs"; |
| 45 | 48 | import { EmptyState } from "../../components/EmptyState"; |
| 46 | | import { |
| 47 | | AndroidHeaderIconButton, |
| 48 | | AndroidScreenHeader, |
| 49 | | type AndroidHeaderAction, |
| 50 | | } from "../../components/AndroidScreenHeader"; |
| 51 | | import { AndroidWorkspaceSidebarButton } from "../layout/workspace-sidebar-toolbar"; |
| 52 | 49 | import { LoadingScreen } from "../../components/LoadingScreen"; |
| 53 | 50 | import { scopedThreadKey } from "../../lib/scopedEntities"; |
| 54 | 51 | import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; |
| ... | ... | @@ -73,11 +70,6 @@ import { |
| 73 | 70 | } from "../terminal/terminalLaunchContext"; |
| 74 | 71 | import { terminalDebugLog } from "../terminal/terminalDebugLog"; |
| 75 | 72 | import { ThreadDetailScreen, type ThreadDetailScreenProps } from "./ThreadDetailScreen"; |
| 76 | | import { |
| 77 | | ThreadGitControls, |
| 78 | | useThreadGitCenterHeaderItems, |
| 79 | | useThreadGitRightHeaderItems, |
| 80 | | } from "./ThreadGitControls"; |
| 81 | 73 | import { GitOverviewSheet } from "./git/GitOverviewSheet"; |
| 82 | 74 | import { useAtomCommand } from "../../state/use-atom-command"; |
| 83 | 75 | import { useSelectedThreadGitActions } from "../../state/use-selected-thread-git-actions"; |
| ... | ... | @@ -93,7 +85,6 @@ import { |
| 93 | 85 | useAdaptiveWorkspacePaneRole, |
| 94 | 86 | useRegisterWorkspaceInspector, |
| 95 | 87 | } from "../layout/AdaptiveWorkspaceLayout"; |
| 96 | | import { withNativeGlassHeaderItem } from "../layout/native-glass-header-items"; |
| 97 | 88 | import { ThreadFileNavigatorPane } from "../files/thread-file-navigator-pane"; |
| 98 | 89 | import { |
| 99 | 90 | ThreadInspectorContentStack, |
| ... | ... | @@ -101,13 +92,109 @@ import { |
| 101 | 92 | } from "./thread-inspector-content-stack"; |
| 102 | 93 | import { threadRouteIsHydrating } from "./thread-route-hydration"; |
| 103 | 94 | |
| 95 | function ThreadHeader( |
| 96 | props: Parameters<typeof useThreadHeaderOptions>[0] & { |
| 97 | readonly hasThreadCwd: boolean; |
| 98 | readonly hasWorkspaceRoot: boolean; |
| 99 | readonly fileInspectorSupported: boolean; |
| 100 | readonly inspectorMode: ThreadInspectorMode | null; |
| 101 | readonly onToggleInspector: () => void; |
| 102 | readonly onOpenGitInspector: () => void; |
| 103 | readonly onOpenFilesInspector: () => void; |
| 104 | }, |
| 105 | ) { |
| 106 | const navigation = useNavigation(); |
| 107 | const { layout, panes, toggleAuxiliaryPane } = useAdaptiveWorkspaceLayout(); |
| 108 | const { onOpenTerminal } = props.gitControls; |
| 109 | const native = useThreadHeaderOptions(props); |
| 110 | const androidHeaderActions = useMemo<ReadonlyArray<ScreenHeaderAction>>(() => { |
| 111 | const actions: ScreenHeaderAction[] = []; |
| 112 | if (props.onReturnToThread) { |
| 113 | actions.push({ |
| 114 | accessibilityLabel: "Return to chat", |
| 115 | icon: "chevron.left", |
| 116 | onPress: props.onReturnToThread, |
| 117 | }); |
| 118 | } |
| 119 | if (props.hasThreadCwd) { |
| 120 | const filesVisible = props.inspectorMode === "files" && panes.auxiliaryPaneVisible; |
| 121 | actions.push({ |
| 122 | accessibilityLabel: filesVisible ? "Close files" : "Open files", |
| 123 | selected: filesVisible, |
| 124 | icon: "folder", |
| 125 | onPress: filesVisible ? toggleAuxiliaryPane : props.onOpenFilesInspector, |
| 126 | }); |
| 127 | } |
| 128 | if (props.hasWorkspaceRoot) { |
| 129 | actions.push({ |
| 130 | accessibilityLabel: "Open terminal", |
| 131 | icon: "terminal", |
| 132 | onPress: () => onOpenTerminal(null), |
| 133 | }); |
| 134 | } |
| 135 | actions.push({ |
| 136 | accessibilityLabel: "Open git controls", |
| 137 | icon: "point.topleft.down.curvedto.point.bottomright.up", |
| 138 | onPress: props.onOpenGitInspector, |
| 139 | }); |
| 140 | return actions; |
| 141 | }, [ |
| 142 | props.inspectorMode, |
| 143 | panes.auxiliaryPaneVisible, |
| 144 | props.onOpenFilesInspector, |
| 145 | onOpenTerminal, |
| 146 | props.onOpenGitInspector, |
| 147 | toggleAuxiliaryPane, |
| 148 | props.onReturnToThread, |
| 149 | props.hasThreadCwd, |
| 150 | props.hasWorkspaceRoot, |
| 151 | ]); |
| 152 | |
| 153 | return ( |
| 154 | <> |
| 155 | <ScreenHeader |
| 156 | title={props.title} |
| 157 | subtitle={props.subtitle} |
| 158 | sidebar={native.sidebar} |
| 159 | options={native.options} |
| 160 | optionsVersion={props.gitControls.projectScripts} |
| 161 | trailing={ |
| 162 | props.fileInspectorSupported && props.hasThreadCwd ? ( |
| 163 | <ScreenHeaderButton |
| 164 | accessibilityLabel={ |
| 165 | props.inspectorMode !== null && panes.auxiliaryPaneVisible |
| 166 | ? "Hide inspector" |
| 167 | : "Show inspector" |
| 168 | } |
| 169 | icon="sidebar.right" |
| 170 | selected={props.inspectorMode !== null && panes.auxiliaryPaneVisible} |
| 171 | onPress={props.onToggleInspector} |
| 172 | /> |
| 173 | ) : null |
| 174 | } |
| 175 | onBack={ |
| 176 | layout.usesSplitView |
| 177 | ? undefined |
| 178 | : () => { |
| 179 | // A deep link or cold start has no previous route; Home is the way out. |
| 180 | // Read the history at press time: it changes without re-rendering this screen. |
| 181 | if (navigation.canGoBack()) navigation.goBack(); |
| 182 | else navigation.dispatch(StackActions.replace("Home")); |
| 183 | } |
| 184 | } |
| 185 | actions={androidHeaderActions} |
| 186 | hideBottomBorder |
| 187 | /> |
| 188 | {native.fallback} |
| 189 | </> |
| 190 | ); |
| 191 | } |
| 192 | |
| 104 | 193 | interface ThreadInspectorSelection { |
| 105 | 194 | readonly routeThreadIdentity: string | null; |
| 106 | 195 | readonly mode: ThreadInspectorMode; |
| 107 | 196 | } |
| 108 | 197 | |
| 109 | | type NativeHeaderItems = ReadonlyArray<Record<string, unknown>>; |
| 110 | | |
| 111 | 198 | function InspectorPaneRoleActivation() { |
| 112 | 199 | useAdaptiveWorkspacePaneRole("inspector"); |
| 113 | 200 | return null; |
| ... | ... | @@ -237,14 +324,8 @@ function ThreadRouteContent( |
| 237 | 324 | ) { |
| 238 | 325 | const { themeVariables } = useAppearancePreferences(); |
| 239 | 326 | const headerColor = themeVariables["--color-header"]; |
| 240 | | const { |
| 241 | | fileInspector, |
| 242 | | layout, |
| 243 | | panes, |
| 244 | | showAuxiliaryPane, |
| 245 | | toggleAuxiliaryPane, |
| 246 | | togglePrimarySidebar, |
| 247 | | } = useAdaptiveWorkspaceLayout(); |
| 327 | const { fileInspector, layout, panes, showAuxiliaryPane, toggleAuxiliaryPane } = |
| 328 | useAdaptiveWorkspaceLayout(); |
| 248 | 329 | const { connectionState } = useRemoteConnectionStatus(); |
| 249 | 330 | const { onReconnectEnvironment } = useRemoteConnections(); |
| 250 | 331 | const { |
| ... | ... | @@ -701,94 +782,6 @@ function ThreadRouteContent( |
| 701 | 782 | onPull: gitActions.onPullSelectedThreadBranch, |
| 702 | 783 | onRunAction: gitActions.onRunSelectedThreadGitAction, |
| 703 | 784 | }; |
| 704 | | const threadCenterHeaderItems = useThreadGitCenterHeaderItems(threadGitControlProps); |
| 705 | | const compactRightHeaderItems = useThreadGitRightHeaderItems(threadGitControlProps); |
| 706 | | const splitLeftHeaderItems = useMemo<NativeHeaderItems>( |
| 707 | | () => [ |
| 708 | | { |
| 709 | | // Match Mail's split-view detail toolbar: the first detail action sits |
| 710 | | // inside the content pane, not flush against the sidebar divider. |
| 711 | | spacing: 18, |
| 712 | | type: "spacing" as const, |
| 713 | | }, |
| 714 | | ...(props.onReturnToThread |
| 715 | | ? [ |
| 716 | | withNativeGlassHeaderItem({ |
| 717 | | accessibilityLabel: "Return to chat", |
| 718 | | icon: { name: "chevron.left", type: "sfSymbol" as const }, |
| 719 | | identifier: "thread-left-return", |
| 720 | | onPress: props.onReturnToThread, |
| 721 | | type: "button" as const, |
| 722 | | }), |
| 723 | | ] |
| 724 | | : []), |
| 725 | | withNativeGlassHeaderItem({ |
| 726 | | accessibilityLabel: panes.primarySidebarVisible |
| 727 | | ? "Maximize content" |
| 728 | | : "Show thread sidebar", |
| 729 | | icon: { |
| 730 | | name: panes.primarySidebarVisible ? "arrow.up.left.and.arrow.down.right" : "sidebar.left", |
| 731 | | type: "sfSymbol" as const, |
| 732 | | }, |
| 733 | | identifier: "thread-left-sidebar", |
| 734 | | onPress: togglePrimarySidebar, |
| 735 | | type: "button" as const, |
| 736 | | }), |
| 737 | | withNativeGlassHeaderItem({ |
| 738 | | accessibilityLabel: "New task", |
| 739 | | icon: { name: "square.and.pencil", type: "sfSymbol" as const }, |
| 740 | | identifier: "thread-left-new-task", |
| 741 | | onPress: () => navigation.navigate("NewTaskSheet", { screen: "NewTask" }), |
| 742 | | type: "button" as const, |
| 743 | | }), |
| 744 | | ], |
| 745 | | [panes.primarySidebarVisible, props.onReturnToThread, navigation, togglePrimarySidebar], |
| 746 | | ); |
| 747 | | const androidHeaderActions = useMemo<ReadonlyArray<AndroidHeaderAction>>(() => { |
| 748 | | if (Platform.OS !== "android") return []; |
| 749 | | |
| 750 | | const actions: AndroidHeaderAction[] = []; |
| 751 | | if (props.onReturnToThread) { |
| 752 | | actions.push({ |
| 753 | | accessibilityLabel: "Return to chat", |
| 754 | | icon: "chevron.left", |
| 755 | | onPress: props.onReturnToThread, |
| 756 | | }); |
| 757 | | } |
| 758 | | if (selectedThreadCwd !== null) { |
| 759 | | const filesVisible = inspectorMode === "files" && panes.auxiliaryPaneVisible; |
| 760 | | actions.push({ |
| 761 | | accessibilityLabel: filesVisible ? "Close files" : "Open files", |
| 762 | | selected: filesVisible, |
| 763 | | icon: "folder", |
| 764 | | onPress: filesVisible ? toggleAuxiliaryPane : handleOpenFilesInspector, |
| 765 | | }); |
| 766 | | } |
| 767 | | if (selectedThreadProject?.workspaceRoot) { |
| 768 | | actions.push({ |
| 769 | | accessibilityLabel: "Open terminal", |
| 770 | | icon: "terminal", |
| 771 | | onPress: () => handleOpenTerminal(null), |
| 772 | | }); |
| 773 | | } |
| 774 | | actions.push({ |
| 775 | | accessibilityLabel: "Open git controls", |
| 776 | | icon: "point.topleft.down.curvedto.point.bottomright.up", |
| 777 | | onPress: handleOpenGitInspector, |
| 778 | | }); |
| 779 | | return actions; |
| 780 | | }, [ |
| 781 | | inspectorMode, |
| 782 | | panes.auxiliaryPaneVisible, |
| 783 | | handleOpenFilesInspector, |
| 784 | | handleOpenTerminal, |
| 785 | | handleOpenGitInspector, |
| 786 | | toggleAuxiliaryPane, |
| 787 | | props.onReturnToThread, |
| 788 | | selectedThreadCwd, |
| 789 | | selectedThreadProject?.workspaceRoot, |
| 790 | | ]); |
| 791 | | |
| 792 | 785 | const handleEditFailedCreation = useCallback(async () => { |
| 793 | 786 | const creation = selectedThreadCreation?.message; |
| 794 | 787 | if (!creation?.creation || routeThreadIdentity === null) { |
| ... | ... | @@ -929,23 +922,6 @@ function ThreadRouteContent( |
| 929 | 922 | preparingWorktree: selectedThreadCreation.message.creation?.workspaceMode === "worktree", |
| 930 | 923 | }; |
| 931 | 924 | })(); |
| 932 | | // Deep links / cold starts land with Thread as the ONLY route, where the |
| 933 | | // native back button does not render. Provide an explicit Home escape for |
| 934 | | // that case; when history exists the native back button is used instead. |
| 935 | | const canGoBack = navigation.canGoBack(); |
| 936 | | const compactHomeHeaderItems = useMemo<NativeHeaderItems>( |
| 937 | | () => [ |
| 938 | | withNativeGlassHeaderItem({ |
| 939 | | accessibilityLabel: "Go to threads list", |
| 940 | | icon: { name: "list.bullet", type: "sfSymbol" as const }, |
| 941 | | identifier: "thread-left-home", |
| 942 | | onPress: () => navigation.dispatch(StackActions.replace("Home")), |
| 943 | | type: "button" as const, |
| 944 | | }), |
| 945 | | ], |
| 946 | | [navigation], |
| 947 | | ); |
| 948 | | |
| 949 | 925 | if (!environmentId || !threadId) { |
| 950 | 926 | return <OpeningThreadLoadingScreen />; |
| 951 | 927 | } |
| ... | ... | @@ -967,10 +943,8 @@ function ThreadRouteContent( |
| 967 | 943 | connectionState: routeConnectionState, |
| 968 | 944 | }); |
| 969 | 945 | const serverConfig = routeEnvironmentRuntime?.serverConfig ?? null; |
| 970 | | const renderThreadRouteBody = (showActionControls: boolean) => ( |
| 946 | const renderThreadRouteBody = () => ( |
| 971 | 947 | <> |
| 972 | | <ThreadGitControls {...threadGitControlProps} showActionControls={showActionControls} /> |
| 973 | | |
| 974 | 948 | <GitActionProgressOverlay progress={gitActionProgress} onDismiss={dismissGitActionResult} /> |
| 975 | 949 | |
| 976 | 950 | <View |
| ... | ... | @@ -1069,84 +1043,23 @@ function ThreadRouteContent( |
| 1069 | 1043 | return ( |
| 1070 | 1044 | <> |
| 1071 | 1045 | {activeInspectorRenderer ? <InspectorPaneRoleActivation /> : null} |
| 1072 | | <NativeStackScreenOptions |
| 1073 | | optionsVersion={threadGitControlProps.projectScripts} |
| 1074 | | options={{ |
| 1075 | | // Android draws its own in-flow header (AndroidScreenHeader below); |
| 1076 | | // the native stack header stays iOS-only. |
| 1077 | | headerShown: Platform.OS !== "android", |
| 1078 | | headerTitle: selectedThread.title, |
| 1079 | | headerTitleStyle: usesNativeHeaderGlass |
| 1080 | | ? { |
| 1081 | | fontSize: 17, |
| 1082 | | fontWeight: "800", |
| 1083 | | } |
| 1084 | | : undefined, |
| 1085 | | title: selectedThread.title, |
| 1086 | | headerBackVisible: !layout.usesSplitView, |
| 1087 | | // Compact uses the NATIVE back button when a previous route exists; |
| 1088 | | // deep links / cold starts get an explicit Home button instead. |
| 1089 | | // Split view always uses its custom left items. |
| 1090 | | unstable_headerLeftItems: |
| 1091 | | Platform.OS === "ios" |
| 1092 | | ? layout.usesSplitView |
| 1093 | | ? () => splitLeftHeaderItems |
| 1094 | | : canGoBack |
| 1095 | | ? undefined |
| 1096 | | : () => compactHomeHeaderItems |
| 1097 | | : undefined, |
| 1098 | | // Search lives in the persistent sidebar, so the split header keeps |
| 1099 | | // the git controls on the RIGHT (no center items — center space is |
| 1100 | | // reserved for future breadcrumbs/status). |
| 1101 | | unstable_headerRightItems: |
| 1102 | | Platform.OS === "ios" |
| 1103 | | ? () => (layout.usesSplitView ? threadCenterHeaderItems : compactRightHeaderItems) |
| 1104 | | : undefined, |
| 1105 | | unstable_headerSubtitle: usesNativeHeaderGlass ? headerSubtitle : undefined, |
| 1106 | | contentStyle: |
| 1107 | | Platform.OS === "android" && true ? { backgroundColor: headerColor } : undefined, |
| 1108 | | }} |
| 1046 | <ThreadHeader |
| 1047 | title={selectedThread.title} |
| 1048 | subtitle={headerSubtitle} |
| 1049 | headerColor={headerColor} |
| 1050 | usesNativeHeaderGlass={usesNativeHeaderGlass} |
| 1051 | gitControls={threadGitControlProps} |
| 1052 | hasThreadCwd={selectedThreadCwd !== null} |
| 1053 | hasWorkspaceRoot={Boolean(selectedThreadProject?.workspaceRoot)} |
| 1054 | fileInspectorSupported={fileInspector.supported} |
| 1055 | inspectorMode={inspectorMode} |
| 1056 | onToggleInspector={handleToggleInspector} |
| 1057 | onOpenGitInspector={handleOpenGitInspector} |
| 1058 | onOpenFilesInspector={handleOpenFilesInspector} |
| 1059 | onReturnToThread={props.onReturnToThread} |
| 1109 | 1060 | /> |
| 1110 | 1061 | |
| 1111 | | {Platform.OS === "android" ? ( |
| 1112 | | <AndroidScreenHeader |
| 1113 | | title={selectedThread.title} |
| 1114 | | subtitle={headerSubtitle} |
| 1115 | | leading={<AndroidWorkspaceSidebarButton />} |
| 1116 | | trailing={ |
| 1117 | | fileInspector.supported && selectedThreadCwd !== null ? ( |
| 1118 | | <AndroidHeaderIconButton |
| 1119 | | accessibilityLabel={ |
| 1120 | | inspectorMode !== null && panes.auxiliaryPaneVisible |
| 1121 | | ? "Hide inspector" |
| 1122 | | : "Show inspector" |
| 1123 | | } |
| 1124 | | icon="sidebar.right" |
| 1125 | | selected={inspectorMode !== null && panes.auxiliaryPaneVisible} |
| 1126 | | onPress={handleToggleInspector} |
| 1127 | | /> |
| 1128 | | ) : null |
| 1129 | | } |
| 1130 | | onBack={ |
| 1131 | | layout.usesSplitView |
| 1132 | | ? undefined |
| 1133 | | : () => { |
| 1134 | | // A deep link or cold start has no previous route; Home is the way out. |
| 1135 | | // Read the history at press time: it changes without re-rendering this screen. |
| 1136 | | if (navigation.canGoBack()) navigation.goBack(); |
| 1137 | | else navigation.dispatch(StackActions.replace("Home")); |
| 1138 | | } |
| 1139 | | } |
| 1140 | | actions={androidHeaderActions} |
| 1141 | | hideBottomBorder |
| 1142 | | /> |
| 1143 | | ) : null} |
| 1144 | | |
| 1145 | | {/* Android surfaces the git/files/inspector actions in its in-flow |
| 1146 | | header above, so the fallback action toolbar stays iOS-only. */} |
| 1147 | | {renderThreadRouteBody( |
| 1148 | | Platform.OS !== "android" && !layout.usesSplitView && !usesNativeHeaderGlass, |
| 1149 | | )} |
| 1062 | {renderThreadRouteBody()} |
| 1150 | 1063 | </> |
| 1151 | 1064 | ); |
| 1152 | 1065 | } |