Update files
This commit is contained in:
@@ -189,7 +189,7 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
|
||||
accessibilityLabel="Select thinking option"
|
||||
testID="agent-thinking-selector"
|
||||
>
|
||||
<Brain size={12} color={theme.colors.foregroundMuted} />
|
||||
<Brain size={12} color={theme.colors.foregroundMuted} style={{ marginTop: 1 }} />
|
||||
<Text style={styles.modeBadgeText}>{displayThinking}</Text>
|
||||
<ChevronDown size={14} color={theme.colors.foregroundMuted} />
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
@@ -12,9 +12,15 @@ interface DiffViewerProps {
|
||||
diffLines: DiffLine[];
|
||||
maxHeight?: number;
|
||||
emptyLabel?: string;
|
||||
fillAvailableHeight?: boolean;
|
||||
}
|
||||
|
||||
export function DiffViewer({ diffLines, maxHeight = 280, emptyLabel = "No changes to display" }: DiffViewerProps) {
|
||||
export function DiffViewer({
|
||||
diffLines,
|
||||
maxHeight,
|
||||
emptyLabel = "No changes to display",
|
||||
fillAvailableHeight = false,
|
||||
}: DiffViewerProps) {
|
||||
const [scrollViewWidth, setScrollViewWidth] = React.useState(0);
|
||||
|
||||
if (!diffLines.length) {
|
||||
@@ -27,7 +33,11 @@ export function DiffViewer({ diffLines, maxHeight = 280, emptyLabel = "No change
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={[styles.verticalScroll, { maxHeight }]}
|
||||
style={[
|
||||
styles.verticalScroll,
|
||||
maxHeight !== undefined && { maxHeight },
|
||||
fillAvailableHeight && styles.fillHeight,
|
||||
]}
|
||||
contentContainerStyle={styles.verticalContent}
|
||||
nestedScrollEnabled
|
||||
showsVerticalScrollIndicator
|
||||
@@ -94,6 +104,10 @@ const styles = StyleSheet.create((theme) => {
|
||||
|
||||
return {
|
||||
verticalScroll: {},
|
||||
fillHeight: {
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
},
|
||||
verticalContent: {
|
||||
flexGrow: 1,
|
||||
paddingBottom: insets.extraBottom,
|
||||
|
||||
@@ -223,6 +223,9 @@ const DiffFileHeader = memo(function DiffFileHeader({
|
||||
testID,
|
||||
}: DiffFileSectionProps) {
|
||||
const expandStartRef = useRef<number | null>(null);
|
||||
const layoutYRef = useRef<number | null>(null);
|
||||
const pressHandledRef = useRef(false);
|
||||
const pressInRef = useRef<{ ts: number; pageX: number; pageY: number } | null>(null);
|
||||
|
||||
const { hunkCount, lineCount, tokenCount } = useMemo(() => {
|
||||
let totalLines = 0;
|
||||
@@ -247,6 +250,7 @@ const DiffFileHeader = memo(function DiffFileHeader({
|
||||
tokenCount >= DIFF_FILE_LOG_TOKEN_THRESHOLD;
|
||||
|
||||
const toggleExpanded = useCallback(() => {
|
||||
pressHandledRef.current = true;
|
||||
if (isPerfLoggingEnabled() && shouldLogFileMetrics) {
|
||||
expandStartRef.current = getNowMs();
|
||||
perfLog(DIFF_FILE_LOG_TAG, {
|
||||
@@ -295,6 +299,7 @@ const DiffFileHeader = memo(function DiffFileHeader({
|
||||
!isExpanded && styles.fileSectionBorder,
|
||||
]}
|
||||
onLayout={(event) => {
|
||||
layoutYRef.current = event.nativeEvent.layout.y;
|
||||
onHeaderHeightChange?.(file.path, event.nativeEvent.layout.height);
|
||||
}}
|
||||
testID={testID}
|
||||
@@ -305,6 +310,34 @@ const DiffFileHeader = memo(function DiffFileHeader({
|
||||
styles.fileHeader,
|
||||
pressed && styles.fileHeaderPressed,
|
||||
]}
|
||||
// Android: prevent parent pan/scroll gestures from canceling the tap release.
|
||||
cancelable={false}
|
||||
onPressIn={(event) => {
|
||||
pressHandledRef.current = false;
|
||||
pressInRef.current = {
|
||||
ts: Date.now(),
|
||||
pageX: event.nativeEvent.pageX,
|
||||
pageY: event.nativeEvent.pageY,
|
||||
};
|
||||
}}
|
||||
onPressOut={(event) => {
|
||||
if (
|
||||
Platform.OS !== "web" &&
|
||||
!pressHandledRef.current &&
|
||||
layoutYRef.current === 0 &&
|
||||
pressInRef.current
|
||||
) {
|
||||
const durationMs = Date.now() - pressInRef.current.ts;
|
||||
const dx = event.nativeEvent.pageX - pressInRef.current.pageX;
|
||||
const dy = event.nativeEvent.pageY - pressInRef.current.pageY;
|
||||
const distance = Math.hypot(dx, dy);
|
||||
// Sticky headers on Android can emit pressIn/pressOut without onPress.
|
||||
// Treat short, low-movement interactions as taps.
|
||||
if (durationMs <= 500 && distance <= 12) {
|
||||
toggleExpanded();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onPress={toggleExpanded}
|
||||
>
|
||||
<View style={styles.fileHeaderLeft}>
|
||||
@@ -562,8 +595,10 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
const isExpanded = expandedByPath[file.path] ?? false;
|
||||
stickyIndices.push(items.length);
|
||||
items.push({ type: "header", file, fileIndex: i, isExpanded });
|
||||
if (isExpanded) {
|
||||
stickyIndices.push(items.length - 1);
|
||||
}
|
||||
if (isExpanded) {
|
||||
items.push({ type: "body", file, fileIndex: i });
|
||||
}
|
||||
@@ -607,6 +642,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
||||
const handleToggleExpanded = useCallback(
|
||||
(path: string) => {
|
||||
const isCurrentlyExpanded = expandedByPath[path] ?? false;
|
||||
const nextExpanded = !isCurrentlyExpanded;
|
||||
const targetOffset = isCurrentlyExpanded ? computeHeaderOffset(path) : null;
|
||||
|
||||
// Anchor to the clicked header before collapsing so visual context is preserved.
|
||||
@@ -619,7 +655,9 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
||||
|
||||
setExpandedByPath((prev) => ({
|
||||
...prev,
|
||||
[path]: !prev[path],
|
||||
// Use a deterministic target value (instead of toggling from prev) so duplicate
|
||||
// onPress events from sticky headers on Android can't flip back immediately.
|
||||
[path]: nextExpanded,
|
||||
}));
|
||||
},
|
||||
[computeHeaderOffset, expandedByPath]
|
||||
|
||||
@@ -401,7 +401,6 @@ const expandableBadgeStylesheet = StyleSheet.create((theme) => ({
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderTopWidth: 0,
|
||||
borderColor: theme.colors.border,
|
||||
backgroundColor: theme.colors.surface1,
|
||||
padding: 0,
|
||||
gap: 0,
|
||||
flexShrink: 1,
|
||||
|
||||
@@ -19,13 +19,17 @@ interface ToolCallDetailsContentProps {
|
||||
detail?: ToolCallDetail;
|
||||
errorText?: string;
|
||||
maxHeight?: number;
|
||||
fillAvailableHeight?: boolean;
|
||||
}
|
||||
|
||||
export function ToolCallDetailsContent({
|
||||
detail,
|
||||
errorText,
|
||||
maxHeight = 300,
|
||||
maxHeight,
|
||||
fillAvailableHeight = false,
|
||||
}: ToolCallDetailsContentProps) {
|
||||
const resolvedMaxHeight = fillAvailableHeight ? undefined : (maxHeight ?? 300);
|
||||
|
||||
// Compute diff lines for edit type
|
||||
const diffLines = useMemo(() => {
|
||||
if (!detail || detail.type !== "edit") return undefined;
|
||||
@@ -39,6 +43,12 @@ export function ToolCallDetailsContent({
|
||||
const sections: ReactNode[] = [];
|
||||
const isFullBleed =
|
||||
detail?.type === "edit" || detail?.type === "shell" || detail?.type === "write";
|
||||
const shouldFill =
|
||||
fillAvailableHeight &&
|
||||
(detail?.type === "shell" ||
|
||||
detail?.type === "edit" ||
|
||||
detail?.type === "write" ||
|
||||
detail?.type === "read");
|
||||
const codeBlockStyle = isFullBleed ? styles.fullBleedBlock : styles.diffContainer;
|
||||
|
||||
if (detail?.type === "shell") {
|
||||
@@ -46,10 +56,17 @@ export function ToolCallDetailsContent({
|
||||
const commandOutput = (detail.output ?? "").replace(/^\n+/, "");
|
||||
const hasOutput = commandOutput.length > 0;
|
||||
sections.push(
|
||||
<View key="shell" style={styles.section}>
|
||||
<View style={codeBlockStyle}>
|
||||
<View
|
||||
key="shell"
|
||||
style={[styles.section, shouldFill && styles.fillHeight]}
|
||||
>
|
||||
<View style={[codeBlockStyle, shouldFill && styles.fillHeight]}>
|
||||
<ScrollView
|
||||
style={[styles.codeVerticalScroll, { maxHeight }]}
|
||||
style={[
|
||||
styles.codeVerticalScroll,
|
||||
resolvedMaxHeight !== undefined && { maxHeight: resolvedMaxHeight },
|
||||
shouldFill && styles.fillHeight,
|
||||
]}
|
||||
contentContainerStyle={styles.codeVerticalContent}
|
||||
nestedScrollEnabled
|
||||
showsVerticalScrollIndicator
|
||||
@@ -74,20 +91,34 @@ export function ToolCallDetailsContent({
|
||||
);
|
||||
} else if (detail?.type === "edit") {
|
||||
sections.push(
|
||||
<View key="edit" style={styles.section}>
|
||||
<View
|
||||
key="edit"
|
||||
style={[styles.section, shouldFill && styles.fillHeight]}
|
||||
>
|
||||
{diffLines ? (
|
||||
<View style={codeBlockStyle}>
|
||||
<DiffViewer diffLines={diffLines} maxHeight={maxHeight} />
|
||||
<View style={[codeBlockStyle, shouldFill && styles.fillHeight]}>
|
||||
<DiffViewer
|
||||
diffLines={diffLines}
|
||||
maxHeight={resolvedMaxHeight}
|
||||
fillAvailableHeight={shouldFill}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
} else if (detail?.type === "write") {
|
||||
sections.push(
|
||||
<View key="write" style={styles.section}>
|
||||
<View
|
||||
key="write"
|
||||
style={[styles.section, shouldFill && styles.fillHeight]}
|
||||
>
|
||||
{detail.content ? (
|
||||
<ScrollView
|
||||
style={[styles.scrollArea, { maxHeight }]}
|
||||
style={[
|
||||
styles.scrollArea,
|
||||
resolvedMaxHeight !== undefined && { maxHeight: resolvedMaxHeight },
|
||||
shouldFill && styles.fillHeight,
|
||||
]}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
nestedScrollEnabled
|
||||
showsVerticalScrollIndicator={true}
|
||||
@@ -104,18 +135,18 @@ export function ToolCallDetailsContent({
|
||||
</View>
|
||||
);
|
||||
} else if (detail?.type === "read") {
|
||||
sections.push(
|
||||
<View key="read" style={styles.section}>
|
||||
{(detail.offset !== undefined || detail.limit !== undefined) ? (
|
||||
<Text style={styles.rangeText}>
|
||||
{detail.offset !== undefined ? `Offset: ${detail.offset}` : ""}
|
||||
{detail.offset !== undefined && detail.limit !== undefined ? " • " : ""}
|
||||
{detail.limit !== undefined ? `Limit: ${detail.limit}` : ""}
|
||||
</Text>
|
||||
) : null}
|
||||
{detail.content ? (
|
||||
if (detail.content) {
|
||||
sections.push(
|
||||
<View
|
||||
key="read"
|
||||
style={[styles.section, shouldFill && styles.fillHeight]}
|
||||
>
|
||||
<ScrollView
|
||||
style={[styles.scrollArea, { maxHeight }]}
|
||||
style={[
|
||||
styles.scrollArea,
|
||||
resolvedMaxHeight !== undefined && { maxHeight: resolvedMaxHeight },
|
||||
shouldFill && styles.fillHeight,
|
||||
]}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
nestedScrollEnabled
|
||||
showsVerticalScrollIndicator={true}
|
||||
@@ -128,9 +159,9 @@ export function ToolCallDetailsContent({
|
||||
<Text selectable style={styles.scrollText}>{detail.content}</Text>
|
||||
</ScrollView>
|
||||
</ScrollView>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
</View>
|
||||
);
|
||||
}
|
||||
} else if (detail?.type === "search") {
|
||||
sections.push(
|
||||
<View key="search" style={styles.section}>
|
||||
@@ -145,16 +176,8 @@ export function ToolCallDetailsContent({
|
||||
|
||||
if (plainInputText !== null) {
|
||||
sections.push(
|
||||
<View key="unknown-plain-text" style={styles.section}>
|
||||
<ScrollView
|
||||
horizontal
|
||||
nestedScrollEnabled
|
||||
style={styles.jsonScroll}
|
||||
contentContainerStyle={styles.jsonContent}
|
||||
showsHorizontalScrollIndicator={true}
|
||||
>
|
||||
<Text selectable style={styles.scrollText}>{plainInputText}</Text>
|
||||
</ScrollView>
|
||||
<View key="unknown-plain-text" style={styles.plainTextSection}>
|
||||
<Text selectable style={styles.scrollText}>{plainInputText}</Text>
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
@@ -225,7 +248,12 @@ export function ToolCallDetailsContent({
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={isFullBleed ? styles.fullBleedContainer : styles.paddedContainer}>
|
||||
<View
|
||||
style={[
|
||||
isFullBleed ? styles.fullBleedContainer : styles.paddedContainer,
|
||||
shouldFill && styles.fillHeight,
|
||||
]}
|
||||
>
|
||||
{sections}
|
||||
</View>
|
||||
);
|
||||
@@ -239,7 +267,7 @@ const styles = StyleSheet.create((theme) => {
|
||||
return {
|
||||
paddedContainer: {
|
||||
gap: theme.spacing[4],
|
||||
padding: theme.spacing[2],
|
||||
padding: 0,
|
||||
},
|
||||
fullBleedContainer: {
|
||||
gap: theme.spacing[2],
|
||||
@@ -249,19 +277,26 @@ const styles = StyleSheet.create((theme) => {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
paddingBottom: theme.spacing[1],
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
paddingVertical: theme.spacing[2],
|
||||
borderBottomWidth: theme.borderWidth[1],
|
||||
borderBottomColor: theme.colors.border,
|
||||
},
|
||||
groupHeaderText: {
|
||||
color: theme.colors.primary,
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.sm,
|
||||
fontWeight: theme.fontWeight.bold,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 1,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
},
|
||||
section: {
|
||||
section: {
|
||||
gap: theme.spacing[2],
|
||||
},
|
||||
fillHeight: {
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
},
|
||||
plainTextSection: {
|
||||
gap: theme.spacing[2],
|
||||
padding: theme.spacing[3],
|
||||
},
|
||||
sectionTitle: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
|
||||
@@ -157,6 +157,7 @@ function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
|
||||
<ToolCallDetailsContent
|
||||
detail={detail}
|
||||
errorText={errorText}
|
||||
fillAvailableHeight
|
||||
/>
|
||||
</BottomSheetScrollView>
|
||||
</View>
|
||||
@@ -208,9 +209,11 @@ const styles = StyleSheet.create((theme) => ({
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
contentContainer: {
|
||||
padding: 0,
|
||||
flexGrow: 1,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -690,12 +690,6 @@ function AgentScreenContent({
|
||||
<MenuHeader title="Agent" />
|
||||
<View style={styles.errorContainer}>
|
||||
<ActivityIndicator size="small" color={theme.colors.foregroundMuted} />
|
||||
<Text style={styles.loadingText}>Loading agent…</Text>
|
||||
{missingAgentState.kind === "error" ? (
|
||||
<Text style={styles.loadingSubtext} numberOfLines={2}>
|
||||
{missingAgentState.message}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
@@ -905,7 +899,6 @@ function AgentScreenContent({
|
||||
{shouldBlockForHistorySync ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={theme.colors.primary} />
|
||||
<Text style={styles.loadingText}>Loading agent...</Text>
|
||||
</View>
|
||||
) : (
|
||||
<ReanimatedAnimated.View
|
||||
@@ -1061,13 +1054,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
fontSize: theme.fontSize.base,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
loadingSubtext: {
|
||||
marginTop: theme.spacing[1],
|
||||
textAlign: "center",
|
||||
fontSize: theme.fontSize.xs,
|
||||
color: theme.colors.foregroundMuted,
|
||||
paddingHorizontal: theme.spacing[6],
|
||||
},
|
||||
centerState: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
|
||||
Reference in New Issue
Block a user