Merge branch 'config-work' into dev

This commit is contained in:
Mohamed Boudra
2026-04-18 16:08:57 +07:00
43 changed files with 2672 additions and 2322 deletions
@@ -85,7 +85,6 @@ const styles = StyleSheet.create((theme) => ({
},
title: {
flex: 1,
color: theme.colors.foreground,
fontSize: theme.fontSize.lg,
fontWeight: theme.fontWeight.medium,
},
@@ -251,7 +250,7 @@ export function AdaptiveModalSheet({
>
<View style={styles.bottomSheetHeader} testID={testID}>
<View style={styles.headerTitleGroup}>
<Text style={styles.title} numberOfLines={1}>
<Text style={[styles.title, { color: theme.colors.foreground }]} numberOfLines={1}>
{title}
</Text>
{subtitle}
@@ -280,7 +279,7 @@ export function AdaptiveModalSheet({
<>
<View style={styles.header}>
<View style={styles.headerTitleGroup}>
<Text style={styles.title} numberOfLines={1}>
<Text style={[styles.title, { color: theme.colors.foreground }]} numberOfLines={1}>
{title}
</Text>
{subtitle}
@@ -70,6 +70,7 @@ export function AddHostMethodModal({
<Pressable
style={styles.option}
onPress={handleDirect}
accessibilityRole="button"
accessibilityLabel="Direct connection"
testID="add-host-method-direct"
>
@@ -81,7 +82,12 @@ export function AddHostMethodModal({
</Pressable>
{isNative ? (
<Pressable style={styles.option} onPress={handleScan} accessibilityLabel="Scan QR code">
<Pressable
style={styles.option}
onPress={handleScan}
accessibilityRole="button"
accessibilityLabel="Scan QR code"
>
<QrCode size={18} color={theme.colors.foreground} />
<View style={styles.optionBody}>
<Text style={styles.optionText}>Scan QR code</Text>
@@ -93,6 +99,7 @@ export function AddHostMethodModal({
<Pressable
style={styles.option}
onPress={handlePaste}
accessibilityRole="button"
accessibilityLabel="Paste pairing link"
testID="add-host-method-pair-link"
>
+2 -18
View File
@@ -132,7 +132,6 @@ function buildConnectionFailureCopy(
export interface AddHostModalProps {
visible: boolean;
onClose: () => void;
targetServerId?: string;
onCancel?: () => void;
onSaved?: (result: {
profile: HostProfile;
@@ -142,13 +141,7 @@ export interface AddHostModalProps {
}) => void;
}
export function AddHostModal({
visible,
onClose,
onCancel,
onSaved,
targetServerId,
}: AddHostModalProps) {
export function AddHostModal({ visible, onClose, onCancel, onSaved }: AddHostModalProps) {
const { theme } = useUnistyles();
const daemons = useHosts();
const { upsertDirectConnection } = useHostMutations();
@@ -211,15 +204,6 @@ export function AddHostModal({
endpoint,
});
await client.close().catch(() => undefined);
if (targetServerId && serverId !== targetServerId) {
const message = `That endpoint belongs to ${serverId}, not ${targetServerId}.`;
setErrorMessage(message);
if (!isMobile) {
Alert.alert("Wrong daemon", message);
}
return;
}
const isNewHost = !daemons.some((daemon) => daemon.serverId === serverId);
const profile = await upsertDirectConnection({
serverId,
@@ -245,7 +229,7 @@ export function AddHostModal({
} finally {
setIsSaving(false);
}
}, [daemons, handleClose, isMobile, isSaving, onSaved, targetServerId, upsertDirectConnection]);
}, [daemons, handleClose, isMobile, isSaving, onSaved, upsertDirectConnection]);
return (
<AdaptiveModalSheet
@@ -1,5 +1,5 @@
import { useRef } from "react";
import { Pressable, Text, View } from "react-native";
import { Pressable, View } from "react-native";
import { useQueryClient } from "@tanstack/react-query";
import { ChevronDown, GitBranch } from "lucide-react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
@@ -8,6 +8,7 @@ import { useIsCompactFormFactor } from "@/constants/layout";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import { useToast } from "@/contexts/toast-context";
import { useBranchSwitcher } from "@/hooks/use-branch-switcher";
import { ScreenTitle } from "@/components/headers/screen-title";
interface BranchSwitcherProps {
currentBranchName: string | null;
@@ -46,9 +47,7 @@ export function BranchSwitcher({
const titleContent = (
<>
{isGitCheckout ? <GitBranch size={14} color={theme.colors.foregroundMuted} /> : null}
<Text testID="workspace-header-title" style={styles.headerTitle} numberOfLines={1}>
{title}
</Text>
<ScreenTitle testID="workspace-header-title">{title}</ScreenTitle>
</>
);
@@ -102,15 +101,6 @@ export function BranchSwitcher({
}
const styles = StyleSheet.create((theme) => ({
headerTitle: {
fontSize: theme.fontSize.base,
fontWeight: {
xs: "400",
md: "300",
},
color: theme.colors.foreground,
flexShrink: 1,
},
branchSwitcherTrigger: {
flexDirection: "row",
alignItems: "center",
@@ -1,31 +1,35 @@
import type { ReactNode } from "react";
import { Pressable, Text } from "react-native";
import { Pressable } from "react-native";
import { router } from "expo-router";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { ArrowLeft } from "lucide-react-native";
import { ScreenHeader } from "./screen-header";
import { ScreenTitle } from "./screen-title";
interface BackHeaderProps {
title?: string;
titleAccessory?: ReactNode;
rightContent?: ReactNode;
onBack?: () => void;
}
export function BackHeader({ title, rightContent, onBack }: BackHeaderProps) {
export function BackHeader({ title, titleAccessory, rightContent, onBack }: BackHeaderProps) {
const { theme } = useUnistyles();
return (
<ScreenHeader
left={
<>
<Pressable onPress={onBack ?? (() => router.back())} style={styles.backButton}>
<Pressable
onPress={onBack ?? (() => router.back())}
style={styles.backButton}
accessibilityRole="button"
accessibilityLabel="Back"
>
<ArrowLeft size={theme.iconSize.lg} color={theme.colors.foregroundMuted} />
</Pressable>
{title && (
<Text style={styles.title} numberOfLines={1}>
{title}
</Text>
)}
{title && <ScreenTitle>{title}</ScreenTitle>}
{titleAccessory}
</>
}
right={rightContent}
@@ -45,13 +49,4 @@ const styles = StyleSheet.create((theme) => ({
},
borderRadius: theme.borderRadius.lg,
},
title: {
flex: 1,
fontSize: theme.fontSize.lg,
fontWeight: {
xs: theme.fontWeight.semibold,
md: "400",
},
color: theme.colors.foreground,
},
}));
@@ -0,0 +1,13 @@
import type { ReactNode } from "react";
import { View } from "react-native";
import { headerIconSlotStyle } from "./header-toggle-button";
/**
* Non-interactive icon slot sitting at the start of a screen header's left
* cluster. Shares the same padding + border-radius as `HeaderToggleButton` so
* decorative headers (settings sections, host detail) line up with the sidebar
* toggle across screens.
*/
export function HeaderIconBadge({ children }: { children: ReactNode }) {
return <View style={headerIconSlotStyle.slot}>{children}</View>;
}
@@ -51,7 +51,7 @@ export function HeaderToggleButton({
onPress={(e) => {
onPress(e);
}}
style={[styles.button, style]}
style={[headerIconSlotStyle.slot, style]}
>
{typeof children === "function"
? (state: { pressed: boolean; hovered?: boolean }) =>
@@ -68,14 +68,17 @@ export function HeaderToggleButton({
);
}
const styles = StyleSheet.create((theme) => ({
button: {
export const headerIconSlotStyle = StyleSheet.create((theme) => ({
slot: {
padding: {
xs: theme.spacing[3],
md: theme.spacing[2],
},
borderRadius: theme.borderRadius.lg,
},
}));
const styles = StyleSheet.create((theme) => ({
tooltipRow: {
flexDirection: "row",
alignItems: "center",
@@ -1,8 +1,9 @@
import type { ReactNode } from "react";
import { Text, View, type StyleProp, type ViewStyle } from "react-native";
import { View, type StyleProp, type ViewStyle } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { PanelLeft } from "lucide-react-native";
import { ScreenHeader } from "./screen-header";
import { ScreenTitle } from "./screen-title";
import { HeaderToggleButton } from "./header-toggle-button";
import { usePanelStore } from "@/stores/panel-store";
import { useIsCompactFormFactor } from "@/constants/layout";
@@ -83,11 +84,7 @@ export function MenuHeader({ title, rightContent, borderless }: MenuHeaderProps)
left={
<>
<SidebarMenuToggle />
{title && (
<Text style={styles.title} numberOfLines={1}>
{title}
</Text>
)}
{title && <ScreenTitle>{title}</ScreenTitle>}
</>
}
right={rightContent}
@@ -101,15 +98,6 @@ const styles = StyleSheet.create((theme) => ({
left: {
gap: theme.spacing[2],
},
title: {
flex: 1,
fontSize: theme.fontSize.base,
fontWeight: {
xs: "400",
md: "300",
},
color: theme.colors.foreground,
},
mobileMenuIcon: {
width: MOBILE_MENU_LINE_WIDTH,
height: 12,
@@ -0,0 +1,35 @@
import type { ReactNode } from "react";
import { Text, type StyleProp, type TextStyle } from "react-native";
import { StyleSheet } from "react-native-unistyles";
interface ScreenTitleProps {
children: ReactNode;
numberOfLines?: number;
testID?: string;
style?: StyleProp<TextStyle>;
}
/**
* Canonical screen title for use inside `ScreenHeader`. One typography, one
* color, responsive weight. Leading icons are siblings (HeaderToggleButton,
* HeaderIconBadge) — never nested inside this component.
*/
export function ScreenTitle({ children, numberOfLines = 1, testID, style }: ScreenTitleProps) {
return (
<Text style={[styles.text, style]} numberOfLines={numberOfLines} testID={testID}>
{children}
</Text>
);
}
const styles = StyleSheet.create((theme) => ({
text: {
flexShrink: 1,
fontSize: theme.fontSize.base,
fontWeight: {
xs: "400",
md: "300",
},
color: theme.colors.foreground,
},
}));
+25 -104
View File
@@ -33,6 +33,7 @@ import { Shortcut } from "@/components/ui/shortcut";
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
import { router, usePathname } from "expo-router";
import { usePanelStore, MIN_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH } from "@/stores/panel-store";
import { SidebarHeaderRow } from "@/components/sidebar/sidebar-header-row";
import { SidebarWorkspaceList } from "./sidebar-workspace-list";
import { SidebarAgentListSkeleton } from "./sidebar-agent-list-skeleton";
import { useSidebarShortcutModel } from "@/hooks/use-sidebar-shortcut-model";
@@ -46,14 +47,10 @@ import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox";
import { useHostRuntimeSnapshot, useHosts } from "@/runtime/host-runtime";
import { formatConnectionStatus } from "@/utils/daemons";
import {
HEADER_INNER_HEIGHT,
HEADER_INNER_HEIGHT_MOBILE,
useIsCompactFormFactor,
} from "@/constants/layout";
import { useIsCompactFormFactor } from "@/constants/layout";
import {
buildHostSessionsRoute,
buildHostSettingsRoute,
buildSettingsRoute,
mapPathnameToServer,
parseServerIdFromPathname,
} from "@/utils/host-routes";
@@ -220,19 +217,13 @@ export const LeftSidebar = memo(function LeftSidebar({
}, [openProjectPicker]);
const handleSettingsMobile = useCallback(() => {
if (!activeServerId) {
return;
}
closeToAgent();
router.push(buildHostSettingsRoute(activeServerId));
}, [activeServerId, closeToAgent]);
router.push(buildSettingsRoute());
}, [closeToAgent]);
const handleSettingsDesktop = useCallback(() => {
if (!activeServerId) {
return;
}
router.push(buildHostSettingsRoute(activeServerId));
}, [activeServerId]);
router.push(buildSettingsRoute());
}, []);
const handleViewMoreNavigate = useCallback(() => {
if (!activeServerId) {
@@ -328,44 +319,6 @@ function HostSwitchOption({
);
}
function SessionsButton({ onPress }: { onPress: () => void }) {
const { theme } = useUnistyles();
const pathname = usePathname();
const isActive = pathname.includes("/sessions");
return (
<Pressable
style={({ hovered }) => [
styles.newAgentButton,
hovered && styles.newAgentButtonHovered,
isActive && styles.newAgentButtonActive,
]}
testID="sidebar-sessions"
accessible
accessibilityRole="button"
accessibilityLabel="Sessions"
onPress={onPress}
>
{({ hovered }) => (
<>
<MessagesSquare
size={theme.iconSize.md}
color={hovered || isActive ? theme.colors.foreground : theme.colors.foregroundMuted}
/>
<Text
style={[
styles.newAgentButtonText,
(hovered || isActive) && styles.newAgentButtonTextHovered,
]}
>
Sessions
</Text>
</>
)}
</Pressable>
);
}
function MobileSidebar({
theme,
activeServerId,
@@ -394,6 +347,8 @@ function MobileSidebar({
handleViewMoreNavigate,
}: MobileSidebarProps) {
const newAgentKeys = useShortcutKeys("new-agent");
const pathname = usePathname();
const isSessionsActive = pathname.includes("/sessions");
const {
translateX,
backdropOpacity,
@@ -544,11 +499,13 @@ function MobileSidebar({
pointerEvents="auto"
>
<View style={styles.sidebarContent} pointerEvents="auto">
<View style={styles.sidebarHeader}>
<View style={styles.sidebarHeaderRow}>
<SessionsButton onPress={handleViewMore} />
</View>
</View>
<SidebarHeaderRow
icon={MessagesSquare}
label="Sessions"
onPress={handleViewMore}
isActive={isSessionsActive}
testID="sidebar-sessions"
/>
{isInitialLoad ? (
<SidebarAgentListSkeleton />
@@ -676,6 +633,8 @@ function DesktopSidebar({
handleViewMore,
}: DesktopSidebarProps) {
const newAgentKeys = useShortcutKeys("new-agent");
const pathname = usePathname();
const isSessionsActive = pathname.includes("/sessions");
const padding = useWindowControlsPadding("sidebar");
const sidebarWidth = usePanelStore((state) => state.sidebarWidth);
const setSidebarWidth = usePanelStore((state) => state.setSidebarWidth);
@@ -732,11 +691,13 @@ function DesktopSidebar({
<View style={styles.sidebarDragArea}>
<TitlebarDragRegion />
{padding.top > 0 ? <View style={{ height: padding.top }} /> : null}
<View style={styles.sidebarHeader}>
<View style={styles.sidebarHeaderRow}>
<SessionsButton onPress={handleViewMore} />
</View>
</View>
<SidebarHeaderRow
icon={MessagesSquare}
label="Sessions"
onPress={handleViewMore}
isActive={isSessionsActive}
testID="sidebar-sessions"
/>
</View>
{isInitialLoad ? (
@@ -881,46 +842,6 @@ const styles = StyleSheet.create((theme) => ({
sidebarDragArea: {
position: "relative",
},
sidebarHeader: {
height: {
xs: HEADER_INNER_HEIGHT_MOBILE,
md: HEADER_INNER_HEIGHT,
},
paddingHorizontal: theme.spacing[2],
justifyContent: "center",
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
userSelect: "none",
},
sidebarHeaderRow: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
gap: theme.spacing[2],
},
newAgentButton: {
flex: 1,
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
paddingVertical: theme.spacing[2],
paddingHorizontal: theme.spacing[3],
borderRadius: theme.borderRadius.lg,
},
newAgentButtonText: {
fontSize: theme.fontSize.base,
fontWeight: theme.fontWeight.normal,
color: theme.colors.foregroundMuted,
},
newAgentButtonTextHovered: {
color: theme.colors.foreground,
},
newAgentButtonHovered: {
backgroundColor: theme.colors.surfaceSidebarHover,
},
newAgentButtonActive: {
backgroundColor: theme.colors.surfaceSidebarHover,
},
hostTrigger: {
flexDirection: "row",
alignItems: "center",
@@ -47,7 +47,6 @@ const styles = StyleSheet.create((theme) => ({
export interface PairLinkModalProps {
visible: boolean;
onClose: () => void;
targetServerId?: string;
onCancel?: () => void;
onSaved?: (result: {
profile: HostProfile;
@@ -57,13 +56,7 @@ export interface PairLinkModalProps {
}) => void;
}
export function PairLinkModal({
visible,
onClose,
onCancel,
onSaved,
targetServerId,
}: PairLinkModalProps) {
export function PairLinkModal({ visible, onClose, onCancel, onSaved }: PairLinkModalProps) {
const { theme } = useUnistyles();
const daemons = useHosts();
const { upsertConnectionFromOfferUrl: upsertDaemonFromOfferUrl } = useHostMutations();
@@ -128,15 +121,6 @@ export function PairLinkModal({
return;
}
if (targetServerId && parsedOffer.serverId !== targetServerId) {
const message = `That pairing link belongs to ${parsedOffer.serverId}, not ${targetServerId}.`;
setErrorMessage(message);
if (!isMobile) {
Alert.alert("Wrong daemon", message);
}
return;
}
try {
setIsSaving(true);
setErrorMessage("");
@@ -165,7 +149,7 @@ export function PairLinkModal({
} finally {
setIsSaving(false);
}
}, [daemons, handleClose, isMobile, isSaving, onSaved, targetServerId, upsertDaemonFromOfferUrl]);
}, [daemons, handleClose, isMobile, isSaving, onSaved, upsertDaemonFromOfferUrl]);
return (
<AdaptiveModalSheet
@@ -0,0 +1,90 @@
import { Pressable, Text, View } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import type { LucideIcon } from "lucide-react-native";
import { HEADER_INNER_HEIGHT, HEADER_INNER_HEIGHT_MOBILE } from "@/constants/layout";
interface SidebarHeaderRowProps {
icon: LucideIcon;
label: string;
onPress: () => void;
isActive?: boolean;
testID?: string;
nativeID?: string;
accessibilityLabel?: string;
}
/**
* Top-of-sidebar header row: a sidebar-height pressable with an icon + label
* and a full-width border separator beneath. Used as the first element of a
* sidebar (workspace "Sessions", settings "Back to workspace"). Owns its own
* separator line so both sidebars converge on the same edge and padding.
*/
export function SidebarHeaderRow({
icon: Icon,
label,
onPress,
isActive = false,
testID,
nativeID,
accessibilityLabel,
}: SidebarHeaderRowProps) {
const { theme } = useUnistyles();
return (
<View style={styles.container}>
<Pressable
onPress={onPress}
testID={testID}
nativeID={nativeID}
accessible
accessibilityRole="button"
accessibilityLabel={accessibilityLabel ?? label}
style={({ hovered }) => [styles.button, (hovered || isActive) && styles.buttonHovered]}
>
{({ hovered }) => {
const isHighlighted = hovered || isActive;
const iconColor = isHighlighted ? theme.colors.foreground : theme.colors.foregroundMuted;
return (
<>
<Icon size={theme.iconSize.md} color={iconColor} />
<Text style={[styles.label, isHighlighted && styles.labelHighlighted]}>{label}</Text>
</>
);
}}
</Pressable>
</View>
);
}
const styles = StyleSheet.create((theme) => ({
container: {
height: {
xs: HEADER_INNER_HEIGHT_MOBILE,
md: HEADER_INNER_HEIGHT,
},
paddingHorizontal: theme.spacing[2],
justifyContent: "center",
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
userSelect: "none",
},
button: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
paddingVertical: theme.spacing[2],
paddingHorizontal: theme.spacing[3],
borderRadius: theme.borderRadius.lg,
},
buttonHovered: {
backgroundColor: theme.colors.surfaceSidebarHover,
},
label: {
fontSize: theme.fontSize.base,
fontWeight: theme.fontWeight.normal,
color: theme.colors.foregroundMuted,
},
labelHighlighted: {
color: theme.colors.foreground,
},
}));
@@ -0,0 +1,18 @@
import { View } from "react-native";
import { StyleSheet } from "react-native-unistyles";
/**
* Edge-to-edge separator line for sidebars. Must render outside any horizontally
* padded container so the line runs the full sidebar width — matching the
* separator beneath the workspace sidebar's "New agent" header.
*/
export function SidebarSeparator() {
return <View style={styles.line} />;
}
const styles = StyleSheet.create((theme) => ({
line: {
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
},
}));
+1 -1
View File
@@ -181,7 +181,7 @@ export function Button({
]}
>
{renderIcon()}
<Text style={resolvedTextStyle}>{children}</Text>
{children != null ? <Text style={resolvedTextStyle}>{children}</Text> : null}
</Pressable>
);
}
+53 -39
View File
@@ -1,8 +1,8 @@
import { useCallback, useEffect, useState, useSyncExternalStore } from "react";
import { Pressable, Text, View, ScrollView } from "react-native";
import { useRouter } from "expo-router";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { QrCode, Link2, ClipboardPaste, ExternalLink } from "lucide-react-native";
import { StyleSheet, useUnistyles, withUnistyles } from "react-native-unistyles";
import { QrCode, Link2, ClipboardPaste, ExternalLink, Settings } from "lucide-react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import type { HostProfile } from "@/types/host-connection";
import {
@@ -13,6 +13,7 @@ import {
} from "@/runtime/host-runtime";
import { AddHostModal } from "./add-host-modal";
import { PairLinkModal } from "./pair-link-modal";
import { Button } from "@/components/ui/button";
import { resolveAppVersion } from "@/utils/app-version";
import { formatVersionWithPrefix } from "@/desktop/updates/desktop-updates";
import { buildHostRootRoute } from "@/utils/host-routes";
@@ -20,6 +21,8 @@ import { PaseoLogo } from "@/components/icons/paseo-logo";
import { openExternalUrl } from "@/utils/open-external-url";
import { isWeb, isNative } from "@/constants/platform";
const ThemedScrollView = withUnistyles(ScrollView);
type WelcomeAction = {
key: "scan-qr" | "direct-connection" | "paste-pairing-link";
label: string;
@@ -30,6 +33,10 @@ type WelcomeAction = {
};
const styles = StyleSheet.create((theme) => ({
scrollView: {
flex: 1,
backgroundColor: theme.colors.surface0,
},
container: {
flexGrow: 1,
backgroundColor: theme.colors.surface0,
@@ -47,14 +54,17 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foreground,
fontSize: theme.fontSize.xl,
fontWeight: theme.fontWeight.medium,
marginBottom: theme.spacing[3],
textAlign: "center",
},
subtitle: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.base,
fontSize: theme.fontSize.sm,
textAlign: "center",
marginBottom: theme.spacing[8],
},
copyBlock: {
alignItems: "center",
gap: theme.spacing[2],
marginBottom: theme.spacing[12],
},
actions: {
width: "100%",
@@ -117,19 +127,11 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.destructive,
fontSize: theme.fontSize.sm,
},
setupHint: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.sm,
textAlign: "center",
marginBottom: theme.spacing[6],
lineHeight: theme.fontSize.sm * 1.5,
},
setupLink: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: 6,
marginBottom: theme.spacing[6],
},
setupLinkText: {
color: theme.colors.accent,
@@ -142,6 +144,10 @@ const styles = StyleSheet.create((theme) => ({
textAlign: "center",
marginTop: theme.spacing[6],
},
settingsButton: {
alignSelf: "center",
marginTop: theme.spacing[6],
},
}));
function useAnyHostOnline(serverIds: string[]): string | null {
@@ -243,13 +249,7 @@ export function WelcomeScreen({ onHostAdded }: WelcomeScreenProps) {
const anyOnlineServerId = useAnyHostOnline(hosts.map((h) => h.serverId));
useEffect(() => {
const currentPathname = typeof window === "undefined" ? null : window.location.pathname || null;
if (currentPathname && currentPathname !== "/welcome") {
return;
}
if (!anyOnlineServerId) {
return;
}
if (!anyOnlineServerId) return;
router.replace(buildHostRootRoute(anyOnlineServerId));
}, [anyOnlineServerId, router]);
@@ -309,8 +309,8 @@ export function WelcomeScreen({ onHostAdded }: WelcomeScreenProps) {
const showHostList = hosts.length > 0 && !anyOnlineServerId;
return (
<ScrollView
style={{ flex: 1, backgroundColor: theme.colors.surface0 }}
<ThemedScrollView
style={styles.scrollView}
contentContainerStyle={[
styles.container,
{ paddingBottom: theme.spacing[6] + insets.bottom },
@@ -320,22 +320,25 @@ export function WelcomeScreen({ onHostAdded }: WelcomeScreenProps) {
>
<View style={styles.content}>
<PaseoLogo size={96} />
<Text style={styles.title}>Welcome to Paseo</Text>
<Text style={styles.subtitle}>
{showHostList ? "Connecting to your hosts…" : "Connect to your host to start"}
</Text>
{!showHostList && isNative && (
<>
<Text style={styles.setupHint}>
You need the Paseo desktop app or server running on your computer first.
</Text>
<Pressable style={styles.setupLink} onPress={() => openExternalUrl("https://paseo.sh")}>
<Text style={styles.setupLinkText}>Get started at paseo.sh</Text>
<ExternalLink size={14} color={theme.colors.accent} />
</Pressable>
</>
)}
<View style={styles.copyBlock}>
<Text style={styles.title}>Welcome to Paseo</Text>
{showHostList ? (
<Text style={styles.subtitle}>Connecting to your hosts…</Text>
) : (
<>
<Text style={styles.subtitle}>Connect your computer to get started</Text>
{isNative ? (
<Pressable
style={styles.setupLink}
onPress={() => openExternalUrl("https://paseo.sh")}
>
<Text style={styles.setupLinkText}>paseo.sh</Text>
<ExternalLink size={14} color={theme.colors.accent} />
</Pressable>
) : null}
</>
)}
</View>
<View style={styles.actions}>
{actions.map((action) => {
@@ -366,6 +369,17 @@ export function WelcomeScreen({ onHostAdded }: WelcomeScreenProps) {
))}
</View>
)}
<Button
variant="ghost"
size="sm"
leftIcon={Settings}
onPress={() => router.push("/settings")}
style={styles.settingsButton}
testID="welcome-open-settings"
>
Settings
</Button>
</View>
<Text style={styles.versionLabel}>{appVersionText}</Text>
@@ -386,6 +400,6 @@ export function WelcomeScreen({ onHostAdded }: WelcomeScreenProps) {
finishOnboarding(serverId);
}}
/>
</ScrollView>
</ThemedScrollView>
);
}