diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx
index bd72d47f5..c9a0a7010 100644
--- a/packages/app/src/app/_layout.tsx
+++ b/packages/app/src/app/_layout.tsx
@@ -252,6 +252,9 @@ function QueryProvider({ children }: { children: ReactNode }) {
return {children};
}
+const rowStyle = { flex: 1, flexDirection: "row" } as const;
+const flexStyle = { flex: 1 } as const;
+
interface AppContainerProps {
children: ReactNode;
selectedAgentId?: string;
@@ -377,11 +380,16 @@ function AppContainer({
]
);
+ const containerStyle = useMemo(
+ () => ({ flex: 1 as const, backgroundColor: theme.colors.surface0 }),
+ [theme.colors.surface0]
+ );
+
const content = (
-
-
+
+
{!isMobile && chromeEnabled && }
-
+
{children}
diff --git a/packages/app/src/components/left-sidebar.tsx b/packages/app/src/components/left-sidebar.tsx
index 172959e9b..8256b58c5 100644
--- a/packages/app/src/components/left-sidebar.tsx
+++ b/packages/app/src/components/left-sidebar.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useMemo, useState, useEffect, useRef, useSyncExternalStore } from 'react'
+import { memo, useCallback, useMemo, useState, useEffect, useRef, useSyncExternalStore } from 'react'
import { View, Pressable, Text, Platform } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import Animated, {
@@ -39,7 +39,7 @@ interface LeftSidebarProps {
selectedAgentId?: string
}
-export function LeftSidebar({ selectedAgentId: _selectedAgentId }: LeftSidebarProps) {
+export const LeftSidebar = memo(function LeftSidebar({ selectedAgentId: _selectedAgentId }: LeftSidebarProps) {
const { theme } = useUnistyles()
const insets = useSafeAreaInsets()
const isMobile = UnistylesRuntime.breakpoint === 'xs' || UnistylesRuntime.breakpoint === 'sm'
@@ -207,76 +207,103 @@ export function LeftSidebar({ selectedAgentId: _selectedAgentId }: LeftSidebarPr
)
// Close gesture (swipe left to close when sidebar is open)
- const closeGesture = Gesture.Pan()
- .withRef(closeGestureRef)
- .enabled(isOpen)
- // Use manual activation so child views keep touch streams unless we detect
- // an intentional left-swipe close (mirrors explorer-sidebar pattern).
- .manualActivation(true)
- .onTouchesDown((event) => {
- const touch = event.changedTouches[0]
- if (!touch) {
- return
- }
- closeTouchStartX.value = touch.absoluteX
- closeTouchStartY.value = touch.absoluteY
- })
- .onTouchesMove((event, stateManager) => {
- const touch = event.changedTouches[0]
- if (!touch || event.numberOfTouches !== 1) {
- stateManager.fail()
- return
- }
+ const closeGesture = useMemo(
+ () =>
+ Gesture.Pan()
+ .withRef(closeGestureRef)
+ .enabled(isOpen)
+ // Use manual activation so child views keep touch streams unless we detect
+ // an intentional left-swipe close (mirrors explorer-sidebar pattern).
+ .manualActivation(true)
+ .onTouchesDown((event) => {
+ const touch = event.changedTouches[0]
+ if (!touch) {
+ return
+ }
+ closeTouchStartX.value = touch.absoluteX
+ closeTouchStartY.value = touch.absoluteY
+ })
+ .onTouchesMove((event, stateManager) => {
+ const touch = event.changedTouches[0]
+ if (!touch || event.numberOfTouches !== 1) {
+ stateManager.fail()
+ return
+ }
- const deltaX = touch.absoluteX - closeTouchStartX.value
- const deltaY = touch.absoluteY - closeTouchStartY.value
- const absDeltaX = Math.abs(deltaX)
- const absDeltaY = Math.abs(deltaY)
+ const deltaX = touch.absoluteX - closeTouchStartX.value
+ const deltaY = touch.absoluteY - closeTouchStartY.value
+ const absDeltaX = Math.abs(deltaX)
+ const absDeltaY = Math.abs(deltaY)
- // Fail quickly on clear rightward or vertical intent so child views keep control.
- if (deltaX >= 10) {
- stateManager.fail()
- return
- }
- if (absDeltaY > 10 && absDeltaY > absDeltaX) {
- stateManager.fail()
- return
- }
+ // Fail quickly on clear rightward or vertical intent so child views keep control.
+ if (deltaX >= 10) {
+ stateManager.fail()
+ return
+ }
+ if (absDeltaY > 10 && absDeltaY > absDeltaX) {
+ stateManager.fail()
+ return
+ }
- // Activate only on intentional leftward movement.
- if (deltaX <= -15 && absDeltaX > absDeltaY) {
- stateManager.activate()
- }
- })
- .onStart(() => {
- isGesturing.value = true
- })
- .onUpdate((event) => {
- if (!isMobile) return
- // Only allow swiping left (closing)
- const newTranslateX = Math.min(0, Math.max(-windowWidth, event.translationX))
- translateX.value = newTranslateX
- backdropOpacity.value = interpolate(
- newTranslateX,
- [-windowWidth, 0],
- [0, 1],
- Extrapolation.CLAMP
- )
- })
- .onEnd((event) => {
- isGesturing.value = false
- if (!isMobile) return
- const shouldClose = event.translationX < -windowWidth / 3 || event.velocityX < -500
- if (shouldClose) {
- animateToClose()
- runOnJS(handleClose)()
- } else {
- animateToOpen()
- }
- })
- .onFinalize(() => {
- isGesturing.value = false
- })
+ // Activate only on intentional leftward movement.
+ if (deltaX <= -15 && absDeltaX > absDeltaY) {
+ stateManager.activate()
+ }
+ })
+ .onStart(() => {
+ isGesturing.value = true
+ })
+ .onUpdate((event) => {
+ if (!isMobile) return
+ // Only allow swiping left (closing)
+ const newTranslateX = Math.min(0, Math.max(-windowWidth, event.translationX))
+ translateX.value = newTranslateX
+ backdropOpacity.value = interpolate(
+ newTranslateX,
+ [-windowWidth, 0],
+ [0, 1],
+ Extrapolation.CLAMP
+ )
+ })
+ .onEnd((event) => {
+ isGesturing.value = false
+ if (!isMobile) return
+ const shouldClose = event.translationX < -windowWidth / 3 || event.velocityX < -500
+ if (shouldClose) {
+ animateToClose()
+ runOnJS(handleClose)()
+ } else {
+ animateToOpen()
+ }
+ })
+ .onFinalize(() => {
+ isGesturing.value = false
+ }),
+ [
+ isOpen,
+ closeGestureRef,
+ closeTouchStartX,
+ closeTouchStartY,
+ isGesturing,
+ isMobile,
+ windowWidth,
+ translateX,
+ backdropOpacity,
+ animateToClose,
+ animateToOpen,
+ handleClose,
+ ]
+ )
+
+ const mobileSidebarInsetStyle = useMemo(
+ () => ({ width: windowWidth, paddingTop: insets.top, paddingBottom: insets.bottom }),
+ [windowWidth, insets.top, insets.bottom]
+ )
+
+ const hostStatusDotStyle = useMemo(
+ () => [styles.hostStatusDot, { backgroundColor: activeHostStatusColor }],
+ [activeHostStatusColor]
+ )
const sidebarAnimatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
@@ -303,7 +330,7 @@ export function LeftSidebar({ selectedAgentId: _selectedAgentId }: LeftSidebarPr
setIsHostPickerOpen(true)}
disabled={hostOptions.length === 0}
>
-
+
{activeHostLabel}
@@ -568,7 +595,7 @@ export function LeftSidebar({ selectedAgentId: _selectedAgentId }: LeftSidebarPr
)
-}
+})
const styles = StyleSheet.create((theme) => ({
backdrop: {
diff --git a/packages/app/src/contexts/sidebar-animation-context.tsx b/packages/app/src/contexts/sidebar-animation-context.tsx
index 59203e697..eed2a16a0 100644
--- a/packages/app/src/contexts/sidebar-animation-context.tsx
+++ b/packages/app/src/contexts/sidebar-animation-context.tsx
@@ -1,4 +1,4 @@
-import { createContext, useContext, useEffect, useRef, type ReactNode } from "react";
+import { createContext, useCallback, useContext, useEffect, useMemo, useRef, type ReactNode } from "react";
import { useWindowDimensions } from "react-native";
import {
useSharedValue,
@@ -78,7 +78,7 @@ export function SidebarAnimationProvider({ children }: { children: ReactNode })
}
}, [isOpen, translateX, backdropOpacity, windowWidth, isGesturing]);
- const animateToOpen = () => {
+ const animateToOpen = useCallback(() => {
"worklet";
translateX.value = withTiming(0, {
duration: ANIMATION_DURATION,
@@ -88,9 +88,9 @@ export function SidebarAnimationProvider({ children }: { children: ReactNode })
duration: ANIMATION_DURATION,
easing: ANIMATION_EASING,
});
- };
+ }, [translateX, backdropOpacity]);
- const animateToClose = () => {
+ const animateToClose = useCallback(() => {
"worklet";
translateX.value = withTiming(-windowWidth, {
duration: ANIMATION_DURATION,
@@ -100,20 +100,23 @@ export function SidebarAnimationProvider({ children }: { children: ReactNode })
duration: ANIMATION_DURATION,
easing: ANIMATION_EASING,
});
- };
+ }, [translateX, backdropOpacity, windowWidth]);
+
+ const value = useMemo(
+ () => ({
+ translateX,
+ backdropOpacity,
+ windowWidth,
+ animateToOpen,
+ animateToClose,
+ isGesturing,
+ closeGestureRef,
+ }),
+ [translateX, backdropOpacity, windowWidth, animateToOpen, animateToClose, isGesturing, closeGestureRef]
+ );
return (
-
+
{children}
);
diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx
index 274536eec..e9fefa0b2 100644
--- a/packages/app/src/screens/workspace/workspace-screen.tsx
+++ b/packages/app/src/screens/workspace/workspace-screen.tsx
@@ -519,6 +519,12 @@ export function WorkspaceScreen({
workspaceId,
openIntent,
}: WorkspaceScreenProps) {
+ const isFocused = useIsFocused();
+
+ if (!isFocused) {
+ return ;
+ }
+
return (
isScreenFocused,
+ isActive: () => true,
handle: handleWorkspaceTabAction,
});
@@ -1794,7 +1799,7 @@ function WorkspaceScreenContent({
] as const,
enabled: Boolean(normalizedServerId && normalizedWorkspaceId),
priority: 100,
- isActive: () => isScreenFocused,
+ isActive: () => true,
handle: handleWorkspacePaneAction,
});