wip: tool call structure investigation and e2e tests
- Add debug logging for tool call payloads in agent-stream-view - Add e2e tests to verify tool call structure for Claude and Codex agents - Include sidebar toggle and horizontal scroll improvements from previous work 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
351735a71b
commit
4f3b2317ab
@@ -15,11 +15,15 @@ import { useState, useEffect, type ReactNode, useMemo } from "react";
|
|||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import { SlidingSidebar } from "@/components/sliding-sidebar";
|
import { SlidingSidebar } from "@/components/sliding-sidebar";
|
||||||
import { useSidebarStore } from "@/stores/sidebar-store";
|
import { useSidebarStore } from "@/stores/sidebar-store";
|
||||||
import { runOnJS, interpolate, Extrapolation } from "react-native-reanimated";
|
import { runOnJS, interpolate, Extrapolation, useSharedValue } from "react-native-reanimated";
|
||||||
import {
|
import {
|
||||||
SidebarAnimationProvider,
|
SidebarAnimationProvider,
|
||||||
useSidebarAnimation,
|
useSidebarAnimation,
|
||||||
} from "@/contexts/sidebar-animation-context";
|
} from "@/contexts/sidebar-animation-context";
|
||||||
|
import {
|
||||||
|
HorizontalScrollProvider,
|
||||||
|
useHorizontalScrollOptional,
|
||||||
|
} from "@/contexts/horizontal-scroll-context";
|
||||||
|
|
||||||
function QueryProvider({ children }: { children: ReactNode }) {
|
function QueryProvider({ children }: { children: ReactNode }) {
|
||||||
const [queryClient] = useState(
|
const [queryClient] = useState(
|
||||||
@@ -48,6 +52,7 @@ interface AppContainerProps {
|
|||||||
function AppContainer({ children, selectedAgentId }: AppContainerProps) {
|
function AppContainer({ children, selectedAgentId }: AppContainerProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const { isOpen, open, toggle } = useSidebarStore();
|
const { isOpen, open, toggle } = useSidebarStore();
|
||||||
|
const horizontalScroll = useHorizontalScrollOptional();
|
||||||
|
|
||||||
// Cmd+B to toggle sidebar (web only)
|
// Cmd+B to toggle sidebar (web only)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -72,15 +77,41 @@ function AppContainer({ children, selectedAgentId }: AppContainerProps) {
|
|||||||
const isMobile =
|
const isMobile =
|
||||||
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
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)
|
// 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(
|
const openGesture = useMemo(
|
||||||
() =>
|
() =>
|
||||||
Gesture.Pan()
|
Gesture.Pan()
|
||||||
.enabled(isMobile && !isOpen)
|
.enabled(isMobile && !isOpen)
|
||||||
// Only activate after 15px horizontal movement to the right
|
.manualActivation(true)
|
||||||
.activeOffsetX(15)
|
|
||||||
// Fail if 10px vertical movement happens first (allow vertical scroll)
|
// Fail if 10px vertical movement happens first (allow vertical scroll)
|
||||||
.failOffsetY([-10, 10])
|
.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(() => {
|
.onStart(() => {
|
||||||
isGesturing.value = true;
|
isGesturing.value = true;
|
||||||
})
|
})
|
||||||
@@ -109,7 +140,7 @@ function AppContainer({ children, selectedAgentId }: AppContainerProps) {
|
|||||||
.onFinalize(() => {
|
.onFinalize(() => {
|
||||||
isGesturing.value = false;
|
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 = (
|
const content = (
|
||||||
@@ -221,7 +252,8 @@ export default function RootLayout() {
|
|||||||
<MultiDaemonSessionHost />
|
<MultiDaemonSessionHost />
|
||||||
<ProvidersWrapper>
|
<ProvidersWrapper>
|
||||||
<SidebarAnimationProvider>
|
<SidebarAnimationProvider>
|
||||||
<AppWithSidebar>
|
<HorizontalScrollProvider>
|
||||||
|
<AppWithSidebar>
|
||||||
<Stack
|
<Stack
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
@@ -242,6 +274,7 @@ export default function RootLayout() {
|
|||||||
<Stack.Screen name="file-explorer" />
|
<Stack.Screen name="file-explorer" />
|
||||||
</Stack>
|
</Stack>
|
||||||
</AppWithSidebar>
|
</AppWithSidebar>
|
||||||
|
</HorizontalScrollProvider>
|
||||||
</SidebarAnimationProvider>
|
</SidebarAnimationProvider>
|
||||||
</ProvidersWrapper>
|
</ProvidersWrapper>
|
||||||
</DaemonConnectionsProvider>
|
</DaemonConnectionsProvider>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
Users,
|
Users,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
PanelRightOpen,
|
PanelRight,
|
||||||
} from "lucide-react-native";
|
} from "lucide-react-native";
|
||||||
import { MenuHeader } from "@/components/headers/menu-header";
|
import { MenuHeader } from "@/components/headers/menu-header";
|
||||||
import { BackHeader } from "@/components/headers/back-header";
|
import { BackHeader } from "@/components/headers/back-header";
|
||||||
@@ -679,18 +679,29 @@ function AgentScreenContent({
|
|||||||
rightContent={
|
rightContent={
|
||||||
<View style={styles.headerRightContent}>
|
<View style={styles.headerRightContent}>
|
||||||
<Pressable onPress={toggleExplorer} style={styles.menuButton}>
|
<Pressable onPress={toggleExplorer} style={styles.menuButton}>
|
||||||
<PanelRightOpen
|
{isMobile ? (
|
||||||
size={20}
|
<Folder
|
||||||
color={
|
size={16}
|
||||||
isExplorerOpen
|
color={
|
||||||
? theme.colors.foreground
|
isExplorerOpen
|
||||||
: theme.colors.mutedForeground
|
? theme.colors.foreground
|
||||||
}
|
: theme.colors.mutedForeground
|
||||||
/>
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<PanelRight
|
||||||
|
size={16}
|
||||||
|
color={
|
||||||
|
isExplorerOpen
|
||||||
|
? theme.colors.foreground
|
||||||
|
: theme.colors.mutedForeground
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<View ref={menuButtonRef} collapsable={false}>
|
<View ref={menuButtonRef} collapsable={false}>
|
||||||
<Pressable onPress={handleOpenMenu} style={styles.menuButton}>
|
<Pressable onPress={handleOpenMenu} style={styles.menuButton}>
|
||||||
<MoreVertical size={20} color={theme.colors.mutedForeground} />
|
<MoreVertical size={16} color={theme.colors.mutedForeground} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { View, Text, Pressable, Modal, RefreshControl, type ListRenderItem } from "react-native";
|
import { View, Text, Pressable, Modal, RefreshControl, FlatList, type ListRenderItem } from "react-native";
|
||||||
import { FlatList } from "react-native-gesture-handler";
|
|
||||||
import { useCallback, useState, type ReactElement } from "react";
|
import { useCallback, useState, type ReactElement } from "react";
|
||||||
import { router, usePathname } from "expo-router";
|
import { router, usePathname } from "expo-router";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
@@ -222,7 +221,7 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
},
|
},
|
||||||
listContent: {
|
listContent: {
|
||||||
paddingHorizontal: theme.spacing[4],
|
paddingHorizontal: theme.spacing[4],
|
||||||
paddingTop: theme.spacing[4],
|
paddingTop: theme.spacing[2],
|
||||||
paddingBottom: theme.spacing[4],
|
paddingBottom: theme.spacing[4],
|
||||||
},
|
},
|
||||||
agentItem: {
|
agentItem: {
|
||||||
|
|||||||
@@ -283,6 +283,8 @@ export function AgentStreamView({
|
|||||||
case "tool_call": {
|
case "tool_call": {
|
||||||
const { payload } = item;
|
const { payload } = item;
|
||||||
|
|
||||||
|
console.log("[TOOL_CALL_DEBUG]", JSON.stringify(payload, null, 2));
|
||||||
|
|
||||||
if (payload.source === "agent") {
|
if (payload.source === "agent") {
|
||||||
const data = payload.data;
|
const data = payload.data;
|
||||||
const toolLabel = data.displayName ?? `${data.server}/${data.tool}`;
|
const toolLabel = data.displayName ?? `${data.server}/${data.tool}`;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
type ViewMode,
|
type ViewMode,
|
||||||
} from "@/stores/explorer-sidebar-store";
|
} from "@/stores/explorer-sidebar-store";
|
||||||
import { useExplorerSidebarAnimation } from "@/contexts/explorer-sidebar-animation-context";
|
import { useExplorerSidebarAnimation } from "@/contexts/explorer-sidebar-animation-context";
|
||||||
|
import { HEADER_INNER_HEIGHT } from "@/constants/layout";
|
||||||
import { GitDiffPane } from "./git-diff-pane";
|
import { GitDiffPane } from "./git-diff-pane";
|
||||||
import { FileExplorerPane } from "./file-explorer-pane";
|
import { FileExplorerPane } from "./file-explorer-pane";
|
||||||
|
|
||||||
@@ -187,16 +188,14 @@ export function ExplorerSidebar({ serverId, agentId }: ExplorerSidebarProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View style={[styles.desktopSidebar, resizeAnimatedStyle]}>
|
<Animated.View style={[styles.desktopSidebar, resizeAnimatedStyle]}>
|
||||||
{/* Resize handle on left edge */}
|
{/* Resize handle - absolutely positioned over left border */}
|
||||||
<GestureDetector gesture={resizeGesture}>
|
<GestureDetector gesture={resizeGesture}>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.resizeHandle,
|
styles.resizeHandle,
|
||||||
Platform.OS === "web" && ({ cursor: "col-resize" } as any),
|
Platform.OS === "web" && ({ cursor: "col-resize" } as any),
|
||||||
]}
|
]}
|
||||||
>
|
/>
|
||||||
<View style={styles.resizeHandleInner} />
|
|
||||||
</View>
|
|
||||||
</GestureDetector>
|
</GestureDetector>
|
||||||
|
|
||||||
<SidebarContent
|
<SidebarContent
|
||||||
@@ -239,7 +238,7 @@ function SidebarContent({
|
|||||||
return (
|
return (
|
||||||
<View style={styles.sidebarContent} pointerEvents="auto">
|
<View style={styles.sidebarContent} pointerEvents="auto">
|
||||||
{/* Header with tabs and close button */}
|
{/* Header with tabs and close button */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header} testID="explorer-header">
|
||||||
<View style={styles.tabsContainer}>
|
<View style={styles.tabsContainer}>
|
||||||
<Pressable
|
<Pressable
|
||||||
style={[styles.tab, activeTab === "changes" && styles.tabActive]}
|
style={[styles.tab, activeTab === "changes" && styles.tabActive]}
|
||||||
@@ -297,7 +296,7 @@ function SidebarContent({
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Content based on active tab */}
|
{/* Content based on active tab */}
|
||||||
<View style={styles.contentArea}>
|
<View style={styles.contentArea} testID="explorer-content-area">
|
||||||
{activeTab === "changes" ? (
|
{activeTab === "changes" ? (
|
||||||
<GitDiffPane serverId={serverId} agentId={agentId} />
|
<GitDiffPane serverId={serverId} agentId={agentId} />
|
||||||
) : (
|
) : (
|
||||||
@@ -355,22 +354,18 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
},
|
},
|
||||||
desktopSidebar: {
|
desktopSidebar: {
|
||||||
flexDirection: "row",
|
position: "relative",
|
||||||
borderLeftWidth: 1,
|
borderLeftWidth: 1,
|
||||||
borderLeftColor: theme.colors.border,
|
borderLeftColor: theme.colors.border,
|
||||||
backgroundColor: theme.colors.background,
|
backgroundColor: theme.colors.background,
|
||||||
},
|
},
|
||||||
resizeHandle: {
|
resizeHandle: {
|
||||||
width: 8,
|
position: "absolute",
|
||||||
alignItems: "center",
|
left: -5,
|
||||||
justifyContent: "center",
|
top: 0,
|
||||||
},
|
bottom: 0,
|
||||||
resizeHandleInner: {
|
width: 10,
|
||||||
width: 2,
|
zIndex: 10,
|
||||||
height: 32,
|
|
||||||
backgroundColor: theme.colors.border,
|
|
||||||
borderRadius: 1,
|
|
||||||
opacity: 0.5,
|
|
||||||
},
|
},
|
||||||
sidebarContent: {
|
sidebarContent: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -378,11 +373,11 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
|
height: HEADER_INNER_HEIGHT,
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
paddingHorizontal: theme.spacing[2],
|
paddingHorizontal: theme.spacing[2],
|
||||||
paddingVertical: theme.spacing[1],
|
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: theme.colors.border,
|
borderBottomColor: theme.colors.border,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
import { useState, useCallback } from "react";
|
import { useState, useCallback, useEffect, useId } from "react";
|
||||||
import { View, Text, ActivityIndicator, Pressable, RefreshControl } from "react-native";
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
ActivityIndicator,
|
||||||
|
Pressable,
|
||||||
|
RefreshControl,
|
||||||
|
type NativeSyntheticEvent,
|
||||||
|
type NativeScrollEvent,
|
||||||
|
} from "react-native";
|
||||||
import { ScrollView } from "react-native-gesture-handler";
|
import { ScrollView } from "react-native-gesture-handler";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { ChevronRight } from "lucide-react-native";
|
import { ChevronRight } from "lucide-react-native";
|
||||||
@@ -10,6 +18,7 @@ import {
|
|||||||
type DiffLine,
|
type DiffLine,
|
||||||
type HighlightToken,
|
type HighlightToken,
|
||||||
} from "@/hooks/use-highlighted-diff-query";
|
} from "@/hooks/use-highlighted-diff-query";
|
||||||
|
import { useHorizontalScrollOptional } from "@/contexts/horizontal-scroll-context";
|
||||||
|
|
||||||
type HighlightStyle = NonNullable<HighlightToken["style"]>;
|
type HighlightStyle = NonNullable<HighlightToken["style"]>;
|
||||||
|
|
||||||
@@ -70,6 +79,7 @@ function HighlightedText({ tokens, lineType }: HighlightedTextProps) {
|
|||||||
interface DiffFileSectionProps {
|
interface DiffFileSectionProps {
|
||||||
file: ParsedDiffFile;
|
file: ParsedDiffFile;
|
||||||
defaultExpanded?: boolean;
|
defaultExpanded?: boolean;
|
||||||
|
testID?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DiffLineView({ line }: { line: DiffLine }) {
|
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 { theme } = useUnistyles();
|
||||||
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
||||||
|
const horizontalScroll = useHorizontalScrollOptional();
|
||||||
|
const scrollId = useId();
|
||||||
|
|
||||||
const toggleExpanded = useCallback(() => {
|
const toggleExpanded = useCallback(() => {
|
||||||
setIsExpanded((prev) => !prev);
|
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<NativeScrollEvent>) => {
|
||||||
|
if (!horizontalScroll) return;
|
||||||
|
const offsetX = event.nativeEvent.contentOffset.x;
|
||||||
|
horizontalScroll.registerScrollOffset(scrollId, offsetX);
|
||||||
|
},
|
||||||
|
[horizontalScroll, scrollId]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.fileSection}>
|
<View style={styles.fileSection} testID={testID}>
|
||||||
<Pressable
|
<Pressable
|
||||||
style={({ pressed }) => [
|
style={({ pressed }) => [
|
||||||
styles.fileHeader,
|
styles.fileHeader,
|
||||||
@@ -154,8 +185,11 @@ function DiffFileSection({ file, defaultExpanded = true }: DiffFileSectionProps)
|
|||||||
horizontal
|
horizontal
|
||||||
nestedScrollEnabled
|
nestedScrollEnabled
|
||||||
showsHorizontalScrollIndicator
|
showsHorizontalScrollIndicator
|
||||||
|
bounces={false}
|
||||||
style={styles.diffContent}
|
style={styles.diffContent}
|
||||||
contentContainerStyle={styles.diffContentInner}
|
contentContainerStyle={styles.diffContentInner}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
scrollEventThrottle={16}
|
||||||
>
|
>
|
||||||
<View style={styles.linesContainer}>
|
<View style={styles.linesContainer}>
|
||||||
{file.hunks.map((hunk, hunkIndex) =>
|
{file.hunks.map((hunk, hunkIndex) =>
|
||||||
@@ -200,7 +234,7 @@ export function GitDiffPane({ serverId, agentId }: GitDiffPaneProps) {
|
|||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={styles.scrollView}
|
style={styles.scrollView}
|
||||||
contentContainerStyle={styles.contentContainer}
|
testID="git-diff-scroll"
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={isFetching && !isLoading}
|
refreshing={isFetching && !isLoading}
|
||||||
@@ -210,24 +244,26 @@ export function GitDiffPane({ serverId, agentId }: GitDiffPaneProps) {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
<View style={styles.contentContainer} testID="git-diff-content">
|
||||||
<View style={styles.loadingContainer}>
|
{isLoading ? (
|
||||||
<ActivityIndicator size="large" />
|
<View style={styles.loadingContainer}>
|
||||||
<Text style={styles.loadingText}>Loading changes...</Text>
|
<ActivityIndicator size="large" />
|
||||||
</View>
|
<Text style={styles.loadingText}>Loading changes...</Text>
|
||||||
) : isError ? (
|
</View>
|
||||||
<View style={styles.errorContainer}>
|
) : isError ? (
|
||||||
<Text style={styles.errorText}>{errorMessage ?? "Failed to load changes"}</Text>
|
<View style={styles.errorContainer}>
|
||||||
</View>
|
<Text style={styles.errorText}>{errorMessage ?? "Failed to load changes"}</Text>
|
||||||
) : !hasChanges ? (
|
</View>
|
||||||
<View style={styles.emptyContainer}>
|
) : !hasChanges ? (
|
||||||
<Text style={styles.emptyText}>No changes</Text>
|
<View style={styles.emptyContainer}>
|
||||||
</View>
|
<Text style={styles.emptyText}>No changes</Text>
|
||||||
) : (
|
</View>
|
||||||
files.map((file, fileIndex) => (
|
) : (
|
||||||
<DiffFileSection key={fileIndex} file={file} />
|
files.map((file, fileIndex) => (
|
||||||
))
|
<DiffFileSection key={fileIndex} file={file} testID={`diff-file-${fileIndex}`} />
|
||||||
)}
|
))
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -238,7 +274,7 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
},
|
},
|
||||||
contentContainer: {
|
contentContainer: {
|
||||||
paddingHorizontal: theme.spacing[2],
|
paddingHorizontal: theme.spacing[2],
|
||||||
paddingTop: theme.spacing[2],
|
paddingTop: theme.spacing[3],
|
||||||
paddingBottom: theme.spacing[8],
|
paddingBottom: theme.spacing[8],
|
||||||
},
|
},
|
||||||
loadingContainer: {
|
loadingContainer: {
|
||||||
@@ -346,7 +382,7 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
diffContent: {
|
diffContent: {
|
||||||
borderTopWidth: theme.borderWidth[1],
|
borderTopWidth: theme.borderWidth[1],
|
||||||
borderTopColor: theme.colors.border,
|
borderTopColor: theme.colors.border,
|
||||||
backgroundColor: "#0d1117", // GitHub dark background
|
backgroundColor: "#0d1117",
|
||||||
},
|
},
|
||||||
diffContentInner: {
|
diffContentInner: {
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
@@ -354,6 +390,7 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
linesContainer: {
|
linesContainer: {
|
||||||
alignSelf: "flex-start",
|
alignSelf: "flex-start",
|
||||||
minWidth: "100%",
|
minWidth: "100%",
|
||||||
|
backgroundColor: "#0d1117",
|
||||||
},
|
},
|
||||||
diffLineContainer: {
|
diffLineContainer: {
|
||||||
paddingHorizontal: theme.spacing[3],
|
paddingHorizontal: theme.spacing[3],
|
||||||
@@ -365,25 +402,25 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
},
|
},
|
||||||
addLineContainer: {
|
addLineContainer: {
|
||||||
backgroundColor: "rgba(46, 160, 67, 0.1)", // GitHub green with transparency
|
backgroundColor: "rgba(46, 160, 67, 0.15)", // GitHub green
|
||||||
},
|
},
|
||||||
addLineText: {
|
addLineText: {
|
||||||
color: "#c9d1d9", // Same text color as all code
|
color: "#c9d1d9", // Same text color as all code
|
||||||
},
|
},
|
||||||
removeLineContainer: {
|
removeLineContainer: {
|
||||||
backgroundColor: "rgba(248, 81, 73, 0.1)", // GitHub red with transparency
|
backgroundColor: "rgba(248, 81, 73, 0.1)", // GitHub red
|
||||||
},
|
},
|
||||||
removeLineText: {
|
removeLineText: {
|
||||||
color: "#c9d1d9", // Same text color as all code
|
color: "#c9d1d9", // Same text color as all code
|
||||||
},
|
},
|
||||||
headerLineContainer: {
|
headerLineContainer: {
|
||||||
backgroundColor: "#161b22", // GitHub dark header
|
backgroundColor: theme.colors.muted,
|
||||||
},
|
},
|
||||||
headerLineText: {
|
headerLineText: {
|
||||||
color: theme.colors.mutedForeground,
|
color: theme.colors.mutedForeground,
|
||||||
},
|
},
|
||||||
contextLineContainer: {
|
contextLineContainer: {
|
||||||
backgroundColor: "#0d1117", // GitHub dark background
|
backgroundColor: "#0d1117",
|
||||||
},
|
},
|
||||||
contextLineText: {
|
contextLineText: {
|
||||||
color: theme.colors.mutedForeground,
|
color: theme.colors.mutedForeground,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { Pressable, Text } from "react-native";
|
import { Pressable, Text } from "react-native";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||||
import { Menu } from "lucide-react-native";
|
import { Menu, PanelLeft } from "lucide-react-native";
|
||||||
import { ScreenHeader } from "./screen-header";
|
import { ScreenHeader } from "./screen-header";
|
||||||
import { useSidebarStore } from "@/stores/sidebar-store";
|
import { useSidebarStore } from "@/stores/sidebar-store";
|
||||||
|
|
||||||
@@ -12,14 +12,21 @@ interface MenuHeaderProps {
|
|||||||
|
|
||||||
export function MenuHeader({ title, rightContent }: MenuHeaderProps) {
|
export function MenuHeader({ title, rightContent }: MenuHeaderProps) {
|
||||||
const { theme } = useUnistyles();
|
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 (
|
return (
|
||||||
<ScreenHeader
|
<ScreenHeader
|
||||||
left={
|
left={
|
||||||
<>
|
<>
|
||||||
<Pressable onPress={toggle} style={styles.menuButton}>
|
<Pressable onPress={toggle} style={styles.menuButton}>
|
||||||
<Menu size={20} color={theme.colors.mutedForeground} />
|
<MenuIcon size={16} color={menuIconColor} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
{title && (
|
{title && (
|
||||||
<Text style={styles.title} numberOfLines={1}>
|
<Text style={styles.title} numberOfLines={1}>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { View, type StyleProp, type ViewStyle } from "react-native";
|
import { View, type StyleProp, type ViewStyle } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
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 {
|
interface ScreenHeaderProps {
|
||||||
left?: ReactNode;
|
left?: ReactNode;
|
||||||
@@ -16,8 +17,9 @@ interface ScreenHeaderProps {
|
|||||||
*/
|
*/
|
||||||
export function ScreenHeader({ left, right, leftStyle, rightStyle }: ScreenHeaderProps) {
|
export function ScreenHeader({ left, right, leftStyle, rightStyle }: ScreenHeaderProps) {
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const { theme } = useUnistyles();
|
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
||||||
const topPadding = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm" ? 8 : 4;
|
// Only add extra padding on mobile for better touch targets; on desktop, only use safe area insets
|
||||||
|
const topPadding = isMobile ? 8 : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
@@ -37,17 +39,11 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
},
|
},
|
||||||
inner: {},
|
inner: {},
|
||||||
row: {
|
row: {
|
||||||
|
height: HEADER_INNER_HEIGHT,
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
paddingHorizontal: {
|
paddingHorizontal: theme.spacing[2],
|
||||||
xs: theme.spacing[2],
|
|
||||||
md: theme.spacing[1],
|
|
||||||
},
|
|
||||||
paddingBottom: {
|
|
||||||
xs: theme.spacing[2],
|
|
||||||
md: theme.spacing[1],
|
|
||||||
},
|
|
||||||
borderBottomWidth: theme.borderWidth[1],
|
borderBottomWidth: theme.borderWidth[1],
|
||||||
borderBottomColor: theme.colors.border,
|
borderBottomColor: theme.colors.border,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1 +1,6 @@
|
|||||||
export const FOOTER_HEIGHT = 75;
|
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;
|
||||||
|
|||||||
@@ -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<boolean>;
|
||||||
|
// Register a scroll view's offset - returns an unregister function
|
||||||
|
registerScrollOffset: (id: string, offset: number) => void;
|
||||||
|
unregisterScrollOffset: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HorizontalScrollContext = createContext<HorizontalScrollContextValue | null>(null);
|
||||||
|
|
||||||
|
export function HorizontalScrollProvider({ children }: { children: ReactNode }) {
|
||||||
|
const isAnyScrolledRight = useSharedValue(false);
|
||||||
|
const scrollOffsetsRef = useRef<Map<string, number>>(new Map());
|
||||||
|
|
||||||
|
const updateIsAnyScrolled = useCallback(() => {
|
||||||
|
let anyScrolled = false;
|
||||||
|
for (const offset of scrollOffsetsRef.current.values()) {
|
||||||
|
if (offset > 1) {
|
||||||
|
anyScrolled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isAnyScrolledRight.value = anyScrolled;
|
||||||
|
}, [isAnyScrolledRight]);
|
||||||
|
|
||||||
|
const registerScrollOffset = useCallback(
|
||||||
|
(id: string, offset: number) => {
|
||||||
|
scrollOffsetsRef.current.set(id, offset);
|
||||||
|
updateIsAnyScrolled();
|
||||||
|
},
|
||||||
|
[updateIsAnyScrolled]
|
||||||
|
);
|
||||||
|
|
||||||
|
const unregisterScrollOffset = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
scrollOffsetsRef.current.delete(id);
|
||||||
|
updateIsAnyScrolled();
|
||||||
|
},
|
||||||
|
[updateIsAnyScrolled]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HorizontalScrollContext.Provider
|
||||||
|
value={{
|
||||||
|
isAnyScrolledRight,
|
||||||
|
registerScrollOffset,
|
||||||
|
unregisterScrollOffset,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</HorizontalScrollContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useHorizontalScroll() {
|
||||||
|
const context = useContext(HorizontalScrollContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useHorizontalScroll must be used within HorizontalScrollProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useHorizontalScrollOptional() {
|
||||||
|
return useContext(HorizontalScrollContext);
|
||||||
|
}
|
||||||
@@ -3115,4 +3115,169 @@ describe("daemon E2E", () => {
|
|||||||
180000 // 3 minute timeout
|
180000 // 3 minute timeout
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("tool call structure", () => {
|
||||||
|
test(
|
||||||
|
"Claude agent tool calls have expected structure",
|
||||||
|
async () => {
|
||||||
|
const cwd = tmpCwd();
|
||||||
|
|
||||||
|
// Create Claude agent with bypass permissions
|
||||||
|
const agent = await ctx.client.createAgent({
|
||||||
|
provider: "claude",
|
||||||
|
cwd,
|
||||||
|
title: "Tool Structure Test - Claude",
|
||||||
|
modeId: "bypassPermissions",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(agent.provider).toBe("claude");
|
||||||
|
|
||||||
|
ctx.client.clearMessageQueue();
|
||||||
|
|
||||||
|
// Prompt that triggers a Read tool call
|
||||||
|
await ctx.client.sendMessage(
|
||||||
|
agent.id,
|
||||||
|
"Read the file /etc/hosts and tell me how many lines it has. Be brief."
|
||||||
|
);
|
||||||
|
|
||||||
|
const finalState = await ctx.client.waitForAgentIdle(agent.id, 120000);
|
||||||
|
expect(finalState.status).toBe("idle");
|
||||||
|
|
||||||
|
// Extract tool_call timeline items
|
||||||
|
const queue = ctx.client.getMessageQueue();
|
||||||
|
const toolCalls: AgentTimelineItem[] = [];
|
||||||
|
for (const m of queue) {
|
||||||
|
if (
|
||||||
|
m.type === "agent_stream" &&
|
||||||
|
m.payload.agentId === agent.id &&
|
||||||
|
m.payload.event.type === "timeline" &&
|
||||||
|
m.payload.event.item.type === "tool_call"
|
||||||
|
) {
|
||||||
|
toolCalls.push(m.payload.event.item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(toolCalls.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Log and verify structure for each tool call
|
||||||
|
for (const tc of toolCalls) {
|
||||||
|
expect(tc.type).toBe("tool_call");
|
||||||
|
|
||||||
|
// Current structure has: server, tool, displayName, kind
|
||||||
|
expect(typeof tc.server).toBe("string");
|
||||||
|
expect(typeof tc.tool).toBe("string");
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"[CLAUDE TOOL_CALL]",
|
||||||
|
JSON.stringify({
|
||||||
|
server: tc.server,
|
||||||
|
tool: tc.tool,
|
||||||
|
displayName: tc.displayName,
|
||||||
|
kind: tc.kind,
|
||||||
|
callId: tc.callId,
|
||||||
|
status: tc.status,
|
||||||
|
hasInput: tc.input !== undefined,
|
||||||
|
hasOutput: tc.output !== undefined,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find a Read tool call specifically
|
||||||
|
const readCall = toolCalls.find(
|
||||||
|
(tc) =>
|
||||||
|
tc.server === "Read" ||
|
||||||
|
tc.tool === "Read" ||
|
||||||
|
tc.displayName === "Read"
|
||||||
|
);
|
||||||
|
expect(readCall).toBeDefined();
|
||||||
|
expect(readCall?.input).toBeDefined();
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
await ctx.client.deleteAgent(agent.id);
|
||||||
|
rmSync(cwd, { recursive: true, force: true });
|
||||||
|
},
|
||||||
|
180000
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"Codex agent tool calls have expected structure",
|
||||||
|
async () => {
|
||||||
|
const cwd = tmpCwd();
|
||||||
|
|
||||||
|
// Create Codex agent with full access
|
||||||
|
const agent = await ctx.client.createAgent({
|
||||||
|
provider: "codex",
|
||||||
|
cwd,
|
||||||
|
title: "Tool Structure Test - Codex",
|
||||||
|
modeId: "full-access",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(agent.provider).toBe("codex");
|
||||||
|
|
||||||
|
ctx.client.clearMessageQueue();
|
||||||
|
|
||||||
|
// Prompt that triggers a shell command
|
||||||
|
await ctx.client.sendMessage(
|
||||||
|
agent.id,
|
||||||
|
"Run `echo hello` and tell me what it outputs. Be brief."
|
||||||
|
);
|
||||||
|
|
||||||
|
const finalState = await ctx.client.waitForAgentIdle(agent.id, 120000);
|
||||||
|
expect(finalState.status).toBe("idle");
|
||||||
|
|
||||||
|
// Extract tool_call timeline items
|
||||||
|
const queue = ctx.client.getMessageQueue();
|
||||||
|
const toolCalls: AgentTimelineItem[] = [];
|
||||||
|
for (const m of queue) {
|
||||||
|
if (
|
||||||
|
m.type === "agent_stream" &&
|
||||||
|
m.payload.agentId === agent.id &&
|
||||||
|
m.payload.event.type === "timeline" &&
|
||||||
|
m.payload.event.item.type === "tool_call"
|
||||||
|
) {
|
||||||
|
toolCalls.push(m.payload.event.item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(toolCalls.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Log and verify structure for each tool call
|
||||||
|
for (const tc of toolCalls) {
|
||||||
|
expect(tc.type).toBe("tool_call");
|
||||||
|
|
||||||
|
// Current structure has: server, tool, displayName, kind
|
||||||
|
expect(typeof tc.server).toBe("string");
|
||||||
|
expect(typeof tc.tool).toBe("string");
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"[CODEX TOOL_CALL]",
|
||||||
|
JSON.stringify({
|
||||||
|
server: tc.server,
|
||||||
|
tool: tc.tool,
|
||||||
|
displayName: tc.displayName,
|
||||||
|
kind: tc.kind,
|
||||||
|
callId: tc.callId,
|
||||||
|
status: tc.status,
|
||||||
|
hasInput: tc.input !== undefined,
|
||||||
|
hasOutput: tc.output !== undefined,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find a shell/execute tool call
|
||||||
|
const shellCall = toolCalls.find(
|
||||||
|
(tc) =>
|
||||||
|
tc.kind === "execute" ||
|
||||||
|
tc.server?.includes("shell") ||
|
||||||
|
tc.displayName?.includes("echo")
|
||||||
|
);
|
||||||
|
expect(shellCall).toBeDefined();
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
await ctx.client.deleteAgent(agent.id);
|
||||||
|
rmSync(cwd, { recursive: true, force: true });
|
||||||
|
},
|
||||||
|
180000
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user