diff --git a/packages/app/src/components/agent-input-area.tsx b/packages/app/src/components/agent-input-area.tsx index b0c922ec4..1aeece82b 100644 --- a/packages/app/src/components/agent-input-area.tsx +++ b/packages/app/src/components/agent-input-area.tsx @@ -97,6 +97,7 @@ export function AgentInputArea({ }: AgentInputAreaProps) { markScrollInvestigationRender(`AgentInputArea:${serverId}:${agentId}`) const { theme } = useUnistyles() + const buttonIconSize = Platform.OS === 'web' ? theme.iconSize.md : theme.iconSize.lg const insets = useSafeAreaInsets() const isScreenFocused = useIsFocused() @@ -701,7 +702,7 @@ export function AgentInputArea({ {isCancellingAgent ? ( ) : ( - + )} @@ -731,7 +732,7 @@ export function AgentInputArea({ {voice?.isVoiceSwitching ? ( ) : ( - + )} diff --git a/packages/app/src/components/agent-status-bar.tsx b/packages/app/src/components/agent-status-bar.tsx index 4feefe70d..96c0e8353 100644 --- a/packages/app/src/components/agent-status-bar.tsx +++ b/packages/app/src/components/agent-status-bar.tsx @@ -2,13 +2,12 @@ import { useCallback, useMemo, useRef, useState } from 'react' import { View, Text, Platform, Pressable } from 'react-native' import { StyleSheet, useUnistyles } from 'react-native-unistyles' import { - Bot, Brain, ChevronDown, ShieldAlert, ShieldCheck, ShieldOff, - SlidersHorizontal, + } from 'lucide-react-native' import { getProviderIcon } from '@/components/provider-icons' import { CombinedModelSelector } from '@/components/combined-model-selector' @@ -424,7 +423,8 @@ function ControlledStatusBar({ accessibilityLabel="Agent preferences" testID="agent-preferences-button" > - + + {displayModel} ({ lineHeight: theme.fontSize.sm * 1.4, }, prefsButton: { - width: 28, height: 28, - borderRadius: theme.borderRadius.full, + flexDirection: 'row', alignItems: 'center', - justifyContent: 'center', + gap: theme.spacing[1], + paddingHorizontal: theme.spacing[2], + borderRadius: theme.borderRadius['2xl'], }, prefsButtonPressed: { backgroundColor: theme.colors.surface0, }, + prefsButtonText: { + color: theme.colors.foregroundMuted, + fontSize: theme.fontSize.sm, + fontWeight: theme.fontWeight.normal, + flexShrink: 1, + }, sheetSection: { gap: theme.spacing[2], }, diff --git a/packages/app/src/components/git-diff-pane.tsx b/packages/app/src/components/git-diff-pane.tsx index d54bea9b4..26642b1eb 100644 --- a/packages/app/src/components/git-diff-pane.tsx +++ b/packages/app/src/components/git-diff-pane.tsx @@ -25,6 +25,7 @@ import { ListChevronsUpDown, RefreshCcw, Upload, + WrapText, } from "lucide-react-native"; import { useCheckoutGitActionsStore } from "@/stores/checkout-git-actions-store"; import { @@ -47,6 +48,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"; import { GitHubIcon } from "@/components/icons/github-icon"; import { buildGitActions, @@ -153,7 +155,7 @@ interface DiffFileSectionProps { testID?: string; } -function DiffLineView({ line }: { line: DiffLine }) { +function DiffLineView({ line, lineNumber, gutterWidth }: { line: DiffLine; lineNumber: number | null; gutterWidth: number }) { return ( + + + {lineNumber != null ? String(lineNumber) : ""} + + {line.tokens && line.type !== "header" ? ( void; testID?: string; }) { @@ -332,39 +345,86 @@ function DiffFileBody({ }} testID={testID} > - {file.status === "too_large" || file.status === "binary" ? ( - - - {file.status === "binary" ? "Binary file" : "Diff too large to display"} - - - ) : ( - setScrollViewWidth(e.nativeEvent.layout.width)} - // When at left edge, wait for close gesture to fail before scrolling. - // The close gesture fails quickly on leftward swipes (failOffsetX=-10), - // so scrolling left works normally. On rightward swipes, close gesture - // activates and closes the sidebar. - waitFor={isAtLeftEdge && closeGestureRef?.current ? closeGestureRef : undefined} - > - 0 && { minWidth: scrollViewWidth }]}> - {file.hunks.map((hunk, hunkIndex) => - hunk.lines.map((line, lineIndex) => ( - - )) - )} - - - )} + {(() => { + if (file.status === "too_large" || file.status === "binary") { + return ( + + + {file.status === "binary" ? "Binary file" : "Diff too large to display"} + + + ); + } + + const linesContent = (() => { + let maxLineNo = 0; + for (const hunk of file.hunks) { + maxLineNo = Math.max(maxLineNo, hunk.oldStart + hunk.oldCount, hunk.newStart + hunk.newCount); + } + const digitCount = Math.max(1, String(maxLineNo).length); + const gutterWidth = digitCount * 8 + 12; + return file.hunks.map((hunk, hunkIndex) => { + let oldLineNo = hunk.oldStart; + let newLineNo = hunk.newStart; + return hunk.lines.map((line, lineIndex) => { + let lineNumber: number | null = null; + if (line.type === "remove") { + lineNumber = oldLineNo; + oldLineNo++; + } else if (line.type === "add") { + lineNumber = newLineNo; + newLineNo++; + } else if (line.type === "context") { + lineNumber = newLineNo; + oldLineNo++; + newLineNo++; + } + return ( + + ); + }); + }); + })(); + + if (wrapLines) { + return ( + + + {linesContent} + + + ); + } + + return ( + setScrollViewWidth(e.nativeEvent.layout.width)} + // When at left edge, wait for close gesture to fail before scrolling. + // The close gesture fails quickly on leftward swipes (failOffsetX=-10), + // so scrolling left works normally. On rightward swipes, close gesture + // activates and closes the sidebar. + waitFor={isAtLeftEdge && closeGestureRef?.current ? closeGestureRef : undefined} + > + 0 && { minWidth: scrollViewWidth }]}> + {linesContent} + + + ); + })()} ); } @@ -390,6 +450,22 @@ export function GitDiffPane({ serverId, workspaceId, cwd, hideHeaderRow }: GitDi const [actionError, setActionError] = useState(null); const [postShipArchiveSuggested, setPostShipArchiveSuggested] = useState(false); const [shipDefault, setShipDefault] = useState<"merge" | "pr">("merge"); + const [wrapLines, setWrapLines] = useState(false); + + useEffect(() => { + AsyncStorage.getItem("diff-wrap-lines").then((value) => { + if (value === "true") setWrapLines(true); + }); + }, []); + + const handleToggleWrapLines = useCallback(() => { + setWrapLines((prev) => { + const next = !prev; + AsyncStorage.setItem("diff-wrap-lines", String(next)); + return next; + }); + }, []); + const { status, isLoading: isStatusLoading, isFetching: isStatusFetching, isError: isStatusError, error: statusError, refresh: refreshStatus } = useCheckoutStatusQuery({ serverId, cwd }); const gitStatus = status && status.isGit ? status : null; @@ -736,12 +812,13 @@ export function GitDiffPane({ serverId, workspaceId, cwd, hideHeaderRow }: GitDi return ( ); }, - [handleBodyHeightChange, handleHeaderHeightChange, handleToggleExpanded] + [handleBodyHeightChange, handleHeaderHeightChange, handleToggleExpanded, wrapLines] ); const flatKeyExtractor = useCallback( @@ -1018,19 +1095,48 @@ export function GitDiffPane({ serverId, workspaceId, cwd, hideHeaderRow }: GitDi {files.length > 0 ? ( - [ - styles.expandAllButton, - (hovered || pressed) && styles.diffStatusRowHovered, - ]} - onPress={handleToggleExpandAll} - > - {allExpanded ? ( - - ) : ( - - )} - + + + + [ + styles.expandAllButton, + (hovered || pressed) && styles.diffStatusRowHovered, + ]} + onPress={handleToggleWrapLines} + > + + + + + + {wrapLines ? "Scroll long lines" : "Wrap long lines"} + + + + + + [ + styles.expandAllButton, + (hovered || pressed) && styles.diffStatusRowHovered, + ]} + onPress={handleToggleExpandAll} + > + {allExpanded ? ( + + ) : ( + + )} + + + + + {allExpanded ? "Collapse all files" : "Expand all files"} + + + + ) : null} @@ -1125,13 +1231,30 @@ const styles = StyleSheet.create((theme) => ({ diffStatusIconHidden: { opacity: 0, }, + diffStatusButtons: { + flexDirection: "row", + alignItems: "center", + gap: { + xs: theme.spacing[1], + sm: theme.spacing[1], + md: 0, + }, + }, expandAllButton: { flexDirection: "row", alignItems: "center", gap: theme.spacing[1], marginVertical: theme.spacing[2], - paddingHorizontal: theme.spacing[1], - paddingVertical: theme.spacing[1], + paddingHorizontal: { + xs: theme.spacing[2], + sm: theme.spacing[2], + md: theme.spacing[1], + }, + paddingVertical: { + xs: theme.spacing[2], + sm: theme.spacing[2], + md: theme.spacing[1], + }, borderRadius: theme.borderRadius.base, }, actionErrorText: { @@ -1289,10 +1412,34 @@ const styles = StyleSheet.create((theme) => ({ backgroundColor: theme.colors.surface1, }, diffLineContainer: { - paddingHorizontal: theme.spacing[3], + flexDirection: "row", + alignItems: "stretch", + }, + lineNumberGutter: { + borderRightWidth: theme.borderWidth[1], + borderRightColor: theme.colors.border, + marginRight: theme.spacing[2], + alignSelf: "stretch", + justifyContent: "center", + }, + lineNumberText: { + textAlign: "right", + paddingRight: theme.spacing[2], paddingVertical: theme.spacing[1], + fontSize: theme.fontSize.xs, + fontFamily: Fonts.mono, + color: theme.colors.foregroundMuted, + }, + addLineNumberText: { + color: theme.colors.palette.green[400], + }, + removeLineNumberText: { + color: theme.colors.palette.red[500], }, diffLineText: { + flex: 1, + paddingRight: theme.spacing[3], + paddingVertical: theme.spacing[1], fontSize: theme.fontSize.xs, fontFamily: Fonts.mono, color: theme.colors.foreground, @@ -1333,4 +1480,8 @@ const styles = StyleSheet.create((theme) => ({ color: theme.colors.foregroundMuted, fontStyle: "italic", }, + tooltipText: { + fontSize: theme.fontSize.xs, + color: theme.colors.foreground, + }, })); diff --git a/packages/app/src/components/message-input.tsx b/packages/app/src/components/message-input.tsx index 2cf0ea7c7..7ea9d1a4a 100644 --- a/packages/app/src/components/message-input.tsx +++ b/packages/app/src/components/message-input.tsx @@ -212,6 +212,7 @@ export const MessageInput = forwardRef(funct ref ) { const { theme } = useUnistyles() + const buttonIconSize = IS_WEB ? theme.iconSize.md : theme.iconSize.lg const investigationComponentId = `MessageInput:${voiceServerId ?? 'unknown-server'}:${voiceAgentId ?? 'unknown-agent'}` markScrollInvestigationRender(investigationComponentId) const toast = useToast() @@ -950,7 +951,7 @@ export const MessageInput = forwardRef(funct (!isConnected || disabled) && styles.buttonDisabled, ]} > - + Attach images @@ -984,11 +985,11 @@ export const MessageInput = forwardRef(funct ]} > {isDictating ? ( - + ) : isRealtimeVoiceForCurrentAgent && voice?.isMuted ? ( - + ) : ( - + )} @@ -1021,7 +1022,7 @@ export const MessageInput = forwardRef(funct (!isConnected || disabled) && styles.buttonDisabled, ]} > - + @@ -1043,7 +1044,7 @@ export const MessageInput = forwardRef(funct {isSubmitLoading ? ( ) : ( - + )} @@ -1184,12 +1185,12 @@ const styles = StyleSheet.create(((theme: any) => ({ leftButtonGroup: { flexDirection: 'row', alignItems: 'flex-end', - gap: theme.spacing[2], + gap: Platform.OS === 'web' ? theme.spacing[2] : theme.spacing[1], }, rightButtonGroup: { flexDirection: 'row', alignItems: 'center', - gap: theme.spacing[2], + gap: Platform.OS === 'web' ? theme.spacing[2] : theme.spacing[1], }, attachButton: { width: 28,