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,