Polish sidebar drag snapback and workspace rows

This commit is contained in:
Mohamed Boudra
2026-03-01 20:45:09 +07:00
parent e1067c623b
commit ac1106b259
2 changed files with 37 additions and 14 deletions
@@ -346,6 +346,7 @@ export function LeftSidebar({ selectedAgentId }: LeftSidebarProps) {
projects={projects} projects={projects}
isRefreshing={isManualRefresh && isRevalidating} isRefreshing={isManualRefresh && isRevalidating}
onRefresh={handleRefresh} onRefresh={handleRefresh}
onWorkspacePress={closeToAgent}
parentGestureRef={closeGestureRef} parentGestureRef={closeGestureRef}
/> />
)} )}
@@ -51,6 +51,7 @@ interface SidebarAgentListProps {
serverId: string | null serverId: string | null
isRefreshing?: boolean isRefreshing?: boolean
onRefresh?: () => void onRefresh?: () => void
onWorkspacePress?: () => void
listFooterComponent?: ReactElement | null listFooterComponent?: ReactElement | null
/** Gesture ref for coordinating with parent gestures (e.g., sidebar close) */ /** Gesture ref for coordinating with parent gestures (e.g., sidebar close) */
parentGestureRef?: MutableRefObject<GestureType | undefined> parentGestureRef?: MutableRefObject<GestureType | undefined>
@@ -67,6 +68,7 @@ interface ProjectRowProps {
interface WorkspaceRowProps { interface WorkspaceRowProps {
workspace: SidebarWorkspaceEntry workspace: SidebarWorkspaceEntry
compact?: boolean
onPress: () => void onPress: () => void
onLongPress: () => void onLongPress: () => void
} }
@@ -107,9 +109,9 @@ function resolveWorkspaceBranchLabel(workspace: SidebarWorkspaceEntry): string {
return 'Unknown branch' return 'Unknown branch'
} }
function resolveWorkspaceCreatedAtLabel(workspace: SidebarWorkspaceEntry): string { function resolveWorkspaceCreatedAtLabel(workspace: SidebarWorkspaceEntry): string | null {
if (!workspace.createdAt) { if (!workspace.createdAt) {
return 'No agents yet' return null
} }
return formatTimeAgo(workspace.createdAt) return formatTimeAgo(workspace.createdAt)
} }
@@ -195,8 +197,9 @@ function ProjectRow({
) )
} }
function WorkspaceRow({ workspace, onPress, onLongPress }: WorkspaceRowProps) { function WorkspaceRow({ workspace, compact = false, onPress, onLongPress }: WorkspaceRowProps) {
const didLongPressRef = useRef(false) const didLongPressRef = useRef(false)
const createdAtLabel = resolveWorkspaceCreatedAtLabel(workspace)
const handlePress = useCallback(() => { const handlePress = useCallback(() => {
if (didLongPressRef.current) { if (didLongPressRef.current) {
@@ -215,6 +218,7 @@ function WorkspaceRow({ workspace, onPress, onLongPress }: WorkspaceRowProps) {
<Pressable <Pressable
style={({ pressed, hovered = false }) => [ style={({ pressed, hovered = false }) => [
styles.workspaceRow, styles.workspaceRow,
compact && styles.workspaceRowCompact,
hovered && styles.workspaceRowHovered, hovered && styles.workspaceRowHovered,
pressed && styles.workspaceRowPressed, pressed && styles.workspaceRowPressed,
]} ]}
@@ -226,9 +230,11 @@ function WorkspaceRow({ workspace, onPress, onLongPress }: WorkspaceRowProps) {
<Text style={styles.workspaceBranchText} numberOfLines={1}> <Text style={styles.workspaceBranchText} numberOfLines={1}>
{resolveWorkspaceBranchLabel(workspace)} {resolveWorkspaceBranchLabel(workspace)}
</Text> </Text>
<Text style={styles.workspaceCreatedAtText} numberOfLines={1}> {createdAtLabel ? (
{resolveWorkspaceCreatedAtLabel(workspace)} <Text style={styles.workspaceCreatedAtText} numberOfLines={1}>
</Text> {createdAtLabel}
</Text>
) : null}
</Pressable> </Pressable>
) )
} }
@@ -261,6 +267,7 @@ export function SidebarAgentList({
serverId, serverId,
isRefreshing = false, isRefreshing = false,
onRefresh, onRefresh,
onWorkspacePress,
listFooterComponent, listFooterComponent,
parentGestureRef, parentGestureRef,
}: SidebarAgentListProps) { }: SidebarAgentListProps) {
@@ -268,6 +275,7 @@ export function SidebarAgentList({
const showDesktopWebScrollbar = Platform.OS === 'web' && !isMobile const showDesktopWebScrollbar = Platform.OS === 'web' && !isMobile
const pathname = usePathname() const pathname = usePathname()
const [collapsedProjectKeys, setCollapsedProjectKeys] = useState<Set<string>>(new Set()) const [collapsedProjectKeys, setCollapsedProjectKeys] = useState<Set<string>>(new Set())
const [canonicalResyncNonce, setCanonicalResyncNonce] = useState(0)
const getProjectOrder = useSidebarOrderStore((state) => state.getProjectOrder) const getProjectOrder = useSidebarOrderStore((state) => state.getProjectOrder)
const setProjectOrder = useSidebarOrderStore((state) => state.setProjectOrder) const setProjectOrder = useSidebarOrderStore((state) => state.setProjectOrder)
@@ -381,7 +389,7 @@ export function SidebarAgentList({
} }
} }
return next return next
}, [collapsedProjectKeys, projects]) }, [canonicalResyncNonce, collapsedProjectKeys, projects])
const toggleProjectCollapsed = useCallback((projectKey: string) => { const toggleProjectCollapsed = useCallback((projectKey: string) => {
setCollapsedProjectKeys((prev) => { setCollapsedProjectKeys((prev) => {
@@ -417,10 +425,12 @@ export function SidebarAgentList({
return ( return (
<WorkspaceRow <WorkspaceRow
workspace={item.workspace} workspace={item.workspace}
compact={isMobile}
onPress={() => { onPress={() => {
if (!serverId) { if (!serverId) {
return return
} }
onWorkspacePress?.()
navigate(workspaceRoute as any) navigate(workspaceRoute as any)
}} }}
onLongPress={drag} onLongPress={drag}
@@ -429,6 +439,8 @@ export function SidebarAgentList({
}, },
[ [
collapsedProjectKeys, collapsedProjectKeys,
isMobile,
onWorkspacePress,
pathname, pathname,
projectIconByProjectKey, projectIconByProjectKey,
serverId, serverId,
@@ -444,6 +456,7 @@ export function SidebarAgentList({
return return
} }
let didPersistOrderChange = false
const reorderedProjectKeys = reorderedRows const reorderedProjectKeys = reorderedRows
.filter( .filter(
(row): row is Extract<SidebarTreeRow, { kind: 'project' }> => row.kind === 'project' (row): row is Extract<SidebarTreeRow, { kind: 'project' }> => row.kind === 'project'
@@ -457,6 +470,7 @@ export function SidebarAgentList({
reorderedVisibleKeys: reorderedProjectKeys, reorderedVisibleKeys: reorderedProjectKeys,
}) })
) { ) {
didPersistOrderChange = true
setProjectOrder( setProjectOrder(
serverId, serverId,
mergeWithRemainder({ mergeWithRemainder({
@@ -487,6 +501,7 @@ export function SidebarAgentList({
continue continue
} }
didPersistOrderChange = true
setWorkspaceOrder( setWorkspaceOrder(
serverId, serverId,
projectKey, projectKey,
@@ -496,6 +511,12 @@ export function SidebarAgentList({
}) })
) )
} }
// If persisted ordering did not change, force a local resync so draggable UI state
// snaps back to canonical Project -> Workspaces grouping.
if (!didPersistOrderChange) {
setCanonicalResyncNonce((prev) => prev + 1)
}
}, },
[getProjectOrder, getWorkspaceOrder, serverId, setProjectOrder, setWorkspaceOrder] [getProjectOrder, getWorkspaceOrder, serverId, setProjectOrder, setWorkspaceOrder]
) )
@@ -599,20 +620,21 @@ const styles = StyleSheet.create((theme) => ({
flexShrink: 0, flexShrink: 0,
}, },
workspaceRow: { workspaceRow: {
minHeight: 32, minHeight: 34,
marginBottom: theme.spacing[1], marginBottom: theme.spacing[1],
marginLeft: theme.spacing[6], marginLeft: theme.spacing[4],
paddingVertical: theme.spacing[2], paddingVertical: theme.spacing[1],
paddingHorizontal: theme.spacing[3], paddingHorizontal: theme.spacing[2],
borderRadius: theme.borderRadius.lg, borderRadius: theme.borderRadius.lg,
borderWidth: 1,
borderColor: theme.colors.border,
backgroundColor: theme.colors.surface0,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
gap: theme.spacing[2], gap: theme.spacing[2],
}, },
workspaceRowCompact: {
marginLeft: theme.spacing[3],
paddingHorizontal: theme.spacing[1],
},
workspaceRowHovered: { workspaceRowHovered: {
backgroundColor: theme.colors.surface1, backgroundColor: theme.colors.surface1,
}, },