feat(app): add git diff viewer with dropdown menu and UI improvements

- Add git diff viewer screen to display local changes per agent
  - Parse git diff output into file sections with syntax highlighting
  - Horizontal scrolling for long diff lines (no text wrapping)
  - Request diff data on mount via new WebSocket messages

- Add dropdown menu to agent header
  - Three-dot menu button with "View Changes" option
  - Position menu below button using window-aware measurements
  - Account for safe area insets and orientation changes

- Implement server-side git operations
  - Add git_diff_request/response message types
  - Execute `git diff HEAD` in agent's working directory
  - Handle errors gracefully with proper error messages

- Update session context for git diff state
  - Add gitDiffs Map to store diff content per agent
  - Add requestGitDiff helper function
  - WebSocket handler for git_diff_response messages

- Improve agent input area layout
  - Move status bar inline with attachment button
  - Full-width text input above button row
  - Better spacing and visual hierarchy

- Simplify agent status bar to show only status dot

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2025-10-26 22:30:43 +01:00
co-authored by Claude
parent 086dde30cc
commit c5446e808d
4 changed files with 205 additions and 131 deletions
@@ -261,11 +261,6 @@ export function AgentInputArea({ agentId }: AgentInputAreaProps) {
return (
<View style={styles.container}>
{/* Status bar */}
<View style={styles.statusBarContainer}>
<AgentStatusBar agentId={agentId} />
</View>
{/* Border separator */}
<View style={styles.borderSeparator} />
@@ -298,10 +293,26 @@ export function AgentInputArea({ agentId }: AgentInputAreaProps) {
</View>
)}
{/* Input row */}
<View style={styles.inputRow}>
{/* Attachment button */}
{!isRecording && (
{/* Full-width text input */}
<TextInput
value={userInput}
onChangeText={setUserInput}
placeholder="Message agent..."
placeholderTextColor={theme.colors.mutedForeground}
style={[
styles.textInput,
{ height: inputHeight, minHeight: MIN_INPUT_HEIGHT, maxHeight: MAX_INPUT_HEIGHT },
]}
multiline
scrollEnabled={inputHeight >= MAX_INPUT_HEIGHT}
onContentSizeChange={handleContentSizeChange}
editable={!isRecording && ws.isConnected}
/>
{/* Button row below input */}
<View style={styles.buttonRow}>
{/* Left button group */}
<View style={styles.leftButtonGroup}>
<Pressable
onPress={handlePickImage}
disabled={!ws.isConnected}
@@ -312,71 +323,52 @@ export function AgentInputArea({ agentId }: AgentInputAreaProps) {
>
<Paperclip size={20} color={theme.colors.foreground} />
</Pressable>
)}
<AgentStatusBar agentId={agentId} />
</View>
{/* Text input */}
<TextInput
value={userInput}
onChangeText={setUserInput}
placeholder="Message agent..."
placeholderTextColor={theme.colors.mutedForeground}
style={[
styles.textInput,
{ height: inputHeight, minHeight: MIN_INPUT_HEIGHT, maxHeight: MAX_INPUT_HEIGHT },
]}
multiline
scrollEnabled={inputHeight >= MAX_INPUT_HEIGHT}
onContentSizeChange={handleContentSizeChange}
editable={!isRecording && ws.isConnected}
/>
{/* Buttons */}
<View style={styles.buttonRow}>
{hasText || hasImages ? (
// Send button when text is entered or images are selected
{/* Right button group */}
<View style={styles.rightButtonGroup}>
{hasText || hasImages ? (
<Pressable
onPress={handleSendMessage}
disabled={!ws.isConnected || isProcessing}
style={[
styles.sendButton,
(!ws.isConnected || isProcessing) && styles.buttonDisabled,
]}
>
<ArrowUp size={20} color="white" />
</Pressable>
) : !isRealtimeMode ? (
<>
<Pressable
onPress={handleSendMessage}
disabled={!ws.isConnected || isProcessing}
onPress={handleVoicePress}
disabled={!ws.isConnected}
style={[
styles.sendButton,
(!ws.isConnected || isProcessing) && styles.buttonDisabled,
styles.voiceButton,
!ws.isConnected && styles.buttonDisabled,
isRecording && styles.voiceButtonRecording,
]}
>
<ArrowUp size={20} color="white" />
{isRecording ? (
<Square size={14} color="white" fill="white" />
) : (
<Mic size={20} color={theme.colors.foreground} />
)}
</Pressable>
) : !isRealtimeMode ? (
// Voice and Realtime buttons when no text and not in realtime mode
<>
{/* Voice recording button */}
<Pressable
onPress={handleVoicePress}
disabled={!ws.isConnected}
style={[
styles.voiceButton,
!ws.isConnected && styles.buttonDisabled,
isRecording && styles.voiceButtonRecording,
]}
>
{isRecording ? (
<Square size={14} color="white" fill="white" />
) : (
<Mic size={20} color={theme.colors.foreground} />
)}
</Pressable>
{/* Realtime button */}
<Pressable
onPress={startRealtime}
disabled={!ws.isConnected}
style={[
styles.realtimeButton,
!ws.isConnected && styles.buttonDisabled,
]}
>
<AudioLines size={20} color={theme.colors.background} />
</Pressable>
</>
) : null}
<Pressable
onPress={startRealtime}
disabled={!ws.isConnected}
style={[
styles.realtimeButton,
!ws.isConnected && styles.buttonDisabled,
]}
>
<AudioLines size={20} color={theme.colors.background} />
</Pressable>
</>
) : null}
</View>
</View>
</Animated.View>
@@ -400,9 +392,6 @@ const styles = StyleSheet.create((theme) => ({
container: {
flexDirection: "column",
},
statusBarContainer: {
backgroundColor: theme.colors.background,
},
borderSeparator: {
height: theme.borderWidth[1],
backgroundColor: theme.colors.border,
@@ -417,8 +406,8 @@ const styles = StyleSheet.create((theme) => ({
inputContainer: {
flexDirection: "column",
paddingHorizontal: theme.spacing[4],
paddingVertical: BASE_VERTICAL_PADDING,
gap: theme.spacing[2],
paddingVertical: theme.spacing[3],
gap: theme.spacing[3],
minHeight: FOOTER_HEIGHT,
},
overlayContainer: {
@@ -452,7 +441,26 @@ const styles = StyleSheet.create((theme) => ({
alignItems: "center",
justifyContent: "center",
},
inputRow: {
textInput: {
width: "100%",
paddingHorizontal: theme.spacing[4],
paddingVertical: theme.spacing[3],
backgroundColor: theme.colors.muted,
borderRadius: theme.borderRadius.lg,
color: theme.colors.foreground,
fontSize: theme.fontSize.base,
},
buttonRow: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
},
leftButtonGroup: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
},
rightButtonGroup: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
@@ -463,20 +471,6 @@ const styles = StyleSheet.create((theme) => ({
alignItems: "center",
justifyContent: "center",
},
textInput: {
flex: 1,
paddingHorizontal: theme.spacing[4],
paddingVertical: theme.spacing[2],
backgroundColor: theme.colors.muted,
borderRadius: theme.borderRadius.lg,
color: theme.colors.foreground,
fontSize: theme.fontSize.base,
},
buttonRow: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
},
sendButton: {
width: 40,
height: 40,
@@ -78,16 +78,13 @@ export function AgentStatusBar({ agentId }: AgentStatusBarProps) {
</Pressable>
)}
{/* Agent Status Indicator */}
<View style={styles.statusIndicator}>
<View
style={[
styles.statusDot,
{ backgroundColor: getStatusColor(agent.status) },
]}
/>
<Text style={styles.statusText}>{getStatusLabel(agent.status)}</Text>
</View>
{/* Agent Status Indicator - just a dot */}
<View
style={[
styles.statusDot,
{ backgroundColor: getStatusColor(agent.status) },
]}
/>
{/* Mode selector modal */}
<ModeSelectorModal
@@ -105,9 +102,6 @@ const styles = StyleSheet.create((theme) => ({
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[3],
paddingHorizontal: theme.spacing[4],
paddingTop: theme.spacing[2],
paddingBottom: theme.spacing[1],
},
modeBadge: {
flexDirection: "row",