feat(app): improve tool call sheet with word-level diff and better UX

- Add word-level highlighting using LCS algorithm for edit diffs
- Match diff colors to explorer sidebar (GitHub-style rgba backgrounds)
- Use discriminated union for tool call parsing (shell, edit, generic)
- Add horizontal scroll for bash/command output instead of wrapping
- Remove status badge from bottom sheet header
- Fix sheet snap points (60%/95%) and disable dynamic sizing
- Add min-width to diff viewer for full-width backgrounds

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Mohamed Boudra
2026-01-10 23:37:13 +07:00
parent 42129926ae
commit f073be0b98
5 changed files with 518 additions and 468 deletions
+45 -17
View File
@@ -3,7 +3,7 @@ import { View, Text } from "react-native";
import { ScrollView } from "react-native-gesture-handler";
import { StyleSheet } from "react-native-unistyles";
import { Fonts } from "@/constants/theme";
import type { DiffLine } from "@/utils/tool-call-parsers";
import type { DiffLine, DiffSegment } from "@/utils/tool-call-parsers";
interface DiffViewerProps {
diffLines: DiffLine[];
@@ -12,6 +12,8 @@ interface DiffViewerProps {
}
export function DiffViewer({ diffLines, maxHeight = 280, emptyLabel = "No changes to display" }: DiffViewerProps) {
const [scrollViewWidth, setScrollViewWidth] = React.useState(0);
if (!diffLines.length) {
return (
<View style={styles.emptyState}>
@@ -32,8 +34,9 @@ export function DiffViewer({ diffLines, maxHeight = 280, emptyLabel = "No change
nestedScrollEnabled
showsHorizontalScrollIndicator
contentContainerStyle={styles.horizontalContent}
onLayout={(e) => setScrollViewWidth(e.nativeEvent.layout.width)}
>
<View style={styles.linesContainer}>
<View style={[styles.linesContainer, scrollViewWidth > 0 && { minWidth: scrollViewWidth }]}>
{diffLines.map((line, index) => (
<View
key={`${line.type}-${index}`}
@@ -45,17 +48,36 @@ export function DiffViewer({ diffLines, maxHeight = 280, emptyLabel = "No change
line.type === "context" && styles.contextLine,
]}
>
<Text
style={[
styles.lineText,
line.type === "header" && styles.headerText,
line.type === "add" && styles.addText,
line.type === "remove" && styles.removeText,
line.type === "context" && styles.contextText,
]}
>
{line.content}
</Text>
{line.segments ? (
<Text style={styles.lineText}>
<Text style={line.type === "add" ? styles.addText : styles.removeText}>
{line.content[0]}
</Text>
{line.segments.map((segment, segIdx) => (
<Text
key={segIdx}
style={[
line.type === "add" ? styles.addText : styles.removeText,
segment.changed && (line.type === "add" ? styles.addHighlight : styles.removeHighlight),
]}
>
{segment.text}
</Text>
))}
</Text>
) : (
<Text
style={[
styles.lineText,
line.type === "header" && styles.headerText,
line.type === "add" && styles.addText,
line.type === "remove" && styles.removeText,
line.type === "context" && styles.contextText,
]}
>
{line.content}
</Text>
)}
</View>
))}
</View>
@@ -92,16 +114,22 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.mutedForeground,
},
addLine: {
backgroundColor: theme.colors.palette.green[900],
backgroundColor: "rgba(46, 160, 67, 0.15)",
},
addText: {
color: theme.colors.palette.green[200],
color: theme.colors.foreground,
},
removeLine: {
backgroundColor: theme.colors.palette.red[900],
backgroundColor: "rgba(248, 81, 73, 0.1)",
},
removeText: {
color: theme.colors.palette.red[200],
color: theme.colors.foreground,
},
addHighlight: {
backgroundColor: "rgba(46, 160, 67, 0.4)",
},
removeHighlight: {
backgroundColor: "rgba(248, 81, 73, 0.35)",
},
contextLine: {
backgroundColor: theme.colors.card,