Update files

This commit is contained in:
Mohamed Boudra
2026-02-07 00:31:07 +07:00
parent 0878d0fb2e
commit 31f449a5fc
5 changed files with 119 additions and 74 deletions
@@ -108,6 +108,7 @@ export function AgentStreamView({
const hasAutoScrolledOnce = useRef(false); const hasAutoScrolledOnce = useRef(false);
const isNearBottomRef = useRef(true); const isNearBottomRef = useRef(true);
const streamItemCountRef = useRef(0); const streamItemCountRef = useRef(0);
const [expandedInlineToolCallIds, setExpandedInlineToolCallIds] = useState<Set<string>>(new Set());
const openFileExplorer = usePanelStore((state) => state.openFileExplorer); const openFileExplorer = usePanelStore((state) => state.openFileExplorer);
const setExplorerTab = usePanelStore((state) => state.setExplorerTab); const setExplorerTab = usePanelStore((state) => state.setExplorerTab);
@@ -137,6 +138,7 @@ export function AgentStreamView({
hasScrolledInitially.current = false; hasScrolledInitially.current = false;
hasAutoScrolledOnce.current = false; hasAutoScrolledOnce.current = false;
isNearBottomRef.current = true; isNearBottomRef.current = true;
setExpandedInlineToolCallIds(new Set());
}, [agentId]); }, [agentId]);
const handleInlinePathPress = useCallback( const handleInlinePathPress = useCallback(
@@ -289,6 +291,21 @@ export function AgentStreamView({
const renderStreamItemContent = useCallback( const renderStreamItemContent = useCallback(
(item: StreamItem, index: number) => { (item: StreamItem, index: number) => {
const handleInlineDetailsExpandedChange = (expanded: boolean) => {
if (Platform.OS !== "web") {
return;
}
setExpandedInlineToolCallIds((previous) => {
const next = new Set(previous);
if (expanded) {
next.add(item.id);
} else {
next.delete(item.id);
}
return next;
});
};
switch (item.kind) { switch (item.kind) {
case "user_message": { case "user_message": {
// In inverted list: index+1 is the item above, index-1 is below. // In inverted list: index+1 is the item above, index-1 is below.
@@ -326,6 +343,7 @@ export function AgentStreamView({
args={item.text} args={item.text}
status={item.status === "ready" ? "completed" : "executing"} status={item.status === "ready" ? "completed" : "executing"}
isLastInSequence={isLastInSequence} isLastInSequence={isLastInSequence}
onInlineDetailsExpandedChange={handleInlineDetailsExpandedChange}
/> />
); );
} }
@@ -348,6 +366,7 @@ export function AgentStreamView({
status={data.status as "executing" | "completed" | "failed"} status={data.status as "executing" | "completed" | "failed"}
cwd={agent.cwd} cwd={agent.cwd}
isLastInSequence={isLastInSequence} isLastInSequence={isLastInSequence}
onInlineDetailsExpandedChange={handleInlineDetailsExpandedChange}
/> />
); );
} }
@@ -360,6 +379,7 @@ export function AgentStreamView({
result={data.result} result={data.result}
status={data.status} status={data.status}
isLastInSequence={isLastInSequence} isLastInSequence={isLastInSequence}
onInlineDetailsExpandedChange={handleInlineDetailsExpandedChange}
/> />
); );
} }
@@ -651,6 +671,7 @@ export function AgentStreamView({
} }
initialNumToRender={12} initialNumToRender={12}
windowSize={10} windowSize={10}
scrollEnabled={Platform.OS !== "web" || expandedInlineToolCallIds.size === 0}
inverted inverted
/> />
</MessageOuterSpacingProvider> </MessageOuterSpacingProvider>
+4 -2
View File
@@ -1,11 +1,13 @@
import React from "react"; import React from "react";
import { View, Text } from "react-native"; import { View, Text, Platform, ScrollView as RNScrollView } from "react-native";
import { ScrollView } from "react-native-gesture-handler"; import { ScrollView as GHScrollView } from "react-native-gesture-handler";
import { StyleSheet } from "react-native-unistyles"; import { StyleSheet } from "react-native-unistyles";
import { Fonts } from "@/constants/theme"; import { Fonts } from "@/constants/theme";
import type { DiffLine, DiffSegment } from "@/utils/tool-call-parsers"; import type { DiffLine, DiffSegment } from "@/utils/tool-call-parsers";
import { getCodeInsets } from "./code-insets"; import { getCodeInsets } from "./code-insets";
const ScrollView = Platform.OS === "web" ? RNScrollView : GHScrollView;
interface DiffViewerProps { interface DiffViewerProps {
diffLines: DiffLine[]; diffLines: DiffLine[];
maxHeight?: number; maxHeight?: number;
+50 -68
View File
@@ -5,11 +5,10 @@ import {
Text, Text,
ActivityIndicator, ActivityIndicator,
Pressable, Pressable,
SectionList, FlatList,
Platform, Platform,
type NativeSyntheticEvent, type NativeSyntheticEvent,
type NativeScrollEvent, type NativeScrollEvent,
type SectionListRenderItem,
} from "react-native"; } from "react-native";
import { ScrollView, type ScrollView as ScrollViewType } from "react-native-gesture-handler"; import { ScrollView, type ScrollView as ScrollViewType } from "react-native-gesture-handler";
import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { StyleSheet, useUnistyles } from "react-native-unistyles";
@@ -18,7 +17,7 @@ import * as Linking from "expo-linking";
import { import {
Archive, Archive,
ChevronDown, ChevronDown,
ChevronRight,
GitBranch, GitBranch,
GitCommitHorizontal, GitCommitHorizontal,
GitMerge, GitMerge,
@@ -221,7 +220,6 @@ const DiffFileHeader = memo(function DiffFileHeader({
onToggle, onToggle,
testID, testID,
}: DiffFileSectionProps) { }: DiffFileSectionProps) {
const { theme } = useUnistyles();
const expandStartRef = useRef<number | null>(null); const expandStartRef = useRef<number | null>(null);
const { hunkCount, lineCount, tokenCount } = useMemo(() => { const { hunkCount, lineCount, tokenCount } = useMemo(() => {
@@ -305,17 +303,6 @@ const DiffFileHeader = memo(function DiffFileHeader({
onPress={toggleExpanded} onPress={toggleExpanded}
> >
<View style={styles.fileHeaderLeft}> <View style={styles.fileHeaderLeft}>
<View
style={[
styles.chevronContainer,
isExpanded && styles.chevronExpanded,
]}
>
<ChevronRight
size={16}
color={theme.colors.foregroundMuted}
/>
</View>
<Text style={styles.fileName}>{file.path.split("/").pop()}</Text> <Text style={styles.fileName}>{file.path.split("/").pop()}</Text>
<Text style={styles.fileDir} numberOfLines={1}> <Text style={styles.fileDir} numberOfLines={1}>
{file.path.includes("/") {file.path.includes("/")
@@ -425,13 +412,9 @@ interface GitDiffPaneProps {
cwd: string; cwd: string;
} }
type GitDiffSection = { type DiffFlatItem =
key: string; | { type: "header"; file: ParsedDiffFile; fileIndex: number; isExpanded: boolean }
index: number; | { type: "body"; file: ParsedDiffFile; fileIndex: number };
file: ParsedDiffFile;
isExpanded: boolean;
data: ParsedDiffFile[];
};
export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) { export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
@@ -557,17 +540,19 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
})); }));
}, []); }, []);
const diffSections = useMemo((): GitDiffSection[] => { const { flatItems, stickyHeaderIndices } = useMemo(() => {
return files.map((file, index) => { const items: DiffFlatItem[] = [];
const stickyIndices: number[] = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
const isExpanded = expandedByPath[file.path] ?? false; const isExpanded = expandedByPath[file.path] ?? false;
return { stickyIndices.push(items.length);
key: file.path, items.push({ type: "header", file, fileIndex: i, isExpanded });
index, if (isExpanded) {
file, items.push({ type: "body", file, fileIndex: i });
isExpanded, }
data: isExpanded ? [file] : [], }
}; return { flatItems: items, stickyHeaderIndices: stickyIndices };
});
}, [files, expandedByPath]); }, [files, expandedByPath]);
const allExpanded = useMemo(() => { const allExpanded = useMemo(() => {
@@ -714,26 +699,29 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
}); });
}, [runArchiveWorktree, router, serverId, cwd, status?.cwd]); }, [runArchiveWorktree, router, serverId, cwd, status?.cwd]);
const renderFileBody: SectionListRenderItem<ParsedDiffFile, GitDiffSection> = useCallback( const renderFlatItem = useCallback(
({ item, section }) => ( ({ item }: { item: DiffFlatItem }) => {
<DiffFileBody file={item} testID={`diff-file-${section.index}-body`} /> if (item.type === "header") {
), return (
[] <DiffFileHeader
); file={item.file}
isExpanded={item.isExpanded}
const renderSectionHeader = useCallback( onToggle={handleToggleExpanded}
({ section }: { section: GitDiffSection }) => ( testID={`diff-file-${item.fileIndex}`}
<DiffFileHeader />
file={section.file} );
isExpanded={section.isExpanded} }
onToggle={handleToggleExpanded} return (
testID={`diff-file-${section.index}`} <DiffFileBody file={item.file} testID={`diff-file-${item.fileIndex}-body`} />
/> );
), },
[handleToggleExpanded] [handleToggleExpanded]
); );
const keyExtractor = useCallback((item: ParsedDiffFile) => item.path, []); const flatKeyExtractor = useCallback(
(item: DiffFlatItem) => `${item.type}-${item.file.path}`,
[]
);
const hasChanges = files.length > 0; const hasChanges = files.length > 0;
const diffErrorMessage = const diffErrorMessage =
@@ -819,20 +807,19 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
); );
} else { } else {
bodyContent = ( bodyContent = (
<SectionList <FlatList
sections={diffSections} data={flatItems}
renderItem={renderFileBody} renderItem={renderFlatItem}
renderSectionHeader={renderSectionHeader} keyExtractor={flatKeyExtractor}
keyExtractor={keyExtractor} stickyHeaderIndices={stickyHeaderIndices}
stickySectionHeadersEnabled
extraData={expandedByPath} extraData={expandedByPath}
style={styles.scrollView} style={styles.scrollView}
contentContainerStyle={styles.contentContainer} contentContainerStyle={styles.contentContainer}
testID="git-diff-scroll" testID="git-diff-scroll"
onRefresh={handleRefresh} onRefresh={handleRefresh}
refreshing={isManualRefresh && isDiffFetching} refreshing={isManualRefresh && isDiffFetching}
initialNumToRender={3} initialNumToRender={6}
maxToRenderPerBatch={3} maxToRenderPerBatch={6}
windowSize={5} windowSize={5}
/> />
); );
@@ -1444,11 +1431,11 @@ const styles = StyleSheet.create((theme) => ({
overflow: "hidden", overflow: "hidden",
backgroundColor: theme.colors.surface2, backgroundColor: theme.colors.surface2,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: theme.colors.borderAccent, borderBottomColor: theme.colors.border,
}, },
fileSectionHeaderContainer: { fileSectionHeaderContainer: {
overflow: "hidden", overflow: "hidden",
backgroundColor: theme.colors.surface2, backgroundColor: theme.colors.surface1,
}, },
fileSectionBodyContainer: { fileSectionBodyContainer: {
overflow: "hidden", overflow: "hidden",
@@ -1456,16 +1443,17 @@ const styles = StyleSheet.create((theme) => ({
}, },
fileSectionBorder: { fileSectionBorder: {
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: theme.colors.borderAccent, borderBottomColor: theme.colors.border,
}, },
fileHeader: { fileHeader: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
justifyContent: "space-between", justifyContent: "space-between",
paddingHorizontal: theme.spacing[2], paddingLeft: theme.spacing[3],
paddingRight: theme.spacing[2],
paddingVertical: theme.spacing[2], paddingVertical: theme.spacing[2],
gap: theme.spacing[1], gap: theme.spacing[1],
backgroundColor: theme.colors.surface2, backgroundColor: theme.colors.surface1,
zIndex: 2, zIndex: 2,
elevation: 2, elevation: 2,
}, },
@@ -1485,12 +1473,6 @@ const styles = StyleSheet.create((theme) => ({
gap: theme.spacing[1], gap: theme.spacing[1],
flexShrink: 0, flexShrink: 0,
}, },
chevronContainer: {
transform: [{ rotate: "0deg" }],
},
chevronExpanded: {
transform: [{ rotate: "90deg" }],
},
fileName: { fileName: {
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.normal, fontWeight: theme.fontWeight.normal,
+40 -2
View File
@@ -883,6 +883,7 @@ interface ExpandableBadgeProps {
isExpanded: boolean; isExpanded: boolean;
style?: StyleProp<ViewStyle>; style?: StyleProp<ViewStyle>;
onToggle?: () => void; onToggle?: () => void;
onDetailHoverChange?: (hovered: boolean) => void;
renderDetails?: () => ReactNode; renderDetails?: () => ReactNode;
isLoading?: boolean; isLoading?: boolean;
isError?: boolean; isError?: boolean;
@@ -898,6 +899,7 @@ const ExpandableBadge = memo(function ExpandableBadge({
icon, icon,
isExpanded, isExpanded,
onToggle, onToggle,
onDetailHoverChange,
renderDetails, renderDetails,
isLoading = false, isLoading = false,
isError = false, isError = false,
@@ -1017,9 +1019,13 @@ const ExpandableBadge = memo(function ExpandableBadge({
)} )}
</Pressable> </Pressable>
{detailContent ? ( {detailContent ? (
<View style={expandableBadgeStylesheet.detailWrapper}> <Pressable
style={expandableBadgeStylesheet.detailWrapper}
onHoverIn={() => onDetailHoverChange?.(true)}
onHoverOut={() => onDetailHoverChange?.(false)}
>
{detailContent} {detailContent}
</View> </Pressable>
) : null} ) : null}
</View> </View>
); );
@@ -1034,6 +1040,8 @@ interface ToolCallProps {
cwd?: string; cwd?: string;
isLastInSequence?: boolean; isLastInSequence?: boolean;
disableOuterSpacing?: boolean; disableOuterSpacing?: boolean;
onInlineDetailsHoverChange?: (hovered: boolean) => void;
onInlineDetailsExpandedChange?: (expanded: boolean) => void;
} }
// Icon mapping for tool kinds // Icon mapping for tool kinds
@@ -1070,6 +1078,8 @@ export const ToolCall = memo(function ToolCall({
cwd, cwd,
isLastInSequence = false, isLastInSequence = false,
disableOuterSpacing, disableOuterSpacing,
onInlineDetailsHoverChange,
onInlineDetailsExpandedChange,
}: ToolCallProps) { }: ToolCallProps) {
const { openToolCall } = useToolCallSheet(); const { openToolCall } = useToolCallSheet();
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
@@ -1144,6 +1154,33 @@ export const ToolCall = memo(function ToolCall({
} }
}, [isExpanded, isMobile, toolName, kind]); }, [isExpanded, isMobile, toolName, kind]);
useEffect(() => {
if (!onInlineDetailsHoverChange || isMobile || isExpanded) {
return;
}
onInlineDetailsHoverChange(false);
}, [isExpanded, isMobile, onInlineDetailsHoverChange]);
useEffect(() => {
if (!onInlineDetailsExpandedChange) {
return;
}
if (isMobile) {
onInlineDetailsExpandedChange(false);
return;
}
onInlineDetailsExpandedChange(isExpanded);
}, [isExpanded, isMobile, onInlineDetailsExpandedChange]);
useEffect(() => {
if (!onInlineDetailsExpandedChange) {
return;
}
return () => {
onInlineDetailsExpandedChange(false);
};
}, [onInlineDetailsExpandedChange]);
// Render inline details for desktop // Render inline details for desktop
const renderDetails = useCallback(() => { const renderDetails = useCallback(() => {
if (isMobile) return null; if (isMobile) return null;
@@ -1169,6 +1206,7 @@ export const ToolCall = memo(function ToolCall({
isError={status === "failed"} isError={status === "failed"}
isLastInSequence={isLastInSequence} isLastInSequence={isLastInSequence}
disableOuterSpacing={disableOuterSpacing} disableOuterSpacing={disableOuterSpacing}
onDetailHoverChange={onInlineDetailsHoverChange}
/> />
); );
}); });
@@ -1,6 +1,6 @@
import React, { useMemo, ReactNode } from "react"; import React, { useMemo, ReactNode } from "react";
import { View, Text } from "react-native"; import { View, Text, Platform, ScrollView as RNScrollView } from "react-native";
import { ScrollView } from "react-native-gesture-handler"; import { ScrollView as GHScrollView } from "react-native-gesture-handler";
import { StyleSheet } from "react-native-unistyles"; import { StyleSheet } from "react-native-unistyles";
import { Fonts } from "@/constants/theme"; import { Fonts } from "@/constants/theme";
import { getNowMs, isPerfLoggingEnabled, perfLog } from "@/utils/perf"; import { getNowMs, isPerfLoggingEnabled, perfLog } from "@/utils/perf";
@@ -13,6 +13,8 @@ import {
import { DiffViewer } from "./diff-viewer"; import { DiffViewer } from "./diff-viewer";
import { getCodeInsets } from "./code-insets"; import { getCodeInsets } from "./code-insets";
const ScrollView = Platform.OS === "web" ? RNScrollView : GHScrollView;
// ---- Types ---- // ---- Types ----
export interface ToolCallDetailsData { export interface ToolCallDetailsData {