diff --git a/packages/app/src/components/artifact-drawer.tsx b/packages/app/src/components/artifact-drawer.tsx index d6a43a809..55699e1eb 100644 --- a/packages/app/src/components/artifact-drawer.tsx +++ b/packages/app/src/components/artifact-drawer.tsx @@ -2,6 +2,7 @@ import { View, Text, ScrollView, Pressable, Modal } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import { StyleSheet } from "react-native-unistyles"; import { Fonts } from "@/constants/theme"; +import { useWebScrollbarStyle } from "@/hooks/use-web-scrollbar-style"; export interface Artifact { id: string; @@ -147,6 +148,8 @@ const styles = StyleSheet.create((theme) => ({ })); export function ArtifactDrawer({ artifact, onClose }: ArtifactDrawerProps) { + const webScrollbarStyle = useWebScrollbarStyle(); + if (!artifact) { return null; } @@ -190,7 +193,7 @@ export function ArtifactDrawer({ artifact, onClose }: ArtifactDrawerProps) { {/* Content */} {artifact.type === "image" ? ( @@ -200,7 +203,11 @@ export function ArtifactDrawer({ artifact, onClose }: ArtifactDrawerProps) { ) : ( - + {content} diff --git a/packages/app/src/components/diff-scroll.web.tsx b/packages/app/src/components/diff-scroll.web.tsx index 65c8dce2d..364c752b2 100644 --- a/packages/app/src/components/diff-scroll.web.tsx +++ b/packages/app/src/components/diff-scroll.web.tsx @@ -1,4 +1,5 @@ import { ScrollView, type LayoutChangeEvent, type StyleProp, type ViewStyle } from "react-native"; +import { useWebScrollbarStyle } from "@/hooks/use-web-scrollbar-style"; interface DiffScrollProps { children: React.ReactNode; @@ -14,12 +15,14 @@ export function DiffScroll({ style, contentContainerStyle, }: DiffScrollProps) { + const webScrollbarStyle = useWebScrollbarStyle(); + return ( onScrollViewWidthChange(e.nativeEvent.layout.width)} > diff --git a/packages/app/src/components/diff-viewer.tsx b/packages/app/src/components/diff-viewer.tsx index 7fe88e8df..ea4bb7147 100644 --- a/packages/app/src/components/diff-viewer.tsx +++ b/packages/app/src/components/diff-viewer.tsx @@ -4,6 +4,7 @@ import { ScrollView as GHScrollView } from "react-native-gesture-handler"; import { StyleSheet } from "react-native-unistyles"; import { Fonts } from "@/constants/theme"; import type { DiffLine, DiffSegment } from "@/utils/tool-call-parsers"; +import { useWebScrollbarStyle } from "@/hooks/use-web-scrollbar-style"; import { getCodeInsets } from "./code-insets"; import { isWeb } from "@/constants/platform"; @@ -23,6 +24,7 @@ export function DiffViewer({ fillAvailableHeight = false, }: DiffViewerProps) { const [scrollViewWidth, setScrollViewWidth] = React.useState(0); + const webScrollbarStyle = useWebScrollbarStyle(); if (!diffLines.length) { return ( @@ -38,6 +40,7 @@ export function DiffViewer({ styles.verticalScroll, maxHeight !== undefined && { maxHeight }, fillAvailableHeight && styles.fillHeight, + webScrollbarStyle, ]} contentContainerStyle={styles.verticalContent} nestedScrollEnabled @@ -47,6 +50,7 @@ export function DiffViewer({ horizontal nestedScrollEnabled showsHorizontalScrollIndicator + style={webScrollbarStyle} contentContainerStyle={styles.horizontalContent} onLayout={(e) => setScrollViewWidth(e.nativeEvent.layout.width)} > diff --git a/packages/app/src/components/file-pane.tsx b/packages/app/src/components/file-pane.tsx index 7341bf888..4d5be8744 100644 --- a/packages/app/src/components/file-pane.tsx +++ b/packages/app/src/components/file-pane.tsx @@ -13,6 +13,7 @@ import { useIsCompactFormFactor } from "@/constants/layout"; import { Fonts } from "@/constants/theme"; import { useSessionStore, type ExplorerFile } from "@/stores/session-store"; import { useWebScrollViewScrollbar } from "@/components/use-web-scrollbar"; +import { useWebScrollbarStyle } from "@/hooks/use-web-scrollbar-style"; import { highlightCode, darkHighlightColors, @@ -125,6 +126,7 @@ function FilePreviewBody({ const isMarkdownFile = preview?.kind === "text" && isRenderedMarkdownFile(filePath); const previewScrollRef = useRef(null); + const webScrollbarStyle = useWebScrollbarStyle(); const scrollbar = useWebScrollViewScrollbar(previewScrollRef, { enabled: showDesktopWebScrollbar, }); @@ -216,6 +218,7 @@ function FilePreviewBody({ horizontal nestedScrollEnabled showsHorizontalScrollIndicator + style={webScrollbarStyle} contentContainerStyle={styles.previewCodeScrollContent} > {codeLines} diff --git a/packages/app/src/components/tool-call-details.tsx b/packages/app/src/components/tool-call-details.tsx index 75c94aa2e..fbc5614ee 100644 --- a/packages/app/src/components/tool-call-details.tsx +++ b/packages/app/src/components/tool-call-details.tsx @@ -6,6 +6,7 @@ import { Fonts } from "@/constants/theme"; import type { ToolCallDetail } from "@server/server/agent/agent-sdk-types"; import { buildLineDiff, parseUnifiedDiff } from "@/utils/tool-call-parsers"; import { hasMeaningfulToolCallDetail } from "@/utils/tool-call-detail-state"; +import { useWebScrollbarStyle } from "@/hooks/use-web-scrollbar-style"; import { DiffViewer } from "./diff-viewer"; import { getCodeInsets } from "./code-insets"; import { isWeb } from "@/constants/platform"; @@ -30,6 +31,7 @@ export function ToolCallDetailsContent({ showLoadingSkeleton = false, }: ToolCallDetailsContentProps) { const resolvedMaxHeight = fillAvailableHeight ? undefined : (maxHeight ?? 300); + const webScrollbarStyle = useWebScrollbarStyle(); // Compute diff lines for edit type const diffLines = useMemo(() => { @@ -65,6 +67,7 @@ export function ToolCallDetailsContent({ styles.codeVerticalScroll, resolvedMaxHeight !== undefined && { maxHeight: resolvedMaxHeight }, shouldFill && styles.fillHeight, + webScrollbarStyle, ]} contentContainerStyle={styles.codeVerticalContent} nestedScrollEnabled @@ -74,6 +77,7 @@ export function ToolCallDetailsContent({ horizontal nestedScrollEnabled showsHorizontalScrollIndicator + style={webScrollbarStyle} contentContainerStyle={styles.codeHorizontalContent} > @@ -99,6 +103,7 @@ export function ToolCallDetailsContent({ styles.codeVerticalScroll, resolvedMaxHeight !== undefined && { maxHeight: resolvedMaxHeight }, shouldFill && styles.fillHeight, + webScrollbarStyle, ]} contentContainerStyle={styles.codeVerticalContent} nestedScrollEnabled @@ -108,6 +113,7 @@ export function ToolCallDetailsContent({ horizontal nestedScrollEnabled showsHorizontalScrollIndicator + style={webScrollbarStyle} contentContainerStyle={styles.codeHorizontalContent} > @@ -137,6 +143,7 @@ export function ToolCallDetailsContent({ styles.codeVerticalScroll, resolvedMaxHeight !== undefined && { maxHeight: resolvedMaxHeight }, shouldFill && styles.fillHeight, + webScrollbarStyle, ]} contentContainerStyle={styles.codeVerticalContent} nestedScrollEnabled @@ -146,6 +153,7 @@ export function ToolCallDetailsContent({ horizontal nestedScrollEnabled showsHorizontalScrollIndicator + style={webScrollbarStyle} contentContainerStyle={styles.codeHorizontalContent} > @@ -181,12 +189,18 @@ export function ToolCallDetailsContent({ styles.scrollArea, resolvedMaxHeight !== undefined && { maxHeight: resolvedMaxHeight }, shouldFill && styles.fillHeight, + webScrollbarStyle, ]} contentContainerStyle={styles.scrollContent} nestedScrollEnabled showsVerticalScrollIndicator={true} > - + {detail.content} @@ -204,12 +218,18 @@ export function ToolCallDetailsContent({ styles.scrollArea, resolvedMaxHeight !== undefined && { maxHeight: resolvedMaxHeight }, shouldFill && styles.fillHeight, + webScrollbarStyle, ]} contentContainerStyle={styles.scrollContent} nestedScrollEnabled showsVerticalScrollIndicator={true} > - + {detail.content} @@ -236,12 +256,18 @@ export function ToolCallDetailsContent({ style={[ styles.scrollArea, resolvedMaxHeight !== undefined && { maxHeight: resolvedMaxHeight }, + webScrollbarStyle, ]} contentContainerStyle={styles.scrollContent} nestedScrollEnabled showsVerticalScrollIndicator > - + {detail.content} @@ -286,12 +312,18 @@ export function ToolCallDetailsContent({ styles.scrollArea, resolvedMaxHeight !== undefined && { maxHeight: resolvedMaxHeight }, shouldFill && styles.fillHeight, + webScrollbarStyle, ]} contentContainerStyle={styles.scrollContent} nestedScrollEnabled showsVerticalScrollIndicator > - + {detail.result ? `${detail.url}\n\n${detail.result}` : detail.url} @@ -356,7 +388,7 @@ export function ToolCallDetailsContent({ @@ -378,7 +410,7 @@ export function ToolCallDetailsContent({ diff --git a/packages/app/src/components/ui/context-menu.tsx b/packages/app/src/components/ui/context-menu.tsx index a799ba9f4..26e9e527c 100644 --- a/packages/app/src/components/ui/context-menu.tsx +++ b/packages/app/src/components/ui/context-menu.tsx @@ -33,6 +33,7 @@ import { Check, CheckCircle } from "lucide-react-native"; import { BottomSheetBackdrop, BottomSheetModal, BottomSheetScrollView } from "@gorhom/bottom-sheet"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { isWeb, isNative } from "@/constants/platform"; +import { useWebScrollbarStyle } from "@/hooks/use-web-scrollbar-style"; // Keep parity with dropdown-menu action statuses. export type ActionStatus = "idle" | "pending" | "success"; @@ -348,6 +349,7 @@ export function ContextMenuContent({ testID?: string; }>): ReactElement | null { const context = useContextMenuContext("ContextMenuContent"); + const webScrollbarStyle = useWebScrollbarStyle(); const isMobile = useIsCompactFormFactor(); const useMobileSheet = isMobile && mobileMode === "sheet"; const { open, setOpen, triggerRef, anchorRect } = context; @@ -537,6 +539,7 @@ export function ContextMenuContent({ {children} diff --git a/packages/app/src/components/ui/dropdown-menu.tsx b/packages/app/src/components/ui/dropdown-menu.tsx index 405d6714b..c11dbce7d 100644 --- a/packages/app/src/components/ui/dropdown-menu.tsx +++ b/packages/app/src/components/ui/dropdown-menu.tsx @@ -28,6 +28,7 @@ import Animated, { Keyframe, runOnJS } from "react-native-reanimated"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { Check, CheckCircle } from "lucide-react-native"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { useWebScrollbarStyle } from "@/hooks/use-web-scrollbar-style"; // Action status for menu items with loading/success feedback export type ActionStatus = "idle" | "pending" | "success"; @@ -267,6 +268,7 @@ export function DropdownMenuContent({ }>): ReactElement | null { const { open, setOpen, triggerRef } = useDropdownMenuContext("DropdownMenuContent"); const [modalVisible, setModalVisible] = useState(false); + const webScrollbarStyle = useWebScrollbarStyle(); const [closing, setClosing] = useState(false); const [triggerRect, setTriggerRect] = useState(null); const [contentSize, setContentSize] = useState<{ width: number; height: number } | null>(null); @@ -411,6 +413,7 @@ export function DropdownMenuContent({ {children} diff --git a/packages/app/src/hooks/use-web-scrollbar-style.d.ts b/packages/app/src/hooks/use-web-scrollbar-style.d.ts new file mode 100644 index 000000000..afafdb944 --- /dev/null +++ b/packages/app/src/hooks/use-web-scrollbar-style.d.ts @@ -0,0 +1 @@ +export * from "./use-web-scrollbar-style.web"; diff --git a/packages/app/src/hooks/use-web-scrollbar-style.native.ts b/packages/app/src/hooks/use-web-scrollbar-style.native.ts new file mode 100644 index 000000000..d2f336ed5 --- /dev/null +++ b/packages/app/src/hooks/use-web-scrollbar-style.native.ts @@ -0,0 +1,5 @@ +import type { ViewStyle } from "react-native"; + +export function useWebScrollbarStyle(): ViewStyle | undefined { + return undefined; +} diff --git a/packages/app/src/hooks/use-web-scrollbar-style.web.ts b/packages/app/src/hooks/use-web-scrollbar-style.web.ts new file mode 100644 index 000000000..38d22c910 --- /dev/null +++ b/packages/app/src/hooks/use-web-scrollbar-style.web.ts @@ -0,0 +1,21 @@ +import { useMemo } from "react"; +import type { ViewStyle } from "react-native"; +import { useUnistyles } from "react-native-unistyles"; + +// CSS scrollbar properties are supported by React Native Web at runtime +// but are not included in React Native's ViewStyle type definition. +interface WebScrollbarStyle extends ViewStyle { + scrollbarColor: string; + scrollbarWidth: string; +} + +export function useWebScrollbarStyle(): WebScrollbarStyle { + const { theme } = useUnistyles(); + return useMemo( + (): WebScrollbarStyle => ({ + scrollbarColor: `${theme.colors.scrollbarHandle} transparent`, + scrollbarWidth: "thin", + }), + [theme.colors.scrollbarHandle], + ); +} diff --git a/packages/app/src/screens/startup-splash-screen.tsx b/packages/app/src/screens/startup-splash-screen.tsx index 975c6a8ac..d3932dc2d 100644 --- a/packages/app/src/screens/startup-splash-screen.tsx +++ b/packages/app/src/screens/startup-splash-screen.tsx @@ -10,6 +10,7 @@ import { Fonts } from "@/constants/theme"; import { getDesktopDaemonLogs, type DesktopDaemonLogs } from "@/desktop/daemon/desktop-daemon"; import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region"; import { isWeb } from "@/constants/platform"; +import { useWebScrollbarStyle } from "@/hooks/use-web-scrollbar-style"; type StartupSplashScreenProps = { bootstrapState?: { @@ -161,6 +162,7 @@ const styles = StyleSheet.create((theme) => ({ export function StartupSplashScreen({ bootstrapState }: StartupSplashScreenProps) { const { theme } = useUnistyles(); + const webScrollbarStyle = useWebScrollbarStyle(); const [daemonLogs, setDaemonLogs] = useState(null); const [logsError, setLogsError] = useState(null); const [isLoadingLogs, setIsLoadingLogs] = useState(false); @@ -282,7 +284,7 @@ export function StartupSplashScreen({ bootstrapState }: StartupSplashScreenProps @@ -303,7 +305,7 @@ export function StartupSplashScreen({ bootstrapState }: StartupSplashScreenProps