Merge branch 'fix/diff-padding'

This commit is contained in:
Mohamed Boudra
2026-02-04 12:58:26 +07:00
5 changed files with 176 additions and 152 deletions
@@ -0,0 +1,9 @@
export function getCodeInsets(theme: any) {
const paddingX = theme.spacing[3] + theme.spacing[2];
const paddingY = theme.spacing[1];
const extraRight = theme.spacing[4];
const extraBottom = theme.spacing[3];
return { paddingX, paddingY, extraRight, extraBottom };
}
+68 -61
View File
@@ -4,6 +4,7 @@ import { ScrollView } 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";
interface DiffViewerProps { interface DiffViewerProps {
diffLines: DiffLine[]; diffLines: DiffLine[];
@@ -86,64 +87,70 @@ export function DiffViewer({ diffLines, maxHeight = 280, emptyLabel = "No change
); );
} }
const styles = StyleSheet.create((theme) => ({ const styles = StyleSheet.create((theme) => {
verticalScroll: {}, const insets = getCodeInsets(theme);
verticalContent: {
flexGrow: 1, return {
}, verticalScroll: {},
horizontalContent: { verticalContent: {
flexDirection: "column" as const, flexGrow: 1,
}, paddingBottom: insets.extraBottom,
linesContainer: { },
alignSelf: "flex-start", horizontalContent: {
}, flexDirection: "column" as const,
line: { paddingRight: insets.extraRight,
minWidth: "100%", },
paddingHorizontal: theme.spacing[3], linesContainer: {
paddingVertical: theme.spacing[1], alignSelf: "flex-start",
}, },
lineText: { line: {
fontFamily: Fonts.mono, minWidth: "100%",
fontSize: theme.fontSize.xs, paddingHorizontal: insets.paddingX,
color: theme.colors.foreground, paddingVertical: insets.paddingY,
}, },
headerLine: { lineText: {
backgroundColor: theme.colors.surface2, fontFamily: Fonts.mono,
}, fontSize: theme.fontSize.xs,
headerText: { color: theme.colors.foreground,
color: theme.colors.foregroundMuted, },
}, headerLine: {
addLine: { backgroundColor: theme.colors.surface2,
backgroundColor: "rgba(46, 160, 67, 0.15)", },
}, headerText: {
addText: { color: theme.colors.foregroundMuted,
color: theme.colors.foreground, },
}, addLine: {
removeLine: { backgroundColor: "rgba(46, 160, 67, 0.15)",
backgroundColor: "rgba(248, 81, 73, 0.1)", },
}, addText: {
removeText: { color: theme.colors.foreground,
color: theme.colors.foreground, },
}, removeLine: {
addHighlight: { backgroundColor: "rgba(248, 81, 73, 0.1)",
backgroundColor: "rgba(46, 160, 67, 0.4)", },
}, removeText: {
removeHighlight: { color: theme.colors.foreground,
backgroundColor: "rgba(248, 81, 73, 0.35)", },
}, addHighlight: {
contextLine: { backgroundColor: "rgba(46, 160, 67, 0.4)",
backgroundColor: theme.colors.surface2, },
}, removeHighlight: {
contextText: { backgroundColor: "rgba(248, 81, 73, 0.35)",
color: theme.colors.foregroundMuted, },
}, contextLine: {
emptyState: { backgroundColor: theme.colors.surface2,
padding: theme.spacing[4], },
alignItems: "center" as const, contextText: {
justifyContent: "center" as const, color: theme.colors.foregroundMuted,
}, },
emptyText: { emptyState: {
fontSize: theme.fontSize.sm, padding: theme.spacing[4],
color: theme.colors.foregroundMuted, alignItems: "center" as const,
}, justifyContent: "center" as const,
})); },
emptyText: {
fontSize: theme.fontSize.sm,
color: theme.colors.foregroundMuted,
},
};
});
+29 -38
View File
@@ -389,8 +389,8 @@ const expandableBadgeStylesheet = StyleSheet.create((theme) => ({
borderTopWidth: 0, borderTopWidth: 0,
borderColor: theme.colors.borderAccent, borderColor: theme.colors.borderAccent,
backgroundColor: theme.colors.surface0, backgroundColor: theme.colors.surface0,
padding: theme.spacing[2], padding: 0,
gap: theme.spacing[2], gap: 0,
flexShrink: 1, flexShrink: 1,
minWidth: 0, minWidth: 0,
}, },
@@ -781,6 +781,9 @@ interface TodoListCardProps {
} }
const todoListCardStylesheet = StyleSheet.create((theme) => ({ const todoListCardStylesheet = StyleSheet.create((theme) => ({
detailsWrapper: {
padding: theme.spacing[2],
},
list: { list: {
gap: theme.spacing[1], gap: theme.spacing[1],
}, },
@@ -832,26 +835,28 @@ export const TodoListCard = memo(function TodoListCard({
const renderDetails = useCallback(() => { const renderDetails = useCallback(() => {
return ( return (
<View style={todoListCardStylesheet.list}> <View style={todoListCardStylesheet.detailsWrapper}>
{items.length === 0 ? ( <View style={todoListCardStylesheet.list}>
<Text style={todoListCardStylesheet.emptyText}>No tasks yet.</Text> {items.length === 0 ? (
) : ( <Text style={todoListCardStylesheet.emptyText}>No tasks yet.</Text>
items.map((item, idx) => ( ) : (
<View key={`${item.text}-${idx}`} style={todoListCardStylesheet.itemRow}> items.map((item, idx) => (
<View style={todoListCardStylesheet.radioOuter}> <View key={`${item.text}-${idx}`} style={todoListCardStylesheet.itemRow}>
{item.completed ? <View style={todoListCardStylesheet.radioInner} /> : null} <View style={todoListCardStylesheet.radioOuter}>
{item.completed ? <View style={todoListCardStylesheet.radioInner} /> : null}
</View>
<Text
style={[
todoListCardStylesheet.itemText,
item.completed && todoListCardStylesheet.itemTextCompleted,
]}
>
{item.text}
</Text>
</View> </View>
<Text ))
style={[ )}
todoListCardStylesheet.itemText, </View>
item.completed && todoListCardStylesheet.itemTextCompleted,
]}
>
{item.text}
</Text>
</View>
))
)}
</View> </View>
); );
}, [items]); }, [items]);
@@ -1099,12 +1104,13 @@ export const ToolCall = memo(function ToolCall({
args, args,
result, result,
error, error,
cwd,
}); });
} else { } else {
// Desktop: toggle inline expansion // Desktop: toggle inline expansion
setIsExpanded((prev) => !prev); setIsExpanded((prev) => !prev);
} }
}, [isMobile, openToolCall, toolName, kind, status, args, result, error]); }, [isMobile, openToolCall, toolName, kind, status, args, result, error, cwd]);
useEffect(() => { useEffect(() => {
if (isMobile || !isPerfLoggingEnabled()) { if (isMobile || !isPerfLoggingEnabled()) {
@@ -1136,15 +1142,7 @@ export const ToolCall = memo(function ToolCall({
// Render inline details for desktop // Render inline details for desktop
const renderDetails = useCallback(() => { const renderDetails = useCallback(() => {
if (isMobile) return null; if (isMobile) return null;
return ( return <ToolCallDetailsContent display={display} errorText={errorText} maxHeight={400} />;
<View style={toolCallInlineStyles.detailsContainer}>
<ToolCallDetailsContent
display={display}
errorText={errorText}
maxHeight={400}
/>
</View>
);
}, [isMobile, display, errorText]); }, [isMobile, display, errorText]);
return ( return (
@@ -1169,10 +1167,3 @@ export const ToolCall = memo(function ToolCall({
/> />
); );
}); });
const toolCallInlineStyles = StyleSheet.create((theme) => ({
detailsContainer: {
paddingTop: theme.spacing[3],
paddingBottom: theme.spacing[2],
},
}));
@@ -11,6 +11,7 @@ import {
type ToolCallDisplay, type ToolCallDisplay,
} from "@/utils/tool-call-parsers"; } from "@/utils/tool-call-parsers";
import { DiffViewer } from "./diff-viewer"; import { DiffViewer } from "./diff-viewer";
import { getCodeInsets } from "./code-insets";
// ---- Types ---- // ---- Types ----
@@ -157,46 +158,37 @@ export function ToolCallDetailsContent({
}, [display]); }, [display]);
const sections: ReactNode[] = []; const sections: ReactNode[] = [];
const isFullBleed = display.type === "edit" || display.type === "shell";
if (display.type === "shell") { if (display.type === "shell") {
const combinedText = `$ ${display.command}${display.output ? `\n${display.output}` : ""}`;
sections.push( sections.push(
<View key="shell" style={styles.section}> <View key="shell" style={styles.section}>
<Text style={styles.sectionTitle}>Command</Text> <View style={styles.diffContainer}>
<ScrollView
horizontal
nestedScrollEnabled
style={styles.jsonScroll}
contentContainerStyle={styles.jsonContent}
showsHorizontalScrollIndicator={true}
>
<Text selectable style={styles.scrollText}>{display.command}</Text>
</ScrollView>
{display.output ? (
<ScrollView <ScrollView
style={[styles.scrollArea, { maxHeight }]} style={[styles.codeVerticalScroll, { maxHeight }]}
contentContainerStyle={styles.scrollContent} contentContainerStyle={styles.codeVerticalContent}
nestedScrollEnabled nestedScrollEnabled
showsVerticalScrollIndicator={true} showsVerticalScrollIndicator
> >
<ScrollView <ScrollView
horizontal horizontal
nestedScrollEnabled nestedScrollEnabled
showsHorizontalScrollIndicator={true} showsHorizontalScrollIndicator
contentContainerStyle={styles.codeHorizontalContent}
> >
<Text selectable style={styles.scrollText}>{display.output}</Text> <View style={styles.codeLine}>
<Text selectable style={styles.scrollText}>{combinedText}</Text>
</View>
</ScrollView> </ScrollView>
</ScrollView> </ScrollView>
) : null} </View>
</View> </View>
); );
} else if (display.type === "edit") { } else if (display.type === "edit") {
sections.push( sections.push(
<View key="edit" style={styles.section}> <View key="edit" style={styles.section}>
<Text style={styles.sectionTitle}>File</Text> {diffLines ? (
<View style={styles.fileBadge}>
<Text style={styles.fileBadgeText}>{display.filePath}</Text>
</View>
{diffLines && diffLines.length > 0 ? (
<View style={styles.diffContainer}> <View style={styles.diffContainer}>
<DiffViewer diffLines={diffLines} maxHeight={maxHeight} /> <DiffViewer diffLines={diffLines} maxHeight={maxHeight} />
</View> </View>
@@ -206,10 +198,6 @@ export function ToolCallDetailsContent({
} else if (display.type === "read") { } else if (display.type === "read") {
sections.push( sections.push(
<View key="read" style={styles.section}> <View key="read" style={styles.section}>
<Text style={styles.sectionTitle}>File</Text>
<View style={styles.fileBadge}>
<Text style={styles.fileBadgeText}>{display.filePath}</Text>
</View>
{(display.offset !== undefined || display.limit !== undefined) ? ( {(display.offset !== undefined || display.limit !== undefined) ? (
<Text style={styles.rangeText}> <Text style={styles.rangeText}>
{display.offset !== undefined ? `Offset: ${display.offset}` : ""} {display.offset !== undefined ? `Offset: ${display.offset}` : ""}
@@ -326,7 +314,11 @@ export function ToolCallDetailsContent({
); );
} }
return <View style={styles.container}>{sections}</View>; return (
<View style={isFullBleed ? styles.fullBleedContainer : styles.paddedContainer}>
{sections}
</View>
);
} }
// ---- Hook for parsing tool call data ---- // ---- Hook for parsing tool call data ----
@@ -362,10 +354,18 @@ export function useToolCallDetails(data: ToolCallDetailsData) {
// ---- Styles ---- // ---- Styles ----
const styles = StyleSheet.create((theme) => ({ const styles = StyleSheet.create((theme) => {
container: { const insets = getCodeInsets(theme);
gap: theme.spacing[4],
}, return {
paddedContainer: {
gap: theme.spacing[4],
padding: theme.spacing[2],
},
fullBleedContainer: {
gap: theme.spacing[2],
padding: 0,
},
groupHeader: { groupHeader: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
@@ -391,20 +391,6 @@ const styles = StyleSheet.create((theme) => ({
textTransform: "uppercase", textTransform: "uppercase",
letterSpacing: 0.5, letterSpacing: 0.5,
}, },
fileBadge: {
alignSelf: "flex-start",
paddingHorizontal: theme.spacing[2],
paddingVertical: theme.spacing[1],
borderRadius: theme.borderRadius.base,
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.border,
backgroundColor: theme.colors.surface2,
},
fileBadgeText: {
color: theme.colors.foreground,
fontFamily: Fonts.mono,
fontSize: theme.fontSize.xs,
},
rangeText: { rangeText: {
color: theme.colors.foregroundMuted, color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.xs,
@@ -416,6 +402,19 @@ const styles = StyleSheet.create((theme) => ({
overflow: "hidden", overflow: "hidden",
backgroundColor: theme.colors.surface2, backgroundColor: theme.colors.surface2,
}, },
codeVerticalScroll: {},
codeVerticalContent: {
flexGrow: 1,
paddingBottom: insets.extraBottom,
},
codeHorizontalContent: {
paddingRight: insets.extraRight,
},
codeLine: {
minWidth: "100%",
paddingHorizontal: insets.paddingX,
paddingVertical: insets.paddingY,
},
scrollArea: { scrollArea: {
borderWidth: theme.borderWidth[1], borderWidth: theme.borderWidth[1],
borderColor: theme.colors.border, borderColor: theme.colors.border,
@@ -451,4 +450,5 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.sm,
fontStyle: "italic", fontStyle: "italic",
}, },
})); };
});
@@ -16,6 +16,7 @@ import {
BottomSheetBackgroundProps, BottomSheetBackgroundProps,
} from "@gorhom/bottom-sheet"; } from "@gorhom/bottom-sheet";
import { Pencil, Eye, SquareTerminal, Search, Wrench, X } from "lucide-react-native"; import { Pencil, Eye, SquareTerminal, Search, Wrench, X } from "lucide-react-native";
import { extractPrincipalParam } from "@/utils/tool-call-parsers";
import { ToolCallDetailsContent, useToolCallDetails } from "./tool-call-details"; import { ToolCallDetailsContent, useToolCallDetails } from "./tool-call-details";
// ----- Types ----- // ----- Types -----
@@ -24,6 +25,7 @@ export interface ToolCallSheetData {
toolName: string; toolName: string;
kind?: string; kind?: string;
status?: "executing" | "completed" | "failed"; status?: "executing" | "completed" | "failed";
cwd?: string;
args?: unknown; args?: unknown;
result?: unknown; result?: unknown;
error?: unknown; error?: unknown;
@@ -138,13 +140,14 @@ interface ToolCallSheetContentProps {
} }
function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) { function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
const { toolName, kind, args, result, error } = data; const { toolName, kind, cwd, args, result, error } = data;
const IconComponent = kind const IconComponent = kind
? toolKindIcons[kind.toLowerCase()] || Wrench ? toolKindIcons[kind.toLowerCase()] || Wrench
: Wrench; : Wrench;
const { display, errorText } = useToolCallDetails({ toolName, args, result, error }); const { display, errorText } = useToolCallDetails({ toolName, args, result, error });
const principalParam = useMemo(() => extractPrincipalParam(args, cwd), [args, cwd]);
return ( return (
<View style={styles.container}> <View style={styles.container}>
@@ -152,9 +155,16 @@ function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
<View style={styles.header}> <View style={styles.header}>
<View style={styles.headerLeft}> <View style={styles.headerLeft}>
<IconComponent size={20} color={styles.headerIcon.color} /> <IconComponent size={20} color={styles.headerIcon.color} />
<Text style={styles.headerTitle} numberOfLines={1}> <View style={styles.headerTextColumn}>
{display.toolName} <Text style={styles.headerTitle} numberOfLines={1}>
</Text> {display.toolName}
</Text>
{principalParam ? (
<Text style={styles.headerSubtitle} numberOfLines={1}>
{principalParam}
</Text>
) : null}
</View>
</View> </View>
<Pressable onPress={onClose} style={styles.closeButton}> <Pressable onPress={onClose} style={styles.closeButton}>
<X size={20} color={styles.closeIcon.color} /> <X size={20} color={styles.closeIcon.color} />
@@ -200,6 +210,10 @@ const styles = StyleSheet.create((theme) => ({
gap: theme.spacing[2], gap: theme.spacing[2],
flex: 1, flex: 1,
}, },
headerTextColumn: {
flex: 1,
minWidth: 0,
},
headerIcon: { headerIcon: {
color: theme.colors.foreground, color: theme.colors.foreground,
}, },
@@ -209,6 +223,11 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foreground, color: theme.colors.foreground,
flex: 1, flex: 1,
}, },
headerSubtitle: {
marginTop: theme.spacing[1],
fontSize: theme.fontSize.sm,
color: theme.colors.foregroundMuted,
},
closeButton: { closeButton: {
padding: theme.spacing[2], padding: theme.spacing[2],
}, },
@@ -220,8 +239,6 @@ const styles = StyleSheet.create((theme) => ({
backgroundColor: theme.colors.surface2, backgroundColor: theme.colors.surface2,
}, },
contentContainer: { contentContainer: {
paddingHorizontal: theme.spacing[4], padding: 0,
paddingTop: theme.spacing[4],
paddingBottom: theme.spacing[8],
}, },
})); }));