feat: add multi-platform downloads and improve homepage animations

This commit is contained in:
Mohamed Boudra
2026-03-10 11:20:17 +07:00
parent a3e271a1e7
commit cc45c3772f
11 changed files with 661 additions and 152 deletions
@@ -597,7 +597,7 @@ export function AgentInputArea({
const isVoiceModeForAgent = voice?.isVoiceModeForAgent(serverId, agentId) ?? false
const handleToggleRealtimeVoice = useCallback(() => {
if (!voice || !isConnected) {
if (!voice || !isConnected || !agent) {
return
}
if (voice.isVoiceSwitching) {
@@ -614,7 +614,7 @@ export function AgentInputArea({
toast.error(message)
}
})
}, [agentId, isConnected, serverId, toast, voice])
}, [agent, agentId, isConnected, serverId, toast, voice])
function handleEditQueuedMessage(id: string) {
const item = queuedMessages.find((q) => q.id === id)
@@ -704,7 +704,7 @@ export function AgentInputArea({
const rightContent = (
<View style={styles.rightControls}>
{!isVoiceModeForAgent ? (
{!isVoiceModeForAgent && agent ? (
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
<TooltipTrigger
onPress={handleToggleRealtimeVoice}
+11 -3
View File
@@ -27,6 +27,7 @@ interface AgentListProps {
selectedAgentId?: string
onAgentSelect?: () => void
listFooterComponent?: ReactElement | null
showAttentionIndicator?: boolean
}
interface AgentListSection {
@@ -112,12 +113,14 @@ function SessionRow({
agent,
isMobile,
selectedAgentId,
showAttentionIndicator,
onPress,
onLongPress,
}: {
agent: AggregatedAgent
isMobile: boolean
selectedAgentId?: string
showAttentionIndicator: boolean
onPress: (agent: AggregatedAgent) => void
onLongPress: (agent: AggregatedAgent) => void
}) {
@@ -154,7 +157,7 @@ function SessionRow({
{(agent.pendingPermissionCount ?? 0) > 0 ? (
<SessionBadge label={`${agent.pendingPermissionCount} pending`} tone="warning" />
) : null}
{!isMobile && agent.requiresAttention ? (
{!isMobile && showAttentionIndicator && agent.requiresAttention ? (
<SessionBadge label="Attention" tone="danger" />
) : null}
</View>
@@ -187,7 +190,7 @@ function SessionRow({
<Text style={styles.columnMetaFixed}>{timeAgo}</Text>
</>
)}
{isMobile && agent.requiresAttention ? (
{isMobile && showAttentionIndicator && agent.requiresAttention ? (
<View style={styles.rowTrailing}>
<SessionBadge label="Attention" tone="danger" />
</View>
@@ -200,12 +203,14 @@ function SessionTableSection({
section,
isMobile,
selectedAgentId,
showAttentionIndicator,
onAgentPress,
onAgentLongPress,
}: {
section: AgentListSection
isMobile: boolean
selectedAgentId?: string
showAttentionIndicator: boolean
onAgentPress: (agent: AggregatedAgent) => void
onAgentLongPress: (agent: AggregatedAgent) => void
}) {
@@ -225,6 +230,7 @@ function SessionTableSection({
agent={agent}
isMobile={isMobile}
selectedAgentId={selectedAgentId}
showAttentionIndicator={showAttentionIndicator}
onPress={onAgentPress}
onLongPress={onAgentLongPress}
/>
@@ -242,6 +248,7 @@ export function AgentList({
selectedAgentId,
onAgentSelect,
listFooterComponent,
showAttentionIndicator = true,
}: AgentListProps) {
const { theme } = useUnistyles()
const pathname = usePathname()
@@ -325,11 +332,12 @@ export function AgentList({
section={section}
isMobile={isMobile}
selectedAgentId={selectedAgentId}
showAttentionIndicator={showAttentionIndicator}
onAgentPress={handleAgentPress}
onAgentLongPress={handleAgentLongPress}
/>
),
[handleAgentLongPress, handleAgentPress, isMobile, selectedAgentId]
[handleAgentLongPress, handleAgentPress, isMobile, selectedAgentId, showAttentionIndicator]
)
const keyExtractor = useCallback((section: AgentListSection) => section.key, [])
@@ -24,7 +24,7 @@ import { router, usePathname } from 'expo-router'
import { StyleSheet, UnistylesRuntime, useUnistyles } from 'react-native-unistyles'
import { type GestureType } from 'react-native-gesture-handler'
import * as Clipboard from 'expo-clipboard'
import { ChevronDown, ChevronRight, MoreVertical, Plus } from 'lucide-react-native'
import { Archive, ChevronDown, ChevronRight, Copy, MoreVertical, Plus } from 'lucide-react-native'
import { NestableScrollContainer } from 'react-native-draggable-flatlist'
import { DraggableList, type DraggableRenderItemInfo } from './draggable-list'
import type { DraggableListDragHandleProps } from './draggable-list.types'
@@ -43,7 +43,6 @@ import {
} from '@/hooks/use-sidebar-workspaces-list'
import { useSidebarOrderStore } from '@/stores/sidebar-order-store'
import { useKeyboardShortcutsStore } from '@/stores/keyboard-shortcuts-store'
import { formatTimeAgo } from '@/utils/time'
import {
ContextMenu,
ContextMenuContent,
@@ -126,12 +125,6 @@ interface WorkspaceRowInnerProps {
onCopyPath?: () => void
}
function resolveWorkspaceCreatedAtLabel(workspace: SidebarWorkspaceEntry): string | null {
if (!workspace.activityAt) {
return null
}
return formatTimeAgo(workspace.activityAt)
}
function resolveStatusDotColor(input: {
theme: ReturnType<typeof useUnistyles>['theme']
@@ -620,7 +613,8 @@ function WorkspaceRowInner({
onCopyPath,
}: WorkspaceRowInnerProps) {
const { theme } = useUnistyles()
const createdAtLabel = resolveWorkspaceCreatedAtLabel(workspace)
const [isHovered, setIsHovered] = useState(false)
const isMobile = Platform.OS !== 'web'
const interaction = useLongPressDragInteraction({
drag,
menuController,
@@ -636,14 +630,18 @@ function WorkspaceRowInner({
}, [interaction.didLongPressRef, onPress])
return (
<View style={styles.workspaceRowContainer}>
<View
style={styles.workspaceRowContainer}
onPointerEnter={() => setIsHovered(true)}
onPointerLeave={() => setIsHovered(false)}
>
<Pressable
disabled={isArchiving}
style={({ pressed, hovered = false }) => [
style={({ pressed }) => [
styles.workspaceRow,
isDragging && styles.workspaceRowDragging,
selected && styles.sidebarRowSelected,
hovered && styles.workspaceRowHovered,
isHovered && styles.workspaceRowHovered,
pressed && styles.workspaceRowPressed,
]}
onPressIn={interaction.handlePressIn}
@@ -659,41 +657,43 @@ function WorkspaceRowInner({
style={styles.workspaceRowLeft}
>
<WorkspaceStatusIndicator bucket={workspace.statusBucket} loading={isArchiving} />
<Text style={styles.workspaceBranchText} numberOfLines={1}>
<Text
style={[styles.workspaceBranchText, isHovered && styles.workspaceBranchTextHovered]}
numberOfLines={1}
>
{workspace.name}
</Text>
</View>
<View style={styles.workspaceRowRight}>
{workspace.diffStat ? (
<View style={styles.diffStatRow}>
<Text style={styles.diffStatAdditions}>+{workspace.diffStat.additions}</Text>
<Text style={styles.diffStatDeletions}>-{workspace.diffStat.deletions}</Text>
</View>
) : createdAtLabel ? (
<Text style={styles.workspaceCreatedAtText} numberOfLines={1}>
{createdAtLabel}
</Text>
) : null}
{showShortcutBadge && shortcutNumber !== null ? (
<View style={styles.shortcutBadge}>
<Text style={styles.shortcutBadgeText}>{shortcutNumber}</Text>
</View>
) : null}
{onArchive ? (
{onArchive && (isHovered || isMobile) ? (
<DropdownMenu>
<DropdownMenuTrigger
hitSlop={8}
style={styles.kebabButton}
style={({ hovered = false }) => [
styles.kebabButton,
hovered && styles.kebabButtonHovered,
]}
accessibilityRole="button"
accessibilityLabel="Workspace actions"
testID={`sidebar-workspace-kebab-${workspace.workspaceKey}`}
>
<MoreVertical size={14} color={theme.colors.foregroundMuted} />
{({ hovered }) => (
<MoreVertical
size={14}
color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted}
/>
)}
</DropdownMenuTrigger>
<DropdownMenuContent align="end" width={200}>
{onCopyPath ? (
<DropdownMenuItem
testID={`sidebar-workspace-menu-copy-path-${workspace.workspaceKey}`}
leading={<Copy size={14} color={theme.colors.foregroundMuted} />}
onSelect={onCopyPath}
>
Copy path
@@ -701,15 +701,20 @@ function WorkspaceRowInner({
) : null}
<DropdownMenuItem
testID={`sidebar-workspace-menu-archive-${workspace.workspaceKey}`}
leading={<Archive size={14} color={theme.colors.foregroundMuted} />}
status={archiveStatus}
pendingLabel={archivePendingLabel}
destructive
onSelect={onArchive}
>
{archiveLabel ?? 'Archive'}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : workspace.diffStat ? (
<View style={styles.diffStatRow}>
<Text style={styles.diffStatAdditions}>+{workspace.diffStat.additions}</Text>
<Text style={styles.diffStatDeletions}>-{workspace.diffStat.deletions}</Text>
</View>
) : null}
</View>
</Pressable>
@@ -1726,10 +1731,8 @@ const styles = StyleSheet.create((theme) => ({
flex: 1,
minWidth: 0,
},
workspaceCreatedAtText: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
flexShrink: 0,
workspaceBranchTextHovered: {
opacity: 1,
},
diffStatRow: {
flexDirection: 'row',
@@ -1752,6 +1755,9 @@ const styles = StyleSheet.create((theme) => ({
borderRadius: 4,
marginLeft: 2,
},
kebabButtonHovered: {
backgroundColor: theme.colors.surface2,
},
shortcutBadge: {
minWidth: 18,
height: 18,
@@ -8,6 +8,7 @@ import {
useState,
type PropsWithChildren,
type ReactElement,
type ReactNode,
} from "react";
import {
ActivityIndicator,
@@ -184,8 +185,9 @@ type TriggerStyleProp =
| StyleProp<ViewStyle>
| ((state: TriggerState) => StyleProp<ViewStyle>);
interface DropdownMenuTriggerProps extends Omit<PressableProps, "style"> {
interface DropdownMenuTriggerProps extends Omit<PressableProps, "style" | "children"> {
style?: TriggerStyleProp;
children: ReactNode | ((state: TriggerState) => ReactNode);
}
export function DropdownMenuTrigger({
@@ -193,7 +195,7 @@ export function DropdownMenuTrigger({
disabled,
style,
...props
}: PropsWithChildren<DropdownMenuTriggerProps>): ReactElement {
}: DropdownMenuTriggerProps): ReactElement {
const ctx = useDropdownMenuContext("DropdownMenuTrigger");
const handlePress = useCallback(() => {
@@ -215,7 +217,10 @@ export function DropdownMenuTrigger({
return style;
}}
>
{children}
{({ pressed, hovered = false }) => {
const state: TriggerState = { pressed, hovered: Boolean(hovered), open: ctx.open };
return typeof children === "function" ? children(state) : children;
}}
</Pressable>
);
}
@@ -52,6 +52,7 @@ export function AgentsScreen({ serverId }: { serverId: string }) {
showCheckoutInfo={false}
isRefreshing={isManualRefresh && isRevalidating}
onRefresh={handleRefresh}
showAttentionIndicator={false}
/>
</View>
);