diff --git a/mobile/src/layout/host-sidebar-reveal.test.tsx b/mobile/src/layout/host-sidebar-reveal.test.tsx index 6605de9f7b1..d33a936fa29 100644 --- a/mobile/src/layout/host-sidebar-reveal.test.tsx +++ b/mobile/src/layout/host-sidebar-reveal.test.tsx @@ -1,5 +1,6 @@ import { createElement, type ReactNode } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' +import { Pressable } from 'react-native' import { afterEach, describe, expect, it, vi } from 'vitest' import { createFakeBridgePortPair } from '../mobile-web-shell/bridge/bridge-port-pair-test-harness' import { RpcClientProvider } from '../transport/client-context.web' @@ -124,7 +125,9 @@ describe('host sidebar reveal in the session header', () => { } function buttons(label: string) { - return tree().root.findAllByProps({ accessibilityLabel: label }) + return tree().root.findAll( + (node) => node.type === Pressable && node.props.accessibilityLabel === label + ) } it('offers no reveal in a page beside the native sidebar, which it does not draw', async () => { diff --git a/mobile/src/session/MobileSessionHeader.tsx b/mobile/src/session/MobileSessionHeader.tsx index 8aff620e24c..12fd1c88f40 100644 --- a/mobile/src/session/MobileSessionHeader.tsx +++ b/mobile/src/session/MobileSessionHeader.tsx @@ -4,7 +4,6 @@ import { ChevronLeft, Folder, GitBranch, MoreHorizontal, PanelLeftOpen } from 'l import { MobileSessionHeaderIconButton } from './MobileSessionHeaderIconButton' import { MobileSessionTabStrip } from './MobileSessionTabStrip' import { StatusDot } from '../components/StatusDot' -import { colors } from '../theme/mobile-theme' import { styles } from './mobile-session-styles' import { useHostSidebarReveal } from '../layout/host-sidebar-reveal' import { useResponsiveLayout } from '../layout/responsive-layout' @@ -38,25 +37,17 @@ export function MobileSessionHeader({ controller }: { controller: MobileSessionC {revealSidebar ? ( - [styles.backButton, pressed && styles.backButtonPressed]} - onPress={revealSidebar} - hitSlop={8} - accessibilityRole="button" + - - + icon={PanelLeftOpen} + onPress={revealSidebar} + /> ) : null} - [styles.backButton, pressed && styles.backButtonPressed]} - onPress={requestLeaveSession} - hitSlop={8} - accessibilityRole="button" + - - + icon={ChevronLeft} + onPress={requestLeaveSession} + /> onPress: () => void } +// Why: every header and tab-strip button shares this one look and a 44pt touch area (36pt + 4pt slop each side). export function MobileSessionHeaderIconButton({ active = false, + disabled = false, accessibilityLabel, icon: Icon, onPress @@ -25,12 +28,15 @@ export function MobileSessionHeaderIconButton({ return ( [ - styles.filesButton, - pressed && styles.filesButtonPressed, - active && styles.filesButtonActive + styles.headerButton, + pressed && styles.headerButtonPressed, + active && styles.headerButtonActive, + disabled && styles.headerButtonDisabled ]} + disabled={disabled} onPress={onPress} - hitSlop={8} + hitSlop={4} + accessibilityRole="button" accessibilityLabel={accessibilityLabel} > diff --git a/mobile/src/session/MobileSessionTabStrip.tsx b/mobile/src/session/MobileSessionTabStrip.tsx index e1f0888c7e3..ead6cc64dc6 100644 --- a/mobile/src/session/MobileSessionTabStrip.tsx +++ b/mobile/src/session/MobileSessionTabStrip.tsx @@ -1,6 +1,6 @@ import { useCallback } from 'react' import { View, Text, ScrollView, Pressable } from 'react-native' -import { File, FileText, Globe, Plus } from 'lucide-react-native' +import { File, FileText, Globe, Plus, SquareTerminal } from 'lucide-react-native' import { triggerMediumImpact } from '../platform/haptics' import { MobileAgentIcon } from '../components/MobileAgentIcon' import { @@ -8,6 +8,7 @@ import { resolveMobileTerminalTabAgentId } from './mobile-terminal-tab-agent' import { colors } from '../theme/mobile-theme' +import { MobileSessionHeaderIconButton } from './MobileSessionHeaderIconButton' import { QuickCommandsTabButton } from './QuickCommandsTabButton' import { styles } from './mobile-session-styles' import { useKeyboardPersistingTaps } from '../platform/keyboard-persisting-taps' @@ -100,6 +101,9 @@ export function MobileSessionTabStrip({ } }} onPress={() => switchSessionTab(t)} + accessibilityRole="tab" + accessibilityLabel={getMobileSessionTabTitle(t)} + accessibilityState={{ selected: t.id === activeSessionTabId }} onLongPress={() => { triggerMediumImpact() openSessionTabActionSheetAfterKeyboardDismiss(t) @@ -120,7 +124,11 @@ export function MobileSessionTabStrip({ {t.type === 'terminal' && (() => { const agentId = resolveMobileTerminalTabAgentId(t) - return agentId ? : null + return agentId ? ( + + ) : ( + + ) })()} {/* Why: pinned outside the scroll strip so the new-agent button stays reachable however far the tabs scroll. */} - [ - styles.newTerminalButton, - pressed && styles.newTerminalButtonPressed, - (creating || creatingBrowser || creatingMarkdown || connState !== 'connected') && - styles.newTerminalButtonDisabled - ]} + { setCreateError('') setShowCreateTabDrawer(true) }} accessibilityLabel="New tab" - > - - + icon={Plus} + /> {/* Why: stable placement matters, while old hosts must stay gated because they strip agentPrompt. */} - [ - styles.newTerminalButton, - pressed && styles.newTerminalButtonPressed, - disabled && styles.newTerminalButtonDisabled - ]} + - - + icon={SquareChevronRight} + /> ) } diff --git a/mobile/src/session/mobile-session-frame-styles.ts b/mobile/src/session/mobile-session-frame-styles.ts index f037a537ea9..7301c09b2eb 100644 --- a/mobile/src/session/mobile-session-frame-styles.ts +++ b/mobile/src/session/mobile-session-frame-styles.ts @@ -32,32 +32,25 @@ export const mobileSessionFrameStyles = StyleSheet.create({ paddingHorizontal: spacing.sm, paddingVertical: spacing.xs }, - backButton: { - width: 36, - height: 36, - borderRadius: 18, - alignItems: 'center', - justifyContent: 'center', - marginRight: spacing.xs - }, - backButtonPressed: { - backgroundColor: colors.bgRaised - }, - filesButton: { + // Each 36pt visual reserves 4pt per side for its 44pt touch area. + headerButton: { width: 36, height: 36, borderRadius: radii.button, alignItems: 'center', justifyContent: 'center', - marginLeft: spacing.xs + marginHorizontal: spacing.xs }, - filesButtonPressed: { + headerButtonPressed: { backgroundColor: colors.bgRaised }, // Selected state for the active docked-panel icon on wide layouts (R2). - filesButtonActive: { + headerButtonActive: { backgroundColor: colors.bgRaised }, + headerButtonDisabled: { + opacity: 0.45 + }, sessionTitleBlock: { flex: 1, minWidth: 0 @@ -106,7 +99,7 @@ export const mobileSessionFrameStyles = StyleSheet.create({ }, tabScroll: { flex: 1, - maxHeight: 36 + maxHeight: 44 }, tabScrollInTopBar: { alignSelf: 'stretch', @@ -119,10 +112,11 @@ export const mobileSessionFrameStyles = StyleSheet.create({ tabContentInTopBar: { paddingLeft: 0 }, + // Sized to the title (capped), so short titles don't waste width. tab: { - width: 128, + minWidth: 44, maxWidth: 128, - minHeight: 36, + minHeight: 44, alignItems: 'center', justifyContent: 'center', paddingHorizontal: spacing.sm, @@ -157,20 +151,9 @@ export const mobileSessionFrameStyles = StyleSheet.create({ tabTextActive: { color: colors.textPrimary }, - newTerminalButton: { - width: 40, - height: 36, - alignItems: 'center', - justifyContent: 'center', - borderBottomWidth: 2, - borderBottomColor: 'transparent' - }, newTerminalButtonPressed: { backgroundColor: colors.bgRaised }, - newTerminalButtonDisabled: { - opacity: 0.45 - }, // Divider between the + new-terminal button and the Quick Commands launcher, // matching the tab strip's borderSubtle separators. tabActionDivider: {