diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx
index 10616faf7..d623ac4a5 100644
--- a/packages/app/src/app/_layout.tsx
+++ b/packages/app/src/app/_layout.tsx
@@ -15,11 +15,15 @@ import { useState, useEffect, type ReactNode, useMemo } from "react";
import { Platform } from "react-native";
import { SlidingSidebar } from "@/components/sliding-sidebar";
import { useSidebarStore } from "@/stores/sidebar-store";
-import { runOnJS, interpolate, Extrapolation } from "react-native-reanimated";
+import { runOnJS, interpolate, Extrapolation, useSharedValue } from "react-native-reanimated";
import {
SidebarAnimationProvider,
useSidebarAnimation,
} from "@/contexts/sidebar-animation-context";
+import {
+ HorizontalScrollProvider,
+ useHorizontalScrollOptional,
+} from "@/contexts/horizontal-scroll-context";
function QueryProvider({ children }: { children: ReactNode }) {
const [queryClient] = useState(
@@ -48,6 +52,7 @@ interface AppContainerProps {
function AppContainer({ children, selectedAgentId }: AppContainerProps) {
const { theme } = useUnistyles();
const { isOpen, open, toggle } = useSidebarStore();
+ const horizontalScroll = useHorizontalScrollOptional();
// Cmd+B to toggle sidebar (web only)
useEffect(() => {
@@ -72,15 +77,41 @@ function AppContainer({ children, selectedAgentId }: AppContainerProps) {
const isMobile =
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
+ // Track initial touch position for manual activation
+ const touchStartX = useSharedValue(0);
+
// Open gesture: swipe right from anywhere to open sidebar (interactive drag)
+ // If any horizontal scroll is scrolled right, let the scroll view handle the gesture first
const openGesture = useMemo(
() =>
Gesture.Pan()
.enabled(isMobile && !isOpen)
- // Only activate after 15px horizontal movement to the right
- .activeOffsetX(15)
+ .manualActivation(true)
// Fail if 10px vertical movement happens first (allow vertical scroll)
.failOffsetY([-10, 10])
+ .onTouchesDown((event) => {
+ const touch = event.changedTouches[0];
+ if (touch) {
+ touchStartX.value = touch.absoluteX;
+ }
+ })
+ .onTouchesMove((event, stateManager) => {
+ const touch = event.changedTouches[0];
+ if (!touch || event.numberOfTouches !== 1) return;
+
+ const deltaX = touch.absoluteX - touchStartX.value;
+
+ // If horizontal scroll is scrolled right, fail so ScrollView handles it
+ if (horizontalScroll?.isAnyScrolledRight.value) {
+ stateManager.fail();
+ return;
+ }
+
+ // Activate after 15px rightward movement
+ if (deltaX > 15) {
+ stateManager.activate();
+ }
+ })
.onStart(() => {
isGesturing.value = true;
})
@@ -109,7 +140,7 @@ function AppContainer({ children, selectedAgentId }: AppContainerProps) {
.onFinalize(() => {
isGesturing.value = false;
}),
- [isMobile, isOpen, windowWidth, translateX, backdropOpacity, animateToOpen, animateToClose, open, isGesturing]
+ [isMobile, isOpen, windowWidth, translateX, backdropOpacity, animateToOpen, animateToClose, open, isGesturing, horizontalScroll?.isAnyScrolledRight, touchStartX]
);
const content = (
@@ -221,7 +252,8 @@ export default function RootLayout() {
-
+
+
+
diff --git a/packages/app/src/app/agent/[serverId]/[agentId].tsx b/packages/app/src/app/agent/[serverId]/[agentId].tsx
index 12fd89229..55905f1cb 100644
--- a/packages/app/src/app/agent/[serverId]/[agentId].tsx
+++ b/packages/app/src/app/agent/[serverId]/[agentId].tsx
@@ -33,7 +33,7 @@ import {
Users,
ChevronRight,
PlusIcon,
- PanelRightOpen,
+ PanelRight,
} from "lucide-react-native";
import { MenuHeader } from "@/components/headers/menu-header";
import { BackHeader } from "@/components/headers/back-header";
@@ -679,18 +679,29 @@ function AgentScreenContent({
rightContent={
-
+ {isMobile ? (
+
+ ) : (
+
+ )}
-
+
diff --git a/packages/app/src/components/agent-list.tsx b/packages/app/src/components/agent-list.tsx
index 04cd621c5..1f54be7b8 100644
--- a/packages/app/src/components/agent-list.tsx
+++ b/packages/app/src/components/agent-list.tsx
@@ -1,5 +1,4 @@
-import { View, Text, Pressable, Modal, RefreshControl, type ListRenderItem } from "react-native";
-import { FlatList } from "react-native-gesture-handler";
+import { View, Text, Pressable, Modal, RefreshControl, FlatList, type ListRenderItem } from "react-native";
import { useCallback, useState, type ReactElement } from "react";
import { router, usePathname } from "expo-router";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
@@ -222,7 +221,7 @@ const styles = StyleSheet.create((theme) => ({
},
listContent: {
paddingHorizontal: theme.spacing[4],
- paddingTop: theme.spacing[4],
+ paddingTop: theme.spacing[2],
paddingBottom: theme.spacing[4],
},
agentItem: {
diff --git a/packages/app/src/components/agent-stream-view.tsx b/packages/app/src/components/agent-stream-view.tsx
index 0d5aff7f9..09c6aa693 100644
--- a/packages/app/src/components/agent-stream-view.tsx
+++ b/packages/app/src/components/agent-stream-view.tsx
@@ -283,6 +283,8 @@ export function AgentStreamView({
case "tool_call": {
const { payload } = item;
+ console.log("[TOOL_CALL_DEBUG]", JSON.stringify(payload, null, 2));
+
if (payload.source === "agent") {
const data = payload.data;
const toolLabel = data.displayName ?? `${data.server}/${data.tool}`;
diff --git a/packages/app/src/components/explorer-sidebar.tsx b/packages/app/src/components/explorer-sidebar.tsx
index f5e6aca05..e1b9cf6e1 100644
--- a/packages/app/src/components/explorer-sidebar.tsx
+++ b/packages/app/src/components/explorer-sidebar.tsx
@@ -16,6 +16,7 @@ import {
type ViewMode,
} from "@/stores/explorer-sidebar-store";
import { useExplorerSidebarAnimation } from "@/contexts/explorer-sidebar-animation-context";
+import { HEADER_INNER_HEIGHT } from "@/constants/layout";
import { GitDiffPane } from "./git-diff-pane";
import { FileExplorerPane } from "./file-explorer-pane";
@@ -187,16 +188,14 @@ export function ExplorerSidebar({ serverId, agentId }: ExplorerSidebarProps) {
return (
- {/* Resize handle on left edge */}
+ {/* Resize handle - absolutely positioned over left border */}
-
-
+ />
{/* Header with tabs and close button */}
-
+
{/* Content based on active tab */}
-
+
{activeTab === "changes" ? (
) : (
@@ -355,22 +354,18 @@ const styles = StyleSheet.create((theme) => ({
overflow: "hidden",
},
desktopSidebar: {
- flexDirection: "row",
+ position: "relative",
borderLeftWidth: 1,
borderLeftColor: theme.colors.border,
backgroundColor: theme.colors.background,
},
resizeHandle: {
- width: 8,
- alignItems: "center",
- justifyContent: "center",
- },
- resizeHandleInner: {
- width: 2,
- height: 32,
- backgroundColor: theme.colors.border,
- borderRadius: 1,
- opacity: 0.5,
+ position: "absolute",
+ left: -5,
+ top: 0,
+ bottom: 0,
+ width: 10,
+ zIndex: 10,
},
sidebarContent: {
flex: 1,
@@ -378,11 +373,11 @@ const styles = StyleSheet.create((theme) => ({
overflow: "hidden",
},
header: {
+ height: HEADER_INNER_HEIGHT,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingHorizontal: theme.spacing[2],
- paddingVertical: theme.spacing[1],
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
},
diff --git a/packages/app/src/components/git-diff-pane.tsx b/packages/app/src/components/git-diff-pane.tsx
index 6ebf8ef08..872703aec 100644
--- a/packages/app/src/components/git-diff-pane.tsx
+++ b/packages/app/src/components/git-diff-pane.tsx
@@ -1,5 +1,13 @@
-import { useState, useCallback } from "react";
-import { View, Text, ActivityIndicator, Pressable, RefreshControl } from "react-native";
+import { useState, useCallback, useEffect, useId } from "react";
+import {
+ View,
+ Text,
+ ActivityIndicator,
+ Pressable,
+ RefreshControl,
+ type NativeSyntheticEvent,
+ type NativeScrollEvent,
+} from "react-native";
import { ScrollView } from "react-native-gesture-handler";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { ChevronRight } from "lucide-react-native";
@@ -10,6 +18,7 @@ import {
type DiffLine,
type HighlightToken,
} from "@/hooks/use-highlighted-diff-query";
+import { useHorizontalScrollOptional } from "@/contexts/horizontal-scroll-context";
type HighlightStyle = NonNullable;
@@ -70,6 +79,7 @@ function HighlightedText({ tokens, lineType }: HighlightedTextProps) {
interface DiffFileSectionProps {
file: ParsedDiffFile;
defaultExpanded?: boolean;
+ testID?: string;
}
function DiffLineView({ line }: { line: DiffLine }) {
@@ -106,16 +116,37 @@ function DiffLineView({ line }: { line: DiffLine }) {
);
}
-function DiffFileSection({ file, defaultExpanded = true }: DiffFileSectionProps) {
+function DiffFileSection({ file, defaultExpanded = true, testID }: DiffFileSectionProps) {
const { theme } = useUnistyles();
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
+ const horizontalScroll = useHorizontalScrollOptional();
+ const scrollId = useId();
const toggleExpanded = useCallback(() => {
setIsExpanded((prev) => !prev);
}, []);
+ // Register/unregister scroll offset tracking
+ useEffect(() => {
+ if (!horizontalScroll || !isExpanded) return;
+ // Start at 0 (not scrolled)
+ horizontalScroll.registerScrollOffset(scrollId, 0);
+ return () => {
+ horizontalScroll.unregisterScrollOffset(scrollId);
+ };
+ }, [horizontalScroll, isExpanded, scrollId]);
+
+ const handleScroll = useCallback(
+ (event: NativeSyntheticEvent) => {
+ if (!horizontalScroll) return;
+ const offsetX = event.nativeEvent.contentOffset.x;
+ horizontalScroll.registerScrollOffset(scrollId, offsetX);
+ },
+ [horizontalScroll, scrollId]
+ );
+
return (
-
+
[
styles.fileHeader,
@@ -154,8 +185,11 @@ function DiffFileSection({ file, defaultExpanded = true }: DiffFileSectionProps)
horizontal
nestedScrollEnabled
showsHorizontalScrollIndicator
+ bounces={false}
style={styles.diffContent}
contentContainerStyle={styles.diffContentInner}
+ onScroll={handleScroll}
+ scrollEventThrottle={16}
>
{file.hunks.map((hunk, hunkIndex) =>
@@ -200,7 +234,7 @@ export function GitDiffPane({ serverId, agentId }: GitDiffPaneProps) {
return (
}
>
- {isLoading ? (
-
-
- Loading changes...
-
- ) : isError ? (
-
- {errorMessage ?? "Failed to load changes"}
-
- ) : !hasChanges ? (
-
- No changes
-
- ) : (
- files.map((file, fileIndex) => (
-
- ))
- )}
+
+ {isLoading ? (
+
+
+ Loading changes...
+
+ ) : isError ? (
+
+ {errorMessage ?? "Failed to load changes"}
+
+ ) : !hasChanges ? (
+
+ No changes
+
+ ) : (
+ files.map((file, fileIndex) => (
+
+ ))
+ )}
+
);
}
@@ -238,7 +274,7 @@ const styles = StyleSheet.create((theme) => ({
},
contentContainer: {
paddingHorizontal: theme.spacing[2],
- paddingTop: theme.spacing[2],
+ paddingTop: theme.spacing[3],
paddingBottom: theme.spacing[8],
},
loadingContainer: {
@@ -346,7 +382,7 @@ const styles = StyleSheet.create((theme) => ({
diffContent: {
borderTopWidth: theme.borderWidth[1],
borderTopColor: theme.colors.border,
- backgroundColor: "#0d1117", // GitHub dark background
+ backgroundColor: "#0d1117",
},
diffContentInner: {
flexDirection: "column",
@@ -354,6 +390,7 @@ const styles = StyleSheet.create((theme) => ({
linesContainer: {
alignSelf: "flex-start",
minWidth: "100%",
+ backgroundColor: "#0d1117",
},
diffLineContainer: {
paddingHorizontal: theme.spacing[3],
@@ -365,25 +402,25 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foreground,
},
addLineContainer: {
- backgroundColor: "rgba(46, 160, 67, 0.1)", // GitHub green with transparency
+ backgroundColor: "rgba(46, 160, 67, 0.15)", // GitHub green
},
addLineText: {
color: "#c9d1d9", // Same text color as all code
},
removeLineContainer: {
- backgroundColor: "rgba(248, 81, 73, 0.1)", // GitHub red with transparency
+ backgroundColor: "rgba(248, 81, 73, 0.1)", // GitHub red
},
removeLineText: {
color: "#c9d1d9", // Same text color as all code
},
headerLineContainer: {
- backgroundColor: "#161b22", // GitHub dark header
+ backgroundColor: theme.colors.muted,
},
headerLineText: {
color: theme.colors.mutedForeground,
},
contextLineContainer: {
- backgroundColor: "#0d1117", // GitHub dark background
+ backgroundColor: "#0d1117",
},
contextLineText: {
color: theme.colors.mutedForeground,
diff --git a/packages/app/src/components/headers/menu-header.tsx b/packages/app/src/components/headers/menu-header.tsx
index 724153dc0..1b48671b1 100644
--- a/packages/app/src/components/headers/menu-header.tsx
+++ b/packages/app/src/components/headers/menu-header.tsx
@@ -1,7 +1,7 @@
import type { ReactNode } from "react";
import { Pressable, Text } from "react-native";
-import { StyleSheet, useUnistyles } from "react-native-unistyles";
-import { Menu } from "lucide-react-native";
+import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
+import { Menu, PanelLeft } from "lucide-react-native";
import { ScreenHeader } from "./screen-header";
import { useSidebarStore } from "@/stores/sidebar-store";
@@ -12,14 +12,21 @@ interface MenuHeaderProps {
export function MenuHeader({ title, rightContent }: MenuHeaderProps) {
const { theme } = useUnistyles();
- const { toggle } = useSidebarStore();
+ const { isOpen, toggle } = useSidebarStore();
+ const isMobile =
+ UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
+
+ const MenuIcon = isMobile ? Menu : PanelLeft;
+ const menuIconColor = !isMobile && isOpen
+ ? theme.colors.foreground
+ : theme.colors.mutedForeground;
return (
-
+
{title && (
diff --git a/packages/app/src/components/headers/screen-header.tsx b/packages/app/src/components/headers/screen-header.tsx
index 33a78d308..2435fe318 100644
--- a/packages/app/src/components/headers/screen-header.tsx
+++ b/packages/app/src/components/headers/screen-header.tsx
@@ -1,7 +1,8 @@
import type { ReactNode } from "react";
import { View, type StyleProp, type ViewStyle } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
-import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
+import { StyleSheet, UnistylesRuntime } from "react-native-unistyles";
+import { HEADER_INNER_HEIGHT } from "@/constants/layout";
interface ScreenHeaderProps {
left?: ReactNode;
@@ -16,8 +17,9 @@ interface ScreenHeaderProps {
*/
export function ScreenHeader({ left, right, leftStyle, rightStyle }: ScreenHeaderProps) {
const insets = useSafeAreaInsets();
- const { theme } = useUnistyles();
- const topPadding = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm" ? 8 : 4;
+ const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
+ // Only add extra padding on mobile for better touch targets; on desktop, only use safe area insets
+ const topPadding = isMobile ? 8 : 0;
return (
@@ -37,17 +39,11 @@ const styles = StyleSheet.create((theme) => ({
},
inner: {},
row: {
+ height: HEADER_INNER_HEIGHT,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
- paddingHorizontal: {
- xs: theme.spacing[2],
- md: theme.spacing[1],
- },
- paddingBottom: {
- xs: theme.spacing[2],
- md: theme.spacing[1],
- },
+ paddingHorizontal: theme.spacing[2],
borderBottomWidth: theme.borderWidth[1],
borderBottomColor: theme.colors.border,
},
diff --git a/packages/app/src/constants/layout.ts b/packages/app/src/constants/layout.ts
index 77097f884..ed1cb6989 100644
--- a/packages/app/src/constants/layout.ts
+++ b/packages/app/src/constants/layout.ts
@@ -1 +1,6 @@
export const FOOTER_HEIGHT = 75;
+
+// Shared header inner height (excluding safe area insets and border)
+// Used by both agent header (ScreenHeader) and explorer sidebar header
+// This ensures both headers have the same visual height
+export const HEADER_INNER_HEIGHT = 48;
diff --git a/packages/app/src/contexts/horizontal-scroll-context.tsx b/packages/app/src/contexts/horizontal-scroll-context.tsx
new file mode 100644
index 000000000..79673d5ce
--- /dev/null
+++ b/packages/app/src/contexts/horizontal-scroll-context.tsx
@@ -0,0 +1,68 @@
+import { createContext, useContext, useCallback, useRef, type ReactNode } from "react";
+import { useSharedValue, type SharedValue } from "react-native-reanimated";
+
+interface HorizontalScrollContextValue {
+ // Shared value indicating if any registered horizontal scroll is scrolled (offset > 0)
+ isAnyScrolledRight: SharedValue;
+ // Register a scroll view's offset - returns an unregister function
+ registerScrollOffset: (id: string, offset: number) => void;
+ unregisterScrollOffset: (id: string) => void;
+}
+
+const HorizontalScrollContext = createContext(null);
+
+export function HorizontalScrollProvider({ children }: { children: ReactNode }) {
+ const isAnyScrolledRight = useSharedValue(false);
+ const scrollOffsetsRef = useRef