fix(app): make session rename reachable in the mobile sessions dropdown (#2924)

* fix(app): make session rename reachable in the mobile sessions dropdown

The per-session actions menu in the mobile tab switcher (a gorhom bottom
sheet) opened as an anchored popover, which on native renders a second RN
Modal stacked on the sheet — the fragile nested-overlay case docs/floating-panels.md
warns about. The actions menu (incl. Rename) never appeared on native, so
sessions could not be renamed from the dropdown.

Give the menu compactMode="sheet" like every other mobile menu in the file,
so it stacks as a gorhom sheet on a gorhom sheet (the supported path). Extract
the accessory into workspace-tab-trailing-accessory.tsx and add a reachability
test that fails if the sheet presentation regresses.

* test(app): cover compact session rename via the tab switcher e2e

Adds a Playwright spec that drives the mobile-viewport sessions dropdown -> per-session actions sheet -> Rename, exercising the real gorhom sheet interaction the fix restores (the desktop rename specs only cover the right-click context menu, and the unit test mocks the menu engine).

---------

Co-authored-by: Klaude <klaude@Klaudes-MacBook-Pro.local>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
Kevin Aspesi
2026-08-08 21:12:16 +00:00
committed by GitHub
co-authored by Klaude Mohamed Boudra
parent 1e0ce69c54
commit 49b257c55a
4 changed files with 451 additions and 112 deletions
@@ -20,21 +20,15 @@ import * as Clipboard from "expo-clipboard";
import { useTranslation } from "react-i18next";
import { DiffStat } from "@/components/diff-stat";
import {
CopyX,
ArrowLeftToLine,
ArrowRightToLine,
ChevronDown,
Copy,
Ellipsis,
Globe,
Import as ImportIcon,
PanelRight,
Pencil,
RotateCw,
Settings,
SquarePen,
SquareTerminal,
X,
} from "lucide-react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { StyleSheet, withUnistyles } from "react-native-unistyles";
@@ -128,13 +122,13 @@ import {
useWorkspaceTabRename,
WorkspaceTabRenameModal,
} from "@/screens/workspace/use-workspace-tab-rename";
import { MobileTabTrailingAccessory } from "@/screens/workspace/workspace-tab-trailing-accessory";
import {
WorkspaceDesktopTabsRow,
type WorkspaceDesktopTabRowItem,
} from "@/screens/workspace/workspace-desktop-tabs-row";
import {
buildWorkspaceTabMenuEntries,
type WorkspaceTabMenuEntry,
type WorkspaceTabMenuLabels,
} from "@/screens/workspace/workspace-tab-menu";
import { useDesktopBrowserNewTabRequests } from "@/desktop/browser/new-tab-requests";
@@ -249,12 +243,6 @@ const ThemedLoadingSpinner = withUnistyles(LoadingSpinner);
const ThemedEllipsis = withUnistyles(Ellipsis);
const ThemedChevronDown = withUnistyles(ChevronDown);
const ThemedCopy = withUnistyles(Copy);
const ThemedRotateCw = withUnistyles(RotateCw);
const ThemedArrowLeftToLine = withUnistyles(ArrowLeftToLine);
const ThemedArrowRightToLine = withUnistyles(ArrowRightToLine);
const ThemedCopyX = withUnistyles(CopyX);
const ThemedPencil = withUnistyles(Pencil);
const ThemedX = withUnistyles(X);
const ThemedSquarePen = withUnistyles(SquarePen);
const ThemedSquareTerminal = withUnistyles(SquareTerminal);
const ThemedGlobe = withUnistyles(Globe);
@@ -516,95 +504,10 @@ function WorkspaceDocumentTitleEffect({
function noop() {}
function mobileTabMenuTriggerStyle({ open, pressed }: { open?: boolean; pressed?: boolean }) {
return [
styles.mobileTabMenuTrigger,
(Boolean(open) || Boolean(pressed)) && styles.mobileTabMenuTriggerActive,
];
}
function switcherTriggerStyle({ pressed }: { pressed?: boolean }) {
return [styles.switcherTrigger, Boolean(pressed) && styles.switcherTriggerPressed];
}
function MobileTabTrailingAccessory({
menuTestIDBase,
presentationLabel,
menuEntries,
}: {
menuTestIDBase: string;
presentationLabel: string;
menuEntries: WorkspaceTabMenuEntry[];
}) {
const { t } = useTranslation();
return (
<DropdownMenu>
<DropdownMenuTrigger
testID={`${menuTestIDBase}-trigger`}
accessibilityRole="button"
accessibilityLabel={t("workspace.tabs.menu.openFor", { label: presentationLabel })}
hitSlop={8}
style={mobileTabMenuTriggerStyle}
>
<ThemedEllipsis size={14} uniProps={mutedColorMapping} />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" width={220} testID={menuTestIDBase}>
{menuEntries.map((entry) =>
entry.kind === "separator" ? (
<DropdownMenuSeparator key={entry.key} />
) : (
<MobileTabDropdownMenuItem key={entry.key} entry={entry} />
),
)}
</DropdownMenuContent>
</DropdownMenu>
);
}
function MobileTabDropdownMenuItem({
entry,
}: {
entry: Extract<WorkspaceTabMenuEntry, { kind: "item" }>;
}) {
const leading = useMemo(() => {
switch (entry.icon) {
case "copy":
return <ThemedCopy size={16} uniProps={mutedColorMapping} />;
case "rotate-cw":
return <ThemedRotateCw size={16} uniProps={mutedColorMapping} />;
case "arrow-left-to-line":
return <ThemedArrowLeftToLine size={16} uniProps={mutedColorMapping} />;
case "arrow-right-to-line":
return <ThemedArrowRightToLine size={16} uniProps={mutedColorMapping} />;
case "copy-x":
return <ThemedCopyX size={16} uniProps={mutedColorMapping} />;
case "pencil":
return <ThemedPencil size={16} uniProps={mutedColorMapping} />;
case "x":
return <ThemedX size={16} uniProps={mutedColorMapping} />;
default:
return undefined;
}
}, [entry.icon]);
const trailing = useMemo(
() => (entry.hint ? <Text style={styles.menuItemHint}>{entry.hint}</Text> : undefined),
[entry.hint],
);
return (
<DropdownMenuItem
testID={entry.testID}
disabled={entry.disabled}
destructive={entry.destructive}
onSelect={entry.onSelect}
tooltip={entry.tooltip}
leading={leading}
trailing={trailing}
>
{entry.label}
</DropdownMenuItem>
);
}
function MobileWorkspaceTabOption({
tab,
tabIndex,
@@ -4170,20 +4073,6 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
},
mobileTabMenuTrigger: {
width: 28,
height: 28,
borderRadius: theme.borderRadius.md,
alignItems: "center",
justifyContent: "center",
},
mobileTabMenuTriggerActive: {
backgroundColor: theme.colors.surface2,
},
menuItemHint: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
},
headerMenuProfileIconWrapper: {
width: 16,
height: 16,