diff --git a/apps/mobile/src/components/ui/AppBar.tsx b/apps/mobile/src/components/ui/AppBar.tsx
index 7f76d9106613..2f593bcba163 100644
--- a/apps/mobile/src/components/ui/AppBar.tsx
+++ b/apps/mobile/src/components/ui/AppBar.tsx
@@ -10,7 +10,7 @@ import { NativeStackScreenOptions, useScreenOptionsWriter } from "../../native/S
import { useNativeWorkspaceColumnsSupported } from "../../native/NativeWorkspaceColumns";
import { useDeviceType } from "../../native/native-layout-metrics";
import { isDuoDevice } from "../../lib/device-type";
-import { useAdaptiveWorkspaceLayout } from "../../features/layout/AdaptiveWorkspaceLayout";
+import { useAdaptiveWorkspaceLayout } from "../../features/layout/adaptive-workspace-context";
import { useSidebarToggle } from "../../features/layout/useSidebarToggle";
import { InspectorCloseContext } from "../../features/layout/inspector-bar-store";
import {
diff --git a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx
index 19ffe072fb58..e9c6f4f6bab9 100644
--- a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx
+++ b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx
@@ -112,10 +112,11 @@ import {
workspaceTabColumns,
workspaceTabNavigationState,
} from "./workspace/workspace-tab-navigation";
-import type { WorkspaceTab } from "./workspace/workspace-tabs-state";
+import { workspaceUsesColumns, type WorkspaceTab } from "./workspace/workspace-tabs-state";
import { workspaceRoutesOf } from "./workspace-overlay-routes";
import { SettingsWorkspaceLayout } from "../settings/SettingsWorkspaceLayout";
import { WorkspaceThreadSidebar } from "./workspace/WorkspaceThreadSidebar";
+import { WorkspaceSettingsSidebar } from "../settings/WorkspaceSettingsSidebar";
export { useAdaptiveWorkspaceLayout, useWorkspacePaneState };
@@ -263,11 +264,11 @@ function AdaptiveWorkspaceLayoutContent(
const { width: viewportWidth, height } = nativeMetrics ?? windowDimensions;
const pathname = props.pathname;
const workspace = useWorkspaceTabs();
- // Settings and Usage occupy the workspace rather than a list/detail pair.
+ // Android workspace pages fill the viewport; legacy iPad pages retain their sidebar navigation.
+ //
+ // Android 工作区页面铺满窗口;旧版 iPad 保留侧栏导航,避免全屏内容与局部导航错位。
const androidSettingsOpen = useAndroidWorkspaceRoot()?.settingsOpen ?? false;
const safeAreaInsets = useSafeAreaInsets();
- const workspacePage =
- Platform.OS === "android" && (androidSettingsOpen || workspace?.state.selectedTab === "usage");
const navigation = useNavigation();
const [primarySidebarPreferredVisible, setPrimarySidebarPreferredVisible] = useState(true);
const [fileInspectorPreferredVisible, setFileInspectorPreferredVisible] = useState(true);
@@ -290,6 +291,18 @@ function AdaptiveWorkspaceLayoutContent(
() => deriveLayout({ width: viewportWidth, height, nativeMetrics, nativeWorkspace }),
[height, viewportWidth, nativeMetrics, nativeWorkspace],
);
+ const workspacePage =
+ (Platform.OS === "android" &&
+ (androidSettingsOpen || workspace?.state.selectedTab === "usage")) ||
+ (Platform.OS === "ios" &&
+ !nativeWorkspace &&
+ workspace !== null &&
+ !workspaceUsesColumns(
+ workspace.state.selectedTab,
+ workspace.settingsOpen,
+ nativeWorkspace,
+ baseLayout.usesSplitView,
+ ));
// The overlay sidebar closes when a page opens from it, and stays open on a list or on the
// page a tab switch opened (`keepOpenFor`, set by that switch).
const [overlaySidebar, setOverlaySidebar] = useState({
@@ -1010,8 +1023,15 @@ function AdaptiveWorkspaceLayoutContent(
/>
)}
>
- {/* Without a sidebar route (iPadOS before 26) the columns draw the thread list. */}
- {sidebar ?? }
+ {/* The legacy sidebar owns its list while the root navigator owns detail pages.
+
+ 兼容侧栏承载目录,详情仍由根导航管理,切换设置时不再隐藏整列。 */}
+ {sidebar ??
+ (Platform.OS === "ios" && workspace?.settingsOpen ? (
+
+ ) : (
+
+ ))}
{/* Keep the pill under the content throughout the sidebar's clipping animation. */}
diff --git a/apps/mobile/src/features/layout/createWorkspaceStackNavigator.ios.tsx b/apps/mobile/src/features/layout/createWorkspaceStackNavigator.ios.tsx
index bac954d99f11..eaa212393538 100644
--- a/apps/mobile/src/features/layout/createWorkspaceStackNavigator.ios.tsx
+++ b/apps/mobile/src/features/layout/createWorkspaceStackNavigator.ios.tsx
@@ -522,7 +522,12 @@ function WorkspaceStackView(props: ViewProps & { readonly nativeColumns: boolean
const descriptors = props.descriptors;
const baseRoute = { key: `${props.state.key}:workspace`, name: "Workspace" };
const visibleRoute = projection.detail.at(-1) ?? primaryRoute;
- const workspacePage = !workspaceUsesColumns(workspace.state.selectedTab, workspace.settingsOpen);
+ const workspacePage = !workspaceUsesColumns(
+ workspace.state.selectedTab,
+ workspace.settingsOpen,
+ props.nativeColumns,
+ layout.usesSplitView,
+ );
const baseDescriptor: Descriptor = {
...primaryDescriptor,
navigation: props.descriptors[visibleRoute.key]!.navigation,
@@ -649,8 +654,13 @@ function WorkspaceStackNavigator({
);
return (
-
-
+ {/* The fallback sidebar is rendered by NavigationContent's layout, outside its children.
+ Share tab state above that layout so sidebar actions and detail tabs use one owner.
+
+ 兼容侧栏由 NavigationContent 的布局绘制,位于其 children 外部;
+ 将标签状态提升到布局之上,使侧栏入口与详情导航共用同一份状态。 */}
+
+
-
-
+
+
);
}
diff --git a/apps/mobile/src/features/layout/workspace/WorkspaceNativeTabs.ios.tsx b/apps/mobile/src/features/layout/workspace/WorkspaceNativeTabs.ios.tsx
index 2247ea50c9a4..3ea0e4b55616 100644
--- a/apps/mobile/src/features/layout/workspace/WorkspaceNativeTabs.ios.tsx
+++ b/apps/mobile/src/features/layout/workspace/WorkspaceNativeTabs.ios.tsx
@@ -3,7 +3,7 @@ import {
NAVIGATION_TAB_ICONS,
useEnabledNavigationTabs,
} from "./navigation-tabs";
-import { useLayoutEffect, useRef, useState, type ReactNode } from "react";
+import { use, useLayoutEffect, useRef, useState, type ReactNode } from "react";
import { View, useWindowDimensions, type NativeSyntheticEvent } from "react-native";
import { Tabs, type TabSelectedEvent, type TabSelectionRejectedEvent } from "react-native-screens";
@@ -16,6 +16,7 @@ import {
} from "./workspace-tabs-state";
import {
NativeWorkspaceTabActivityContext,
+ NativeWorkspaceModeContext,
WorkspaceSlotOptionsContext,
} from "../../../native/v5-workspace-context";
import {
@@ -83,6 +84,7 @@ export function WorkspaceNativeTabs(props: {
const selectTab = useSelectWorkspaceTab();
const [provenance, setProvenance] = useState(0);
const systemTabBar = useSystemWorkspaceTabBar();
+ const nativeColumns = use(NativeWorkspaceModeContext);
const deviceType = useDeviceType();
const compactTabs = useCompactWorkspaceTabs();
const { layout } = useAdaptiveWorkspaceLayout();
@@ -91,7 +93,12 @@ export function WorkspaceNativeTabs(props: {
const hiddenWithSidebar =
deviceType === "pad" &&
layout.usesSplitView &&
- workspaceUsesColumns(workspace.state.selectedTab, workspace.settingsOpen) &&
+ workspaceUsesColumns(
+ workspace.state.selectedTab,
+ workspace.settingsOpen,
+ nativeColumns,
+ layout.usesSplitView,
+ ) &&
!panes.primarySidebarVisible;
const floatingTabs = useFloatingWorkspaceTabs();
const { width } = useWindowDimensions();
@@ -161,9 +168,17 @@ export function WorkspaceNativeTabs(props: {
{(deviceType === "pad" ||
compactTabs ||
(layout.variant === "medium" &&
- !workspaceUsesColumns(selectedTab, workspace.settingsOpen))) &&
+ !workspaceUsesColumns(selectedTab, workspace.settingsOpen, nativeColumns))) &&
!props.hidden &&
!props.tabsInSidebarColumn &&
+ // The legacy split sidebar owns its own tabs; this host sits in the detail column.
+ //
+ // 兼容分栏的侧栏自行绘制导航,此容器位于右侧详情,不能重复绘制。
+ !(
+ !nativeColumns &&
+ layout.usesSplitView &&
+ workspaceUsesColumns(selectedTab, workspace.settingsOpen, nativeColumns)
+ ) &&
!hiddenWithSidebar ? (
{props.children}
+ {/* Keep legacy iPad navigation in its sidebar; the detail host previously covered the composer.
+
+ 旧版 iPad 的导航跟随侧栏,避免此前挂在详情容器上遮挡输入框。 */}
+ {deviceType === "pad" && workspace !== null ? : null}
);
}
diff --git a/apps/mobile/src/features/layout/workspace/workspace-tab-navigation.test.ts b/apps/mobile/src/features/layout/workspace/workspace-tab-navigation.test.ts
index 3f217fd70c8b..fea6688a690b 100644
--- a/apps/mobile/src/features/layout/workspace/workspace-tab-navigation.test.ts
+++ b/apps/mobile/src/features/layout/workspace/workspace-tab-navigation.test.ts
@@ -51,6 +51,18 @@ describe("workspace tab selections", () => {
describe("opening a workspace tab", () => {
const columns = { columns: true, check: available };
+ it("opens the remembered Settings detail beside its list on legacy iPads", () => {
+ const next = workspaceTabNavigationState(
+ { routes: [home, thread] },
+ "settings",
+ { settings: "SettingsAppearance" },
+ { columns: workspaceTabColumns("ios", "settings", true), check: available },
+ );
+ expect(names(next)).toEqual(["Home", "Settings", "SettingsAppearance"]);
+ expect(workspaceTabColumns("ios", "threads", true)).toBe(true);
+ expect(workspaceTabColumns("ios", "settings", false)).toBe(false);
+ });
+
it("opens Android Settings beside its default page only in a wide window", () => {
const open = (wide: boolean) =>
workspaceTabNavigationState(
diff --git a/apps/mobile/src/features/layout/workspace/workspace-tab-navigation.ts b/apps/mobile/src/features/layout/workspace/workspace-tab-navigation.ts
index 6a44e617e53e..082fbb50a700 100644
--- a/apps/mobile/src/features/layout/workspace/workspace-tab-navigation.ts
+++ b/apps/mobile/src/features/layout/workspace/workspace-tab-navigation.ts
@@ -22,6 +22,9 @@ export function workspaceTabColumns(
tab: WorkspaceTab | "settings",
columns: boolean,
) {
+ // Both iPad column implementations host Settings lists, so restore the detail beside either one.
+ //
+ // 两种 iPad 分栏都承载设置目录,恢复右侧设置页,避免兼容布局只显示全屏目录。
return columns && (platform === "ios" || (platform === "android" && tab === "settings"));
}
diff --git a/apps/mobile/src/features/layout/workspace/workspace-tabs-state.test.ts b/apps/mobile/src/features/layout/workspace/workspace-tabs-state.test.ts
index f1c839630eb3..c1e10fcd72e3 100644
--- a/apps/mobile/src/features/layout/workspace/workspace-tabs-state.test.ts
+++ b/apps/mobile/src/features/layout/workspace/workspace-tabs-state.test.ts
@@ -53,6 +53,16 @@ describe("Duo list clearance", () => {
});
describe("workspace root navigation", () => {
+ it("keeps every legacy iPad tab in columns while retaining native Usage layout", () => {
+ expect(workspaceUsesColumns("threads", true, false)).toBe(true);
+ expect(workspaceUsesColumns("threads", true, true)).toBe(true);
+ expect(workspaceUsesColumns("usage", true, false)).toBe(true);
+ expect(workspaceUsesColumns("usage", true, true)).toBe(true);
+ expect(workspaceUsesColumns("threads", false, false)).toBe(true);
+ expect(workspaceUsesColumns("usage", false, false)).toBe(true);
+ expect(workspaceUsesColumns("usage", false, false, false)).toBe(false);
+ expect(workspaceUsesColumns("threads", true, false, false)).toBe(false);
+ });
it("pairs Settings, chats and pull requests with a detail column, and keeps Usage one page", () => {
expect(workspaceUsesColumns("usage")).toBe(false);
expect(workspaceUsesColumns("usage", true)).toBe(true);
diff --git a/apps/mobile/src/features/layout/workspace/workspace-tabs-state.ts b/apps/mobile/src/features/layout/workspace/workspace-tabs-state.ts
index 3598894115dc..5a9c188443d5 100644
--- a/apps/mobile/src/features/layout/workspace/workspace-tabs-state.ts
+++ b/apps/mobile/src/features/layout/workspace/workspace-tabs-state.ts
@@ -16,9 +16,22 @@ export type WorkspaceTab = NavigationTab;
export type WorkspaceSearchTab = WorkspaceTab | "settings";
export type WorkspaceSearchMode = "resting" | "active" | "expanded";
-/** Settings, Chats and pull requests pair a list with a detail column; Usage is one page. */
-export function workspaceUsesColumns(tab: WorkspaceTab, settingsOpen = false) {
- return settingsOpen || tab === "threads" || tab === "pullRequests";
+/** Native Usage is one page; the legacy workspace keeps its sidebar for every tab.
+ *
+ * 原生分栏的用量为单页,兼容工作区的每个标签均保留侧栏。
+ */
+export function workspaceUsesColumns(
+ tab: WorkspaceTab,
+ settingsOpen = false,
+ nativeColumns = true,
+ splitView = true,
+) {
+ // Legacy iPad tabs retain their sidebar; full-page Settings and Usage stranded its navigation.
+ //
+ // 旧版 iPad 各标签保留侧栏,避免设置和用量铺满页面后导航仍滞留在左下角。
+ return settingsOpen
+ ? nativeColumns || splitView
+ : (!nativeColumns && splitView) || tab === "threads" || tab === "pullRequests";
}
export interface WorkspaceTabState {
diff --git a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx
index a4177bfdd337..0c5d58ef6915 100644
--- a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx
+++ b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx
@@ -16,7 +16,7 @@ import { ScreenScrollView as ScrollView } from "../../components/ui/ScreenScroll
import { EmptyState } from "../../components/ui/EmptyState";
import { useAuth, useUser } from "@clerk/expo";
import { useNavigation, useNavigationState } from "@react-navigation/native";
-import { use, type ComponentProps } from "react";
+import { use, type ComponentProps, type ReactNode } from "react";
import { Platform, View } from "react-native";
import { useNativeColumnLayoutMetrics } from "../../native/native-layout-metrics";
import { deriveProjectGroupLabel } from "@t3tools/client-runtime/state/project-grouping";
@@ -65,13 +65,14 @@ function useSettingsListPlacement() {
return { sidebar, selected };
}
-function SettingsListContent() {
+export function SettingsListContent() {
return hasCloudPublicConfig() ? : ;
}
export function SettingsListScreen(props: {
readonly sidebar: boolean;
readonly selected: string | null;
+ readonly children?: ReactNode;
}) {
const workspace = useWorkspaceTabs();
const environmentMenu = useSettingsEnvironmentMenu();
@@ -93,7 +94,7 @@ export function SettingsListScreen(props: {
: undefined
}
>
-
+ {props.children ?? }
diff --git a/apps/mobile/src/features/settings/WorkspaceSettingsSidebar.test.tsx b/apps/mobile/src/features/settings/WorkspaceSettingsSidebar.test.tsx
new file mode 100644
index 000000000000..3efb2b040c1e
--- /dev/null
+++ b/apps/mobile/src/features/settings/WorkspaceSettingsSidebar.test.tsx
@@ -0,0 +1,145 @@
+import { act, type ContextType, type ReactNode } from "react";
+import { createRoot } from "react-dom/client";
+import { describe, expect, it, vi } from "vite-plus/test";
+import {
+ NavigationContainerRefContext,
+ NavigationContext,
+ type NavigationState,
+} from "@react-navigation/native";
+
+vi.mock("@react-navigation/native", async () => {
+ // Resolve native's own core so the test exercises its real navigation-context fallback.
+ //
+ // 使用 native 实际依赖的 core,验证真实的导航上下文回退行为。
+ const { createRequire } = await import("node:module");
+ const require = createRequire(import.meta.url);
+ const core = createRequire(require.resolve("@react-navigation/native")).resolve(
+ "@react-navigation/core",
+ );
+ const actual = await import(core);
+ return {
+ ...actual,
+ useNavigationState: (select: (state: NavigationState) => unknown) =>
+ select(actual.useNavigation().getState()),
+ };
+});
+vi.mock("react-native", () => ({
+ Platform: {
+ OS: "ios",
+ select: (values: { ios?: unknown; default?: unknown }) => values.ios ?? values.default,
+ },
+ I18nManager: { isRTL: false },
+ Linking: {},
+}));
+vi.mock("../../components/ui/ListRow", () => ({
+ ListRow: ({ label, onPress }: { label: string; onPress: () => void }) => (
+
+ ),
+}));
+vi.mock("../../components/ui/ListRowLayout", () => ({ ListRowLayout: () => null }));
+vi.mock("../../components/ui/RowPressable", () => ({ RowPressable: () => null }));
+vi.mock("./components/SettingsSection", async () => {
+ const { createContext } = await import("react");
+ return { SettingsSidebarContext: createContext(null) };
+});
+vi.mock("./SettingsRouteScreen", async () => {
+ const { SettingsRow } = await import("./components/SettingsRow");
+ return {
+ SettingsListScreen: ({ children }: { children: ReactNode }) => children,
+ SettingsListContent: () => (
+
+ ),
+ };
+});
+vi.mock("../layout/workspace/WorkspaceSidebarColumn", async () => {
+ const { NavigationContainerRefContext, NavigationContext } =
+ await import("@react-navigation/native");
+ return {
+ WorkspaceSidebarColumn: ({ children }: { children: ReactNode }) => {
+ const standalone = {
+ getState: () => ({
+ key: "standalone",
+ routes: [{ key: "standalone-route", name: "Standalone" }],
+ index: 0,
+ }),
+ getParent: () => undefined,
+ dispatch: () => {},
+ };
+ return (
+ }
+ >
+ {children}
+
+ );
+ },
+ };
+});
+
+import { WorkspaceSettingsSidebar } from "./WorkspaceSettingsSidebar";
+
+describe("legacy Settings sidebar navigation", () => {
+ it.each([false, true])(
+ "opens General in the workspace with a screen navigation context of %s",
+ (screenContext) => {
+ const state = {
+ key: "workspace",
+ type: "stack",
+ stale: false,
+ index: 2,
+ routeNames: ["Home", "Settings", "SettingsEnvironments", "SettingsOrganization"],
+ routes: [
+ { key: "home", name: "Home" },
+ { key: "settings", name: "Settings" },
+ { key: "environments", name: "SettingsEnvironments" },
+ ],
+ };
+ const dispatch = vi.fn();
+ const navigation = {
+ getState: () => state,
+ getRootState: () => state,
+ getParent: () => undefined,
+ addListener: () => () => {},
+ dispatch,
+ };
+ const container = document.createElement("div");
+ const root = createRoot(container);
+ try {
+ act(() =>
+ root.render(
+ }
+ >
+ )
+ : undefined
+ }
+ >
+
+
+ ,
+ ),
+ );
+ act(() => container.querySelector("button")!.click());
+ expect(dispatch).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: "RESET",
+ target: "workspace",
+ payload: expect.objectContaining({
+ routeNames: state.routeNames,
+ routes: [
+ state.routes[0],
+ state.routes[1],
+ { name: "SettingsOrganization", params: undefined },
+ ],
+ }),
+ }),
+ );
+ } finally {
+ act(() => root.unmount());
+ }
+ },
+ );
+});
diff --git a/apps/mobile/src/features/settings/WorkspaceSettingsSidebar.tsx b/apps/mobile/src/features/settings/WorkspaceSettingsSidebar.tsx
new file mode 100644
index 000000000000..8d830965c428
--- /dev/null
+++ b/apps/mobile/src/features/settings/WorkspaceSettingsSidebar.tsx
@@ -0,0 +1,35 @@
+import { use } from "react";
+import {
+ NavigationContainerRefContext,
+ NavigationContext,
+ NavigationRouteContext,
+ useNavigationState,
+} from "@react-navigation/native";
+
+import { WorkspaceSidebarColumn } from "../layout/workspace/WorkspaceSidebarColumn";
+import { SettingsListContent, SettingsListScreen } from "./SettingsRouteScreen";
+
+export function WorkspaceSettingsSidebar(props: { readonly width: number }) {
+ const navigation = use(NavigationContext);
+ const container = use(NavigationContainerRefContext);
+ const state = useNavigationState((state) => state);
+ const index = state.routes.findIndex((route) => route.name === "Settings");
+ const route = state.routes[index];
+ if (!route) return null;
+ return (
+
+
+ {/* Root layouts can lack a screen navigation object; preserve the container fallback too.
+
+ 根布局可能没有页面导航对象;同时恢复容器回退,避免目录操作落到独立侧栏。 */}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/mobile/src/features/settings/components/SettingsScreen.tsx b/apps/mobile/src/features/settings/components/SettingsScreen.tsx
index 240ac009bdc5..2f74d0250e60 100644
--- a/apps/mobile/src/features/settings/components/SettingsScreen.tsx
+++ b/apps/mobile/src/features/settings/components/SettingsScreen.tsx
@@ -45,8 +45,12 @@ export function SettingsScreen(
search={props.search}
trailing={props.trailing}
menus={props.menus}
- onBack={workspaceRoot ? undefined : back}
- guardBack={props.guardBack}
+ // Hide system Back as well as the callback, so the first detail cannot reveal a second list.
+ //
+ // 首个详情页同时隐藏系统返回并禁用回滑,避免右列退回后重复显示设置目录。
+ onBack={workspaceRoot || back.columnRoot ? undefined : back.onBack}
+ onBackToList={!workspaceRoot && back.columnRoot ? back.onBack : undefined}
+ guardBack={props.guardBack || back.columnRoot}
hideBottomBorder
/>
diff --git a/apps/mobile/src/features/settings/useSettingsPageBack.ts b/apps/mobile/src/features/settings/useSettingsPageBack.ts
index 6ce909183410..e8df55db4809 100644
--- a/apps/mobile/src/features/settings/useSettingsPageBack.ts
+++ b/apps/mobile/src/features/settings/useSettingsPageBack.ts
@@ -5,9 +5,9 @@ import { resetAndroidWorkspace } from "../../lib/android-workspace-navigation";
import { useHardwareBack } from "../../lib/backHandler";
import { useAdaptiveWorkspaceLayout } from "../layout/adaptive-workspace-context";
-/**
- * The page's Back action, or undefined for the first page beside the Settings list on wide
- * Android: that page roots the detail column, so Back leaves Settings rather than emptying it.
+/** The first Settings detail roots its column; nested pages can navigate back within it.
+ *
+ * 设置首个详情页为右列根页,后续子页才可在列内返回。
*/
export function useSettingsPageBack() {
const navigation = useNavigation();
@@ -15,11 +15,13 @@ export function useSettingsPageBack() {
const { layout } = useAdaptiveWorkspaceLayout();
const routes = navigation.getState()?.routes ?? [];
const below = routes[routes.findIndex((candidate) => candidate.key === route.key) - 1];
- const columnRoot =
- Platform.OS === "android" && layout.usesSplitView && below?.name === "Settings";
- useHardwareBack(columnRoot, () => {
+ // The first detail page roots either split layout; Back must not duplicate the sidebar list.
+ //
+ // 两种分栏的首个设置页均为详情根页,避免返回后在右列重复显示左侧目录。
+ const columnRoot = layout.usesSplitView && below?.name === "Settings";
+ useHardwareBack(Platform.OS === "android" && columnRoot, () => {
resetAndroidWorkspace(navigation);
return true;
});
- return columnRoot ? undefined : () => navigation.goBack();
+ return { columnRoot, onBack: () => navigation.goBack() };
}
diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
index 3d0253e2d3a2..9cb74429bef5 100644
--- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
+++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
@@ -48,6 +48,8 @@ import { usePendingTaskListActions } from "../home/usePendingTaskListActions";
import { useThreadListActions } from "../home/useThreadListActions";
import { useFabScroll } from "../home/FabScrollContext";
import { WorkspaceListHeader } from "../layout/workspace/WorkspaceListHeader";
+import { useDeviceType } from "../../native/native-layout-metrics";
+import { workspaceDockBottomClearance } from "../layout/workspace/workspace-tabs-state";
import { WorkspaceSidebarColumn } from "../layout/workspace/WorkspaceSidebarColumn";
import {
ThreadListV2PendingRow,
@@ -110,6 +112,7 @@ export function ThreadNavigationSidebar(props: ThreadNavigationSidebarProps) {
function ThreadNavigationSidebarPane(props: ThreadNavigationSidebarProps) {
const floatingTabsEnabled = useAndroidFloatingTabs();
+ const deviceType = useDeviceType();
const { themeVariables: materialTheme } = useAppearancePreferences();
const drawerColor = materialTheme["--color-drawer"];
@@ -597,7 +600,16 @@ function ThreadNavigationSidebarPane(props: ThreadNavigationSidebarProps) {
contentContainerStyle={[
styles.threadListContent,
{
- paddingBottom: Math.max(insets.bottom, 16) + 16,
+ // The iPad dock now belongs to this column; its last row must scroll above it.
+ //
+ // iPad 导航现归属此侧栏,末行需能滚动到导航上方,避免底部继续被遮挡。
+ paddingBottom:
+ deviceType === "pad"
+ ? workspaceDockBottomClearance({
+ bottomInset: insets.bottom,
+ keyboardHeight: 0,
+ })
+ : Math.max(insets.bottom, 16) + 16,
paddingTop: 6,
},
]}
diff --git a/apps/mobile/vite.config.ts b/apps/mobile/vite.config.ts
index 046a2dc97b23..42bdfd799a00 100644
--- a/apps/mobile/vite.config.ts
+++ b/apps/mobile/vite.config.ts
@@ -4,6 +4,7 @@ import { defineConfig, normalizePath } from "vite-plus";
import "vite-plus/test/config";
const reactTests = [
+ "src/features/settings/WorkspaceSettingsSidebar.test.tsx",
"src/features/threads/git/GitBranchesSheet.test.tsx",
"src/features/threads/new-task-flow-provider.test.tsx",
"src/features/layout/workspace/workspace-tabs-state.test.ts",