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
@@ -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: {
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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 (
|
||||
<Animated.View style={[styles.desktopSidebar, resizeAnimatedStyle]}>
|
||||
{/* Resize handle on left edge */}
|
||||
{/* Resize handle - absolutely positioned over left border */}
|
||||
<GestureDetector gesture={resizeGesture}>
|
||||
<View
|
||||
style={[
|
||||
styles.resizeHandle,
|
||||
Platform.OS === "web" && ({ cursor: "col-resize" } as any),
|
||||
]}
|
||||
>
|
||||
<View style={styles.resizeHandleInner} />
|
||||
</View>
|
||||
/>
|
||||
</GestureDetector>
|
||||
|
||||
<SidebarContent
|
||||
@@ -239,7 +238,7 @@ function SidebarContent({
|
||||
return (
|
||||
<View style={styles.sidebarContent} pointerEvents="auto">
|
||||
{/* Header with tabs and close button */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.header} testID="explorer-header">
|
||||
<View style={styles.tabsContainer}>
|
||||
<Pressable
|
||||
style={[styles.tab, activeTab === "changes" && styles.tabActive]}
|
||||
@@ -297,7 +296,7 @@ function SidebarContent({
|
||||
</View>
|
||||
|
||||
{/* Content based on active tab */}
|
||||
<View style={styles.contentArea}>
|
||||
<View style={styles.contentArea} testID="explorer-content-area">
|
||||
{activeTab === "changes" ? (
|
||||
<GitDiffPane serverId={serverId} agentId={agentId} />
|
||||
) : (
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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<HighlightToken["style"]>;
|
||||
|
||||
@@ -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<NativeScrollEvent>) => {
|
||||
if (!horizontalScroll) return;
|
||||
const offsetX = event.nativeEvent.contentOffset.x;
|
||||
horizontalScroll.registerScrollOffset(scrollId, offsetX);
|
||||
},
|
||||
[horizontalScroll, scrollId]
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.fileSection}>
|
||||
<View style={styles.fileSection} testID={testID}>
|
||||
<Pressable
|
||||
style={({ pressed }) => [
|
||||
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}
|
||||
>
|
||||
<View style={styles.linesContainer}>
|
||||
{file.hunks.map((hunk, hunkIndex) =>
|
||||
@@ -200,7 +234,7 @@ export function GitDiffPane({ serverId, agentId }: GitDiffPaneProps) {
|
||||
return (
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
testID="git-diff-scroll"
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isFetching && !isLoading}
|
||||
@@ -210,24 +244,26 @@ export function GitDiffPane({ serverId, agentId }: GitDiffPaneProps) {
|
||||
/>
|
||||
}
|
||||
>
|
||||
{isLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" />
|
||||
<Text style={styles.loadingText}>Loading changes...</Text>
|
||||
</View>
|
||||
) : isError ? (
|
||||
<View style={styles.errorContainer}>
|
||||
<Text style={styles.errorText}>{errorMessage ?? "Failed to load changes"}</Text>
|
||||
</View>
|
||||
) : !hasChanges ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Text style={styles.emptyText}>No changes</Text>
|
||||
</View>
|
||||
) : (
|
||||
files.map((file, fileIndex) => (
|
||||
<DiffFileSection key={fileIndex} file={file} />
|
||||
))
|
||||
)}
|
||||
<View style={styles.contentContainer} testID="git-diff-content">
|
||||
{isLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" />
|
||||
<Text style={styles.loadingText}>Loading changes...</Text>
|
||||
</View>
|
||||
) : isError ? (
|
||||
<View style={styles.errorContainer}>
|
||||
<Text style={styles.errorText}>{errorMessage ?? "Failed to load changes"}</Text>
|
||||
</View>
|
||||
) : !hasChanges ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Text style={styles.emptyText}>No changes</Text>
|
||||
</View>
|
||||
) : (
|
||||
files.map((file, fileIndex) => (
|
||||
<DiffFileSection key={fileIndex} file={file} testID={`diff-file-${fileIndex}`} />
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
<ScreenHeader
|
||||
left={
|
||||
<>
|
||||
<Pressable onPress={toggle} style={styles.menuButton}>
|
||||
<Menu size={20} color={theme.colors.mutedForeground} />
|
||||
<MenuIcon size={16} color={menuIconColor} />
|
||||
</Pressable>
|
||||
{title && (
|
||||
<Text style={styles.title} numberOfLines={1}>
|
||||
|
||||
@@ -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 (
|
||||
<View style={styles.header}>
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user