authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-18 23:58:18-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-18 23:58:18-07:00
log29a23fdf1481485593d7d90e195b7c8bf2a705a8
treede38807a6b63ea7b761333eba8701112f501867c
parent803f94e787faef498edecba5dbd07493ac8bc817
signature Signed by PGP key B5690EEEBB952194

fix(mobile): adapt workspace navigation and expand controls (#12551)


12 files changed, 109 insertions(+), 124 deletions(-)

apps/mobile/src/Stack.tsx+38-23
...@@ -11,7 +11,14 @@ import {...@@ -11,7 +11,14 @@ import {
11 type NativeStackNavigationOptions,11 type NativeStackNavigationOptions,
12} from "@react-navigation/native-stack";12} from "@react-navigation/native-stack";
13import { useEffect, useRef } from "react";13import { useEffect, useRef } from "react";
14import { Platform, Pressable, ScrollView, StyleSheet, View } from "react-native";14import {
15 Platform,
16 Pressable,
17 ScrollView,
18 StyleSheet,
19 View,
20 useWindowDimensions,
21} from "react-native";
15import { useResolveClassNames } from "uniwind";22import { useResolveClassNames } from "uniwind";
1623
17import { AppText as Text } from "./components/AppText";24import { AppText as Text } from "./components/AppText";
...@@ -93,6 +100,7 @@ import {...@@ -93,6 +100,7 @@ import {
93 transitionIncomingSharePresentation,100 transitionIncomingSharePresentation,
94} from "./features/sharing/incoming-share-presentation";101} from "./features/sharing/incoming-share-presentation";
95import { NATIVE_LIQUID_GLASS_SUPPORTED } from "./native/native-glass";102import { NATIVE_LIQUID_GLASS_SUPPORTED } from "./native/native-glass";
103import { deriveLayout } from "./lib/layout";
96import { nativeHeaderScrollEdgeEffects } from "./native/StackHeader";104import { nativeHeaderScrollEdgeEffects } from "./native/StackHeader";
97import { FORM_SHEET_PRESENTATION_OPTIONS } from "./native/sheet-surface";105import { FORM_SHEET_PRESENTATION_OPTIONS } from "./native/sheet-surface";
98import { useThreadOutboxDrain } from "./state/use-thread-outbox-drain";106import { useThreadOutboxDrain } from "./state/use-thread-outbox-drain";
...@@ -339,9 +347,8 @@ const SettingsSheetStack = createNativeStackNavigator({...@@ -339,9 +347,8 @@ const SettingsSheetStack = createNativeStackNavigator({
339// the same deep-link URLs the nested config produced.347// the same deep-link URLs the nested config produced.
340const THREAD_LINKING_PREFIX = "threads/:environmentId/:threadId";348const THREAD_LINKING_PREFIX = "threads/:environmentId/:threadId";
341349
342// New-task / add-project flow: nested navigator inside the formSheet (Settings-sheet350// New-task / add-project flow: the nested navigator owns the header and pushes
343// pattern — a plain formSheet screen cannot render a stack header; the header and351// whether the flow opens in the workspace or in a compact form sheet.
344// in-sheet pushes come from this nested stack).
345const NewTaskSheetStack = createNativeStackNavigator({352const NewTaskSheetStack = createNativeStackNavigator({
346 initialRouteName: "NewTask",353 initialRouteName: "NewTask",
347 screenOptions: {354 screenOptions: {
...@@ -570,7 +577,7 @@ function NotFoundScreen() {...@@ -570,7 +577,7 @@ function NotFoundScreen() {
570 );577 );
571}578}
572579
573export const RootStack = createNativeStackNavigator({580const RootStackConfig = createNativeStackNavigator({
574 initialRouteName: "Home",581 initialRouteName: "Home",
575 layout: RootStackLayout,582 layout: RootStackLayout,
576 screenOptions: {583 screenOptions: {
...@@ -698,15 +705,6 @@ export const RootStack = createNativeStackNavigator({...@@ -698,15 +705,6 @@ export const RootStack = createNativeStackNavigator({
698 options: {705 options: {
699 gestureEnabled: true,706 gestureEnabled: true,
700 headerShown: false,707 headerShown: false,
701 // Android pushes settings as a regular full page with an in-screen
702 // back header; iOS keeps the detented form sheet.
703 ...(Platform.OS === "android"
704 ? { presentation: "card" as const }
705 : {
706 ...FORM_SHEET_PRESENTATION_OPTIONS,
707 sheetAllowedDetents: [0.92],
708 sheetGrabberVisible: true,
709 }),
710 },708 },
711 }),709 }),
712 SettingsLegal: createNativeStackScreen({710 SettingsLegal: createNativeStackScreen({
...@@ -770,15 +768,6 @@ export const RootStack = createNativeStackNavigator({...@@ -770,15 +768,6 @@ export const RootStack = createNativeStackNavigator({
770 options: {768 options: {
771 gestureEnabled: true,769 gestureEnabled: true,
772 headerShown: false,770 headerShown: false,
773 // Android pushes the flow as a regular full page — the draft should
774 // read like a thread that just doesn't exist yet; iOS keeps the sheet.
775 ...(Platform.OS === "android"
776 ? { presentation: "card" as const }
777 : {
778 ...FORM_SHEET_PRESENTATION_OPTIONS,
779 sheetAllowedDetents: [0.92],
780 sheetGrabberVisible: true,
781 }),
782 },771 },
783 }),772 }),
784 NotFound: createNativeStackScreen({773 NotFound: createNativeStackScreen({
...@@ -787,6 +776,32 @@ export const RootStack = createNativeStackNavigator({...@@ -787,6 +776,32 @@ export const RootStack = createNativeStackNavigator({
787 }),776 }),
788 },777 },
789});778});
779
780export const RootStack = RootStackConfig.with(function AdaptiveRootStack({ Navigator }) {
781 const { width, height } = useWindowDimensions();
782 const usesWorkspaceFlowScreens =
783 Platform.OS === "android" || deriveLayout({ width, height }).usesSplitView;
784
785 return (
786 <Navigator
787 screenOptions={({ route }) => {
788 if (route.name !== "SettingsSheet" && route.name !== "NewTaskSheet") {
789 return {};
790 }
791
792 // Follow the workspace viewport as it resizes; compact iOS keeps sheets.
793 return usesWorkspaceFlowScreens
794 ? { presentation: "card" }
795 : {
796 ...FORM_SHEET_PRESENTATION_OPTIONS,
797 sheetAllowedDetents: [0.92],
798 sheetGrabberVisible: true,
799 };
800 }}
801 />
802 );
803});
804
790type RootStackType = typeof RootStack;805type RootStackType = typeof RootStack;
791806
792const navigationPathConfig = {807const navigationPathConfig = {
apps/mobile/src/components/AndroidScreenHeader.tsx+1-7
...@@ -25,13 +25,7 @@ export function AndroidHeaderIconButton(props: {...@@ -25,13 +25,7 @@ export function AndroidHeaderIconButton(props: {
25 readonly disabled?: boolean;25 readonly disabled?: boolean;
26 readonly selected?: boolean;26 readonly selected?: boolean;
27}) {27}) {
28 return (28 return <MaterialIconButton {...props} tintColorClassName="accent-header-foreground" />;
29 <MaterialIconButton
30 {...props}
31 tintColorClassName="accent-header-foreground"
32 variant={props.selected ? "tonal" : "standard"}
33 />
34 );
35}29}
3630
37export function AndroidScreenHeader(props: {31export function AndroidScreenHeader(props: {
apps/mobile/src/components/AppSymbol.tsx+2-2
...@@ -19,7 +19,7 @@ import IconArrowUp from "@tabler/icons-react-native/IconArrowUp";...@@ -19,7 +19,7 @@ import IconArrowUp from "@tabler/icons-react-native/IconArrowUp";
19import IconArrowUpCircle from "@tabler/icons-react-native/IconArrowUpCircle";19import IconArrowUpCircle from "@tabler/icons-react-native/IconArrowUpCircle";
20import IconArrowUpRight from "@tabler/icons-react-native/IconArrowUpRight";20import IconArrowUpRight from "@tabler/icons-react-native/IconArrowUpRight";
21import IconArrowUpRightCircle from "@tabler/icons-react-native/IconArrowUpRightCircle";21import IconArrowUpRightCircle from "@tabler/icons-react-native/IconArrowUpRightCircle";
22import IconArrowsMaximize from "@tabler/icons-react-native/IconArrowsMaximize";22import IconArrowsDiagonal2 from "@tabler/icons-react-native/IconArrowsDiagonal2";
23import IconArrowsMinimize from "@tabler/icons-react-native/IconArrowsMinimize";23import IconArrowsMinimize from "@tabler/icons-react-native/IconArrowsMinimize";
24import IconBellRinging from "@tabler/icons-react-native/IconBellRinging";24import IconBellRinging from "@tabler/icons-react-native/IconBellRinging";
25import IconBolt from "@tabler/icons-react-native/IconBolt";25import IconBolt from "@tabler/icons-react-native/IconBolt";
...@@ -122,7 +122,7 @@ const ANDROID_ICON_BY_SF_SYMBOL = {...@@ -122,7 +122,7 @@ const ANDROID_ICON_BY_SF_SYMBOL = {
122 "arrow.turn.left.up": IconArrowBackUp,122 "arrow.turn.left.up": IconArrowBackUp,
123 "arrow.up": IconArrowUp,123 "arrow.up": IconArrowUp,
124 "arrow.up.circle": IconArrowUpCircle,124 "arrow.up.circle": IconArrowUpCircle,
125 "arrow.up.left.and.arrow.down.right": IconArrowsMaximize,125 "arrow.up.left.and.arrow.down.right": IconArrowsDiagonal2,
126 "arrow.down.right.and.arrow.up.left": IconArrowsMinimize,126 "arrow.down.right.and.arrow.up.left": IconArrowsMinimize,
127 "arrow.up.right": IconArrowUpRight,127 "arrow.up.right": IconArrowUpRight,
128 "arrow.up.right.circle": IconArrowUpRightCircle,128 "arrow.up.right.circle": IconArrowUpRightCircle,
apps/mobile/src/components/ScreenHeader.tsx+17-13
...@@ -103,7 +103,7 @@ export function ScreenHeader(props: ScreenHeaderProps) {...@@ -103,7 +103,7 @@ export function ScreenHeader(props: ScreenHeaderProps) {
103 ...props.options,103 ...props.options,
104 }}104 }}
105 />105 />
106 {props.sidebar !== false && layout.usesSplitView ? (106 {layout.usesSplitView && (props.sidebar !== false || props.backInSplitView) ? (
107 <NativeHeaderToolbar placement="left">107 <NativeHeaderToolbar placement="left">
108 {props.backInSplitView && (props.backInSplitView.onPress || props.onBack) ? (108 {props.backInSplitView && (props.backInSplitView.onPress || props.onBack) ? (
109 <ScreenHeaderButton109 <ScreenHeaderButton
...@@ -111,18 +111,22 @@ export function ScreenHeader(props: ScreenHeaderProps) {...@@ -111,18 +111,22 @@ export function ScreenHeader(props: ScreenHeaderProps) {
111 onPress={props.backInSplitView.onPress ?? props.onBack}111 onPress={props.backInSplitView.onPress ?? props.onBack}
112 />112 />
113 ) : null}113 ) : null}
114 <ScreenHeaderButton114 {props.sidebar !== false ? (
115 accessibilityLabel={115 <ScreenHeaderButton
116 panes.primarySidebarVisible ? `Maximize ${props.title.toLowerCase()}` : "Show threads"116 accessibilityLabel={
117 }117 panes.primarySidebarVisible
118 icon={118 ? `Maximize ${props.title.toLowerCase()}`
119 panes.primarySidebarVisible ? "arrow.up.left.and.arrow.down.right" : "sidebar.left"119 : "Show threads"
120 }120 }
121 onPress={togglePrimarySidebar}121 icon={
122 separateBackground={122 panes.primarySidebarVisible ? "arrow.up.left.and.arrow.down.right" : "sidebar.left"
123 props.backInSplitView ? props.backInSplitView.separateBackground : true123 }
124 }124 onPress={togglePrimarySidebar}
125 />125 separateBackground={
126 props.backInSplitView ? props.backInSplitView.separateBackground : true
127 }
128 />
129 ) : null}
126 </NativeHeaderToolbar>130 </NativeHeaderToolbar>
127 ) : null}131 ) : null}
128 {(props.actions?.length ||132 {(props.actions?.length ||
apps/mobile/src/features/home/HomeRouteScreen.tsx+4-10
...@@ -11,10 +11,6 @@ import { useWorkspaceState } from "../../state/workspace";...@@ -11,10 +11,6 @@ import { useWorkspaceState } from "../../state/workspace";
11import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry";11import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry";
12import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout";12import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout";
13import { WorkspaceEmptyDetail } from "../layout/WorkspaceEmptyDetail";13import { WorkspaceEmptyDetail } from "../layout/WorkspaceEmptyDetail";
14import {
15 AndroidWorkspaceSidebarButton,
16 WorkspaceSidebarToolbar,
17} from "../layout/workspace-sidebar-toolbar";
18import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";14import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";
19import { checkForAppUpdateOnLaunch, startAppUpdateForegroundRecheck } from "../updates/app-updates";15import { checkForAppUpdateOnLaunch, startAppUpdateForegroundRecheck } from "../updates/app-updates";
20import { AndroidHomeFabLayout } from "./AndroidHomeFab";16import { AndroidHomeFabLayout } from "./AndroidHomeFab";
...@@ -121,18 +117,16 @@ export function HomeRouteScreen() {...@@ -121,18 +117,16 @@ export function HomeRouteScreen() {
121 : { title: "", headerTitle: "", unstable_headerLeftItems: () => [] }117 : { title: "", headerTitle: "", unstable_headerLeftItems: () => [] }
122 }118 }
123 />119 />
124 <WorkspaceSidebarToolbar120 {Platform.OS === "ios" ? (
125 afterSidebarButton={121 <NativeHeaderToolbar placement="left">
126 <NativeHeaderToolbar.Button122 <NativeHeaderToolbar.Button
127 accessibilityLabel="New task"123 accessibilityLabel="New task"
128 icon="square.and.pencil"124 icon="square.and.pencil"
129 onPress={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })}125 onPress={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })}
130 />126 />
131 }127 </NativeHeaderToolbar>
132 />
133 {Platform.OS === "android" ? (
134 <AndroidScreenHeader title="Threads" leading={<AndroidWorkspaceSidebarButton />} />
135 ) : null}128 ) : null}
129 {Platform.OS === "android" ? <AndroidScreenHeader title="Threads" /> : null}
136 <WorkspaceEmptyDetail130 <WorkspaceEmptyDetail
137 onAddConnection={131 onAddConnection={
138 Platform.OS === "android" && !catalogState.hasConnections132 Platform.OS === "android" && !catalogState.hasConnections
apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx+14-14
...@@ -238,6 +238,7 @@ function AdaptiveWorkspaceLayoutContent(...@@ -238,6 +238,7 @@ function AdaptiveWorkspaceLayoutContent(
238 const navigation = useNavigation();238 const navigation = useNavigation();
239 const activeRoleOwner = useRef<symbol | null>(null);239 const activeRoleOwner = useRef<symbol | null>(null);
240 const [primarySidebarPreferredVisible, setPrimarySidebarPreferredVisible] = useState(true);240 const [primarySidebarPreferredVisible, setPrimarySidebarPreferredVisible] = useState(true);
241 const showPrimarySidebar = pathname === "/" || primarySidebarPreferredVisible;
241 const [supplementaryPanePreferredVisible, setSupplementaryPanePreferredVisible] = useState(true);242 const [supplementaryPanePreferredVisible, setSupplementaryPanePreferredVisible] = useState(true);
242 const [supplementaryPanePreferredWidth, setSupplementaryPanePreferredWidth] = useState<243 const [supplementaryPanePreferredWidth, setSupplementaryPanePreferredWidth] = useState<
243 number | null244 number | null
...@@ -262,17 +263,9 @@ function AdaptiveWorkspaceLayoutContent(...@@ -262,17 +263,9 @@ function AdaptiveWorkspaceLayoutContent(
262 viewportWidth: width,263 viewportWidth: width,
263 preferredWidth: fileInspectorPreferredWidth ?? undefined,264 preferredWidth: fileInspectorPreferredWidth ?? undefined,
264 reservedLeadingWidth:265 reservedLeadingWidth:
265 shouldRenderPrimarySidebar && primarySidebarPreferredVisible266 shouldRenderPrimarySidebar && showPrimarySidebar ? (layout.listPaneWidth ?? 0) : 0,
266 ? (layout.listPaneWidth ?? 0)
267 : 0,
268 }),267 }),
269 [268 [fileInspectorPreferredWidth, layout, showPrimarySidebar, shouldRenderPrimarySidebar, width],
270 fileInspectorPreferredWidth,
271 layout,
272 primarySidebarPreferredVisible,
273 shouldRenderPrimarySidebar,
274 width,
275 ],
276 );269 );
277 const auxiliaryPaneRole: WorkspaceAuxiliaryPaneRole =270 const auxiliaryPaneRole: WorkspaceAuxiliaryPaneRole =
278 focusedAuxiliaryPaneRole ?? (/\/files(?:\/|$)/.test(pathname) ? "inspector" : "supplementary");271 focusedAuxiliaryPaneRole ?? (/\/files(?:\/|$)/.test(pathname) ? "inspector" : "supplementary");
...@@ -289,7 +282,7 @@ function AdaptiveWorkspaceLayoutContent(...@@ -289,7 +282,7 @@ function AdaptiveWorkspaceLayoutContent(
289 deriveWorkspacePaneLayout({282 deriveWorkspacePaneLayout({
290 layout,283 layout,
291 viewportWidth: width,284 viewportWidth: width,
292 primarySidebarPreferredVisible,285 primarySidebarPreferredVisible: showPrimarySidebar,
293 auxiliaryPanePreferredVisible,286 auxiliaryPanePreferredVisible,
294 auxiliaryPaneRole,287 auxiliaryPaneRole,
295 auxiliaryPanePreferredWidth: auxiliaryPanePreferredWidth ?? undefined,288 auxiliaryPanePreferredWidth: auxiliaryPanePreferredWidth ?? undefined,
...@@ -299,7 +292,7 @@ function AdaptiveWorkspaceLayoutContent(...@@ -299,7 +292,7 @@ function AdaptiveWorkspaceLayoutContent(
299 auxiliaryPaneRole,292 auxiliaryPaneRole,
300 auxiliaryPanePreferredWidth,293 auxiliaryPanePreferredWidth,
301 layout,294 layout,
302 primarySidebarPreferredVisible,295 showPrimarySidebar,
303 width,296 width,
304 ],297 ],
305 );298 );
...@@ -357,13 +350,16 @@ function AdaptiveWorkspaceLayoutContent(...@@ -357,13 +350,16 @@ function AdaptiveWorkspaceLayoutContent(
357 };350 };
358 }, []);351 }, []);
359 const togglePrimarySidebar = useCallback(() => {352 const togglePrimarySidebar = useCallback(() => {
353 if (pathname === "/") {
354 return;
355 }
360 if (!panes.primarySidebarVisible && panes.primarySidebarSuppressedByAuxiliary) {356 if (!panes.primarySidebarVisible && panes.primarySidebarSuppressedByAuxiliary) {
361 setFileInspectorPreferredVisible(false);357 setFileInspectorPreferredVisible(false);
362 setPrimarySidebarPreferredVisible(true);358 setPrimarySidebarPreferredVisible(true);
363 return;359 return;
364 }360 }
365 setPrimarySidebarPreferredVisible((current) => !current);361 setPrimarySidebarPreferredVisible((current) => !current);
366 }, [panes.primarySidebarSuppressedByAuxiliary, panes.primarySidebarVisible]);362 }, [panes.primarySidebarSuppressedByAuxiliary, panes.primarySidebarVisible, pathname]);
367 const revealPrimarySidebar = useCallback(() => {363 const revealPrimarySidebar = useCallback(() => {
368 if (panes.primarySidebarSuppressedByAuxiliary) {364 if (panes.primarySidebarSuppressedByAuxiliary) {
369 setFileInspectorPreferredVisible(false);365 setFileInspectorPreferredVisible(false);
...@@ -374,7 +370,11 @@ function AdaptiveWorkspaceLayoutContent(...@@ -374,7 +370,11 @@ function AdaptiveWorkspaceLayoutContent(
374 togglePrimarySidebar();370 togglePrimarySidebar();
375 return true;371 return true;
376 }, [togglePrimarySidebar]);372 }, [togglePrimarySidebar]);
377 useHardwareKeyboardCommand("toggleSidebar", handleToggleSidebarCommand);373 const sidebarCommands = useMemo(
374 () => (pathname === "/" ? [] : (["toggleSidebar"] as const)),
375 [pathname],
376 );
377 useHardwareKeyboardCommand(sidebarCommands, handleToggleSidebarCommand);
378 const showAuxiliaryPane = useCallback((role: WorkspaceAuxiliaryPaneRole) => {378 const showAuxiliaryPane = useCallback((role: WorkspaceAuxiliaryPaneRole) => {
379 if (role === "inspector") {379 if (role === "inspector") {
380 setFocusedAuxiliaryPaneRole("inspector");380 setFocusedAuxiliaryPaneRole("inspector");
apps/mobile/src/features/layout/workspace-sidebar-toolbar.tsx+2-34
...@@ -1,5 +1,3 @@...@@ -1,5 +1,3 @@
1import { NativeHeaderToolbar } from "../../native/StackHeader";
2import type { ReactNode } from "react";
3import { Platform } from "react-native";1import { Platform } from "react-native";
42
5import { AndroidHeaderIconButton } from "../../components/AndroidScreenHeader";3import { AndroidHeaderIconButton } from "../../components/AndroidScreenHeader";
...@@ -12,39 +10,9 @@ export function AndroidWorkspaceSidebarButton() {...@@ -12,39 +10,9 @@ export function AndroidWorkspaceSidebarButton() {
1210
13 return (11 return (
14 <AndroidHeaderIconButton12 <AndroidHeaderIconButton
15 accessibilityLabel={13 accessibilityLabel={panes.primarySidebarVisible ? "Maximize content" : "Show thread sidebar"}
16 panes.primarySidebarVisible ? "Hide thread sidebar" : "Show thread sidebar"14 icon={panes.primarySidebarVisible ? "arrow.up.left.and.arrow.down.right" : "sidebar.left"}
17 }
18 icon="sidebar.left"
19 selected={panes.primarySidebarVisible}
20 onPress={togglePrimarySidebar}15 onPress={togglePrimarySidebar}
21 />16 />
22 );17 );
23}18}
24
25export function WorkspaceSidebarToolbar(
26 props: {
27 readonly children?: ReactNode;
28 readonly afterSidebarButton?: ReactNode;
29 } = {},
30) {
31 const { layout, panes, togglePrimarySidebar } = useAdaptiveWorkspaceLayout();
32
33 if (Platform.OS === "android" || !layout.usesSplitView) {
34 return null;
35 }
36
37 return (
38 <NativeHeaderToolbar placement="left">
39 {props.children}
40 <NativeHeaderToolbar.Button
41 accessibilityLabel={
42 panes.primarySidebarVisible ? "Maximize content" : "Show thread sidebar"
43 }
44 icon={panes.primarySidebarVisible ? "arrow.up.left.and.arrow.down.right" : "sidebar.left"}
45 onPress={togglePrimarySidebar}
46 />
47 {props.afterSidebarButton}
48 </NativeHeaderToolbar>
49 );
50}
apps/mobile/src/features/settings/SettingsRouteScreen.tsx+13-2
...@@ -6,7 +6,8 @@ import { deriveProjectGroupLabel } from "@t3tools/client-runtime/state/project-g...@@ -6,7 +6,8 @@ import { deriveProjectGroupLabel } from "@t3tools/client-runtime/state/project-g
6import { useSafeAreaInsets } from "react-native-safe-area-context";6import { useSafeAreaInsets } from "react-native-safe-area-context";
77
8import { hasCloudPublicConfig } from "../cloud/publicConfig";8import { hasCloudPublicConfig } from "../cloud/publicConfig";
9import { WorkspaceSidebarToolbar } from "../layout/workspace-sidebar-toolbar";9import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout";
10import { NativeHeaderToolbar } from "../../native/StackHeader";
10import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry";11import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry";
11import { SettingsRow } from "./components/SettingsRow";12import { SettingsRow } from "./components/SettingsRow";
12import { SettingsSection } from "./components/SettingsSection";13import { SettingsSection } from "./components/SettingsSection";
...@@ -18,6 +19,8 @@ import {...@@ -18,6 +19,8 @@ import {
18import { useSettingsEnvironmentFilter } from "./settings-environment-filter";19import { useSettingsEnvironmentFilter } from "./settings-environment-filter";
1920
20export function SettingsRouteScreen() {21export function SettingsRouteScreen() {
22 const navigation = useNavigation();
23 const { layout } = useAdaptiveWorkspaceLayout();
21 const content = hasCloudPublicConfig() ? (24 const content = hasCloudPublicConfig() ? (
22 <ConfiguredSettingsRouteScreen />25 <ConfiguredSettingsRouteScreen />
23 ) : (26 ) : (
...@@ -26,7 +29,15 @@ export function SettingsRouteScreen() {...@@ -26,7 +29,15 @@ export function SettingsRouteScreen() {
2629
27 return (30 return (
28 <>31 <>
29 <WorkspaceSidebarToolbar />32 {Platform.OS === "ios" && layout.usesSplitView ? (
33 <NativeHeaderToolbar placement="left">
34 <NativeHeaderToolbar.Button
35 accessibilityLabel="Go back"
36 icon="chevron.left"
37 onPress={() => navigation.goBack()}
38 />
39 </NativeHeaderToolbar>
40 ) : null}
30 <SettingsEnvironmentFilterHeader closeSettings />41 <SettingsEnvironmentFilterHeader closeSettings />
31 {Platform.OS === "android" ? (42 {Platform.OS === "android" ? (
32 <SettingsScreen title="Settings" trailing={<AndroidSettingsEnvironmentFilter />}>43 <SettingsScreen title="Settings" trailing={<AndroidSettingsEnvironmentFilter />}>
apps/mobile/src/features/settings/components/SettingsEnvironmentFilterHeader.tsx+5-1
...@@ -5,10 +5,13 @@ import { ControlPillMenu } from "../../../components/ControlPill";...@@ -5,10 +5,13 @@ import { ControlPillMenu } from "../../../components/ControlPill";
5import { SymbolView } from "../../../components/AppSymbol";5import { SymbolView } from "../../../components/AppSymbol";
6import { NativeStackScreenOptions } from "../../../native/StackHeader";6import { NativeStackScreenOptions } from "../../../native/StackHeader";
7import { withNativeGlassHeaderItem } from "../../layout/native-glass-header-items";7import { withNativeGlassHeaderItem } from "../../layout/native-glass-header-items";
8import { useAdaptiveWorkspaceLayout } from "../../layout/AdaptiveWorkspaceLayout";
8import { useSettingsEnvironmentFilter } from "../settings-environment-filter";9import { useSettingsEnvironmentFilter } from "../settings-environment-filter";
910
10export function SettingsEnvironmentFilterHeader(props: { readonly closeSettings?: boolean }) {11export function SettingsEnvironmentFilterHeader(props: { readonly closeSettings?: boolean }) {
11 const navigation = useNavigation();12 const navigation = useNavigation();
13 const { layout } = useAdaptiveWorkspaceLayout();
14 const closeSettings = props.closeSettings === true && !layout.usesSplitView;
12 const {15 const {
13 availableTargets,16 availableTargets,
14 selectedTargets,17 selectedTargets,
...@@ -26,6 +29,7 @@ export function SettingsEnvironmentFilterHeader(props: { readonly closeSettings?...@@ -26,6 +29,7 @@ export function SettingsEnvironmentFilterHeader(props: { readonly closeSettings?
26 ? "line.3.horizontal.decrease"29 ? "line.3.horizontal.decrease"
27 : "line.3.horizontal.decrease.circle.fill";30 : "line.3.horizontal.decrease.circle.fill";
28 const filterVersion = JSON.stringify({31 const filterVersion = JSON.stringify({
32 closeSettings,
29 selection: selectedIds === null ? null : [...selectedIds].sort(),33 selection: selectedIds === null ? null : [...selectedIds].sort(),
30 targets: availableTargets.map((entry) => [entry.environmentId, entry.label, entry.displayUrl]),34 targets: availableTargets.map((entry) => [entry.environmentId, entry.label, entry.displayUrl]),
31 project: selectedProjectKey,35 project: selectedProjectKey,
...@@ -94,7 +98,7 @@ export function SettingsEnvironmentFilterHeader(props: { readonly closeSettings?...@@ -94,7 +98,7 @@ export function SettingsEnvironmentFilterHeader(props: { readonly closeSettings?
94 ],98 ],
95 },99 },
96 }),100 }),
97 ...(props.closeSettings101 ...(closeSettings
98 ? [102 ? [
99 withNativeGlassHeaderItem({103 withNativeGlassHeaderItem({
100 accessibilityLabel: "Close settings",104 accessibilityLabel: "Close settings",
apps/mobile/src/features/settings/components/SettingsScreen.tsx+1-1
...@@ -23,7 +23,7 @@ export function SettingsScreen(...@@ -23,7 +23,7 @@ export function SettingsScreen(
23 title={props.title}23 title={props.title}
24 actions={props.actions}24 actions={props.actions}
25 trailing={Platform.OS === "android" ? props.trailing : undefined}25 trailing={Platform.OS === "android" ? props.trailing : undefined}
26 sidebar={Platform.OS === "android" && !props.formSheet}26 sidebar={false}
27 onBack={() => navigation.goBack()}27 onBack={() => navigation.goBack()}
28 hideBottomBorder={!props.formSheet}28 hideBottomBorder={!props.formSheet}
29 />29 />
apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx+2-3
...@@ -100,9 +100,8 @@ function TerminalHeader(props: {...@@ -100,9 +100,8 @@ function TerminalHeader(props: {
100 subtitle={props.subtitle}100 subtitle={props.subtitle}
101 onBack={props.onCloseTerminal}101 onBack={props.onCloseTerminal}
102 backInSplitView={{102 backInSplitView={{
103 accessibilityLabel: "Close terminal",103 accessibilityLabel: "Back to chat",
104 icon: "xmark",104 icon: "chevron.left",
105 separateBackground: true,
106 }}105 }}
107 menus={106 menus={
108 props.isEnvironmentReady107 props.isEnvironmentReady
apps/mobile/src/features/threads/NewTaskRouteScreen.tsx+10-14
...@@ -98,28 +98,24 @@ function NewTaskHeader(props: {...@@ -98,28 +98,24 @@ function NewTaskHeader(props: {
98 title={props.title}98 title={props.title}
99 subtitle={props.subtitle ?? undefined}99 subtitle={props.subtitle ?? undefined}
100 sidebar={false}100 sidebar={false}
101 backInSplitView={{
102 accessibilityLabel: "Go back",
103 icon: "chevron.left",
104 }}
105 options={{ headerBackVisible: !layout.usesSplitView }}
101 hideBottomBorder106 hideBottomBorder
102 onBack={() => navigation.goBack()}107 onBack={() => navigation.goBack()}
103 actions={[108 actions={
104 ...(Platform.OS === "ios" && layout.usesSplitView109 props.canAddProject
105 ? [
106 {
107 accessibilityLabel: "Close new task",
108 icon: "xmark" as const,
109 onPress: () => navigation.goBack(),
110 },
111 ]
112 : []),
113 ...(props.canAddProject
114 ? [110 ? [
115 {111 {
116 accessibilityLabel: "Add project",112 accessibilityLabel: "Add project",
117 icon: "plus" as const,113 icon: "plus",
118 onPress: () => navigation.dispatch(StackActions.push("AddProject")),114 onPress: () => navigation.dispatch(StackActions.push("AddProject")),
119 },115 },
120 ]116 ]
121 : []),117 : []
122 ]}118 }
123 search={{119 search={{
124 value: props.searchText,120 value: props.searchText,
125 onChangeText: props.onSearchTextChange,121 onChangeText: props.onSearchTextChange,