Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions apps/mobile/src/Stack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,12 @@ export const RootStack = createNativeStackNavigator({
linking: "",
options: {
...GLASS_HEADER_OPTIONS,
// Android draws its own in-flow header (AndroidHomeHeader in compact,
// the sidebar brand + empty detail in split), so the native stack
// header must never mount there — runtime `headerShown` toggling does
// not reliably remove an already-shown native header and would leave
// a duplicate brand header in the split main pane.
headerShown: Platform.OS !== "android",
contentStyle: { backgroundColor: "transparent" },
headerBackVisible: false,
...getCompactBrandHeaderOptions(),
Expand All @@ -415,17 +421,31 @@ export const RootStack = createNativeStackNavigator({
Thread: createNativeStackScreen({
screen: ThreadRouteScreen,
linking: THREAD_LINKING_PREFIX,
options: GLASS_HEADER_OPTIONS,
options: {
...GLASS_HEADER_OPTIONS,
// Android draws its own in-flow header (AndroidScreenHeader in
// ThreadRouteScreen); the native stack header stays iOS-only. Keeping
// it disabled statically avoids a stale native header surviving a
// fold/unfold layout change (runtime headerShown toggling cannot
// reliably unmount an already-shown native header).
headerShown: Platform.OS !== "android",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium src/Stack.tsx:431

ThreadRouteScreen's early-return branches (OpeningThreadLoadingScreen and ThreadUnavailableScreen) render no AndroidScreenHeader, but the Thread route now statically sets headerShown: Platform.OS !== "android", so on Android those screens have no header at all — no visible back button or navigation chrome while a thread is hydrating or when it can't be opened. The same gap exists for the loading/unavailable early returns in ThreadTerminalRouteScreen and ThreadFilesTreeScreen. Consider rendering a fallback AndroidScreenHeader in those branches, or only suppressing the native header once the replacement in-flow header is actually mounted.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/Stack.tsx around line 431:

`ThreadRouteScreen`'s early-return branches (`OpeningThreadLoadingScreen` and `ThreadUnavailableScreen`) render no `AndroidScreenHeader`, but the `Thread` route now statically sets `headerShown: Platform.OS !== "android"`, so on Android those screens have no header at all — no visible back button or navigation chrome while a thread is hydrating or when it can't be opened. The same gap exists for the loading/unavailable early returns in `ThreadTerminalRouteScreen` and `ThreadFilesTreeScreen`. Consider rendering a fallback `AndroidScreenHeader` in those branches, or only suppressing the native header once the replacement in-flow header is actually mounted.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0a5f5950b: Thread/Terminal/Files early loading and unavailable states now render an in-flow AndroidScreenHeader (via a new AndroidHeaderScreen wrapper) so Android keeps navigation chrome when the native stack header is statically disabled. iOS behavior unchanged.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

},
}),
ThreadTerminal: createNativeStackScreen({
screen: ThreadTerminalRouteScreen,
linking: `${THREAD_LINKING_PREFIX}/terminal`,
options: SOLID_HEADER_OPTIONS,
options: {
...SOLID_HEADER_OPTIONS,
headerShown: Platform.OS !== "android",
},
}),
ThreadReview: createNativeStackScreen({
screen: ReviewSheet,
linking: `${THREAD_LINKING_PREFIX}/review`,
options: SOLID_HEADER_OPTIONS,
options: {
...SOLID_HEADER_OPTIONS,
headerShown: Platform.OS !== "android",
},
Comment thread
cursor[bot] marked this conversation as resolved.
}),
ThreadReviewComment: createNativeStackScreen({
screen: ReviewCommentComposerSheet,
Expand All @@ -443,6 +463,10 @@ export const RootStack = createNativeStackNavigator({
linking: `${THREAD_LINKING_PREFIX}/files`,
options: {
...GLASS_HEADER_OPTIONS,
// Android draws its own in-flow header (AndroidScreenHeader in
// ThreadFilesTreeScreen); keep the native header iOS-only statically
// so it cannot survive a fold/unfold layout change.
headerShown: Platform.OS !== "android",
contentStyle:
SHEET_BACKGROUND_COLOR !== undefined
? { backgroundColor: SHEET_BACKGROUND_COLOR }
Expand Down
32 changes: 28 additions & 4 deletions apps/mobile/src/components/AndroidScreenHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ReactNode } from "react";
import { Pressable, View } from "react-native";
import { Platform, Pressable, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";

import { SymbolView, type AppSymbolName } from "./AppSymbol";
Expand Down Expand Up @@ -63,14 +63,15 @@ export function AndroidScreenHeader(props: {
paddingTop: props.embedded ? 8 : Math.max(insets.top, 12),
}}
>
<View className="min-h-12 flex-row items-center gap-2">
<View className="min-h-12 flex-row items-center gap-1">
{props.onBack ? (
<Pressable
accessibilityLabel="Navigate up"
accessibilityRole="button"
hitSlop={8}
collapsable={false}
hitSlop={12}
onPress={props.onBack}
className="-mr-2 size-11 items-center justify-center"
className="h-12 w-12 shrink-0 items-center justify-center"
>
<SymbolView
name="chevron.left"
Expand Down Expand Up @@ -115,3 +116,26 @@ export function AndroidSheetHeader(
) {
return <AndroidScreenHeader {...props} embedded />;
}

/**
* Android in-flow screen wrapper for routes whose native stack header is
* disabled statically. Renders the AndroidScreenHeader above the content on
* Android and renders the content unchanged elsewhere (iOS keeps its native
* stack header). Used by route early states (loading / unavailable) so they
* keep navigation chrome even though the native header never mounts.
*/
export function AndroidHeaderScreen(props: {
readonly title: string;
readonly onBack?: () => void;
readonly children: ReactNode;
}) {
if (Platform.OS !== "android") {
return <>{props.children}</>;
}
return (
<View className="flex-1">
<AndroidScreenHeader title={props.title} onBack={props.onBack} />
{props.children}
</View>
);
}
3 changes: 3 additions & 0 deletions apps/mobile/src/components/CompactBrandTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export function brandTitleOffset(nativeLeadingItem: boolean): number {
*/
export function CompactBrandTitle(
props: {
readonly allowFontScaling?: boolean;
readonly nativeLeadingItem?: boolean;
} = {},
) {
Expand All @@ -57,6 +58,7 @@ export function CompactBrandTitle(
>
<T3Wordmark color={iconColor} height={15} />
<Text
allowFontScaling={props.allowFontScaling}
style={{
color: mutedColor,
fontFamily: "DMSans-Medium",
Expand All @@ -75,6 +77,7 @@ export function CompactBrandTitle(
}}
>
<Text
allowFontScaling={props.allowFontScaling}
style={{
color: mutedColor,
fontFamily: "DMSans-Bold",
Expand Down
12 changes: 11 additions & 1 deletion apps/mobile/src/components/LoadingScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,24 @@ import { BrandMark } from "./BrandMark";
export function LoadingScreen(props: {
readonly message: string;
readonly messagePlacement?: "above-spinner" | "below-spinner";
/**
* Clear the status bar for the in-flow Android header rendered above. When
* false, the screen is expected to sit under an AndroidScreenHeader that
* already owns the top inset.
*/
readonly includeTopInset?: boolean;
}) {
const colorScheme = useColorScheme();
const screenBg = useThemeColor("--color-screen");
const insets = useSafeAreaInsets();
const messagePlacement = props.messagePlacement ?? "below-spinner";
const includeTopInset = props.includeTopInset ?? true;

return (
<View className="flex-1 bg-screen" style={{ paddingTop: insets.top }}>
<View
className="flex-1 bg-screen"
style={includeTopInset ? { paddingTop: insets.top } : undefined}
>
<StatusBar
barStyle={colorScheme === "dark" ? "light-content" : "dark-content"}
backgroundColor={screenBg as string}
Expand Down
41 changes: 38 additions & 3 deletions apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import {
ThreadId,
} from "@t3tools/contracts";

import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";
import {
AndroidHeaderScreen,
AndroidScreenHeader,
type AndroidHeaderAction,
} from "../../components/AndroidScreenHeader";
import { SymbolView } from "../../components/AppSymbol";
import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText";
import { EmptyState } from "../../components/EmptyState";
Expand Down Expand Up @@ -340,11 +344,29 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {
/>
);
}
return <LoadingScreen message="Opening files..." messagePlacement="above-spinner" />;
return (
<AndroidHeaderScreen
title="Files"
onBack={navigation.canGoBack() ? () => navigation.goBack() : undefined}
>
<LoadingScreen
message="Opening files..."
messagePlacement="above-spinner"
includeTopInset={Platform.OS !== "android"}
/>
</AndroidHeaderScreen>
);
}

if (cwd === null) {
return <FilesUnavailable />;
return (
<AndroidHeaderScreen
title="Files"
onBack={navigation.canGoBack() ? () => navigation.goBack() : undefined}
>
<FilesUnavailable />
</AndroidHeaderScreen>
);
}

if (fileInspector.supported) {
Expand Down Expand Up @@ -402,6 +424,19 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {
subtitle={projectName}
onBack={handleReturnToThread}
actions={[
...(layout.usesSplitView
? [
{
accessibilityLabel: panes.primarySidebarVisible
? "Hide thread sidebar"
: "Show thread sidebar",
icon: panes.primarySidebarVisible
? "arrow.up.left.and.arrow.down.right"
: "sidebar.left",
onPress: togglePrimarySidebar,
} satisfies AndroidHeaderAction,
]
: []),
{
accessibilityLabel: "Refresh files",
icon: "arrow.clockwise",
Expand Down
31 changes: 21 additions & 10 deletions apps/mobile/src/features/home/HomeRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as Arr from "effect/Array";
import * as Order from "effect/Order";
import { useNavigation } from "@react-navigation/native";
import { useEffect, useMemo, useState } from "react";
import { Platform } from "react-native";

import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader";
import { useProjects, useThreadShells } from "../../state/entities";
Expand All @@ -24,7 +25,7 @@ import { getConnectionAwareBrandHeaderOptions } from "./WorkspaceConnectionTitle
/* ─── Route screen ───────────────────────────────────────────────────── */

export function HomeRouteScreen() {
const { layout } = useAdaptiveWorkspaceLayout();
const { layout, panes, togglePrimarySidebar } = useAdaptiveWorkspaceLayout();
const projects = useProjects();
const threads = useThreadShells();
const { environments: workspaceEnvironments, state: catalogState } = useWorkspaceState();
Expand Down Expand Up @@ -104,7 +105,11 @@ export function HomeRouteScreen() {
return (
<>
<NativeStackScreenOptions
options={{ title: "", headerTitle: "", unstable_headerLeftItems: () => [] }}
options={
Platform.OS === "android"
? { headerShown: false }
: { title: "", headerTitle: "", unstable_headerLeftItems: () => [] }
}
/>
<WorkspaceSidebarToolbar
afterSidebarButton={
Expand All @@ -117,6 +122,8 @@ export function HomeRouteScreen() {
/>
<WorkspaceEmptyDetail
onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })}
onToggleSidebar={Platform.OS === "android" ? togglePrimarySidebar : undefined}
primarySidebarVisible={panes.primarySidebarVisible}
/>
</>
);
Expand All @@ -127,15 +134,19 @@ export function HomeRouteScreen() {
onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })}
>
<>
{/* Restore the compact title after the split branch blanks the detail
header. The brand slot doubles as the connection status surface:
while an environment reconnects, the lockup fades to a status label
in place (no layout shift in the list below). */}
{/* Restore the header after leaving split view; screen options are
shallow-merged. Android never mounts the native Home header (it
draws its own in-flow header in compact and the sidebar brand in
split), so only iOS re-enables it here. The brand slot also doubles
as the connection status surface while an environment reconnects. */}
<NativeStackScreenOptions
options={getConnectionAwareBrandHeaderOptions({
onOpenEnvironments: () =>
navigation.navigate("SettingsSheet", { screen: "SettingsEnvironments" }),
})}
options={{
...getConnectionAwareBrandHeaderOptions({
onOpenEnvironments: () =>
navigation.navigate("SettingsSheet", { screen: "SettingsEnvironments" }),
}),
headerShown: Platform.OS !== "android",
}}
/>
<HomeHeader
environments={environments}
Expand Down
Loading
Loading