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:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user