Update files

This commit is contained in:
Mohamed Boudra
2026-02-10 13:43:07 +07:00
parent ade4fb2e45
commit 7e5537bcc5
29 changed files with 1296 additions and 515 deletions
@@ -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>
+16 -2
View File
@@ -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,
+40 -2
View File
@@ -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]
-1
View File
@@ -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",