Add agent list sidebar on large screens

Show AgentList as a left sidebar (280px) when viewing an agent on
screens >= 768px wide. The current agent is highlighted with a
primary-colored border and accent background.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2025-12-25 11:43:14 +07:00
co-authored by Claude Opus 4.5
parent 7b00544caa
commit fe5e86d514
2 changed files with 84 additions and 37 deletions
@@ -19,6 +19,8 @@ import { MoreVertical, GitBranch, Folder, RotateCcw, PlusCircle, Download, Users
import { BackHeader } from "@/components/headers/back-header"; import { BackHeader } from "@/components/headers/back-header";
import { AgentStreamView } from "@/components/agent-stream-view"; import { AgentStreamView } from "@/components/agent-stream-view";
import { AgentInputArea } from "@/components/agent-input-area"; import { AgentInputArea } from "@/components/agent-input-area";
import { AgentList } from "@/components/agent-list";
import { useAggregatedAgents } from "@/hooks/use-aggregated-agents";
import { CreateAgentModal, ImportAgentModal, type CreateAgentInitialValues } from "@/components/create-agent-modal"; import { CreateAgentModal, ImportAgentModal, type CreateAgentInitialValues } from "@/components/create-agent-modal";
import { useDaemonConnections } from "@/contexts/daemon-connections-context"; import { useDaemonConnections } from "@/contexts/daemon-connections-context";
import type { ConnectionStatus } from "@/contexts/daemon-connections-context"; import type { ConnectionStatus } from "@/contexts/daemon-connections-context";
@@ -133,11 +135,17 @@ type AgentScreenContentProps = {
onBack: () => void; onBack: () => void;
}; };
const SIDEBAR_WIDTH = 280;
const LARGE_SCREEN_BREAKPOINT = 768;
function AgentScreenContent({ serverId, agentId, onBack }: AgentScreenContentProps) { function AgentScreenContent({ serverId, agentId, onBack }: AgentScreenContentProps) {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const router = useRouter(); const router = useRouter();
const { width: windowWidth, height: windowHeight } = useWindowDimensions(); const { width: windowWidth, height: windowHeight } = useWindowDimensions();
const isLargeScreen = windowWidth >= LARGE_SCREEN_BREAKPOINT;
const { agents: aggregatedAgents, isRevalidating, refreshAll } = useAggregatedAgents();
const [menuVisible, setMenuVisible] = useState(false); const [menuVisible, setMenuVisible] = useState(false);
const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0 }); const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0 });
const [menuContentHeight, setMenuContentHeight] = useState(0); const [menuContentHeight, setMenuContentHeight] = useState(0);
@@ -531,43 +539,60 @@ function AgentScreenContent({ serverId, agentId, onBack }: AgentScreenContentPro
return ( return (
<> <>
<View style={styles.container}> <View style={styles.container}>
{/* Header */} <View style={[styles.mainLayout, isLargeScreen && styles.mainLayoutRow]}>
<BackHeader {/* Sidebar - only on large screens */}
title={agent.title || "Agent"} {isLargeScreen && (
onBack={handleBack} <View style={[styles.sidebar, { width: SIDEBAR_WIDTH }]}>
rightContent={ <AgentList
<View ref={menuButtonRef} collapsable={false}> agents={aggregatedAgents}
<Pressable onPress={handleOpenMenu} style={styles.menuButton}> isRefreshing={isRevalidating}
<MoreVertical size={20} color={theme.colors.foreground} /> onRefresh={refreshAll}
</Pressable> selectedAgentId={resolvedAgentId}
</View>
}
/>
{/* Content Area with Keyboard Animation */}
<View style={styles.contentContainer}>
<ReanimatedAnimated.View style={[styles.content, animatedKeyboardStyle]}>
{isInitializing ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={theme.colors.primary} />
<Text style={styles.loadingText}>Loading agent...</Text>
</View>
) : (
<AgentStreamView
agentId={agent.id}
serverId={serverId}
agent={agent}
streamItems={streamItems}
pendingPermissions={pendingPermissions}
/> />
)} </View>
</ReanimatedAnimated.View> )}
</View>
{/* Agent Input Area */} {/* Main agent panel */}
{!isInitializing && agent && resolvedAgentId && ( <View style={styles.agentPanel}>
<AgentInputArea agentId={resolvedAgentId} serverId={serverId} /> {/* Header */}
)} <BackHeader
title={agent.title || "Agent"}
onBack={handleBack}
rightContent={
<View ref={menuButtonRef} collapsable={false}>
<Pressable onPress={handleOpenMenu} style={styles.menuButton}>
<MoreVertical size={20} color={theme.colors.foreground} />
</Pressable>
</View>
}
/>
{/* Content Area with Keyboard Animation */}
<View style={styles.contentContainer}>
<ReanimatedAnimated.View style={[styles.content, animatedKeyboardStyle]}>
{isInitializing ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={theme.colors.primary} />
<Text style={styles.loadingText}>Loading agent...</Text>
</View>
) : (
<AgentStreamView
agentId={agent.id}
serverId={serverId}
agent={agent}
streamItems={streamItems}
pendingPermissions={pendingPermissions}
/>
)}
</ReanimatedAnimated.View>
</View>
{/* Agent Input Area */}
{!isInitializing && agent && resolvedAgentId && (
<AgentInputArea agentId={resolvedAgentId} serverId={serverId} />
)}
</View>
</View>
{/* Dropdown Menu */} {/* Dropdown Menu */}
<Modal <Modal
@@ -793,6 +818,19 @@ const styles = StyleSheet.create((theme) => ({
flex: 1, flex: 1,
backgroundColor: theme.colors.background, backgroundColor: theme.colors.background,
}, },
mainLayout: {
flex: 1,
},
mainLayoutRow: {
flexDirection: "row",
},
sidebar: {
borderRightWidth: 1,
borderRightColor: theme.colors.border,
},
agentPanel: {
flex: 1,
},
contentContainer: { contentContainer: {
flex: 1, flex: 1,
overflow: "hidden", overflow: "hidden",
+11 -2
View File
@@ -12,9 +12,10 @@ interface AgentListProps {
agents: AggregatedAgent[]; agents: AggregatedAgent[];
isRefreshing?: boolean; isRefreshing?: boolean;
onRefresh?: () => void; onRefresh?: () => void;
selectedAgentId?: string;
} }
export function AgentList({ agents, isRefreshing = false, onRefresh }: AgentListProps) { export function AgentList({ agents, isRefreshing = false, onRefresh, selectedAgentId }: AgentListProps) {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const [actionAgent, setActionAgent] = useState<AggregatedAgent | null>(null); const [actionAgent, setActionAgent] = useState<AggregatedAgent | null>(null);
@@ -87,11 +88,13 @@ export function AgentList({ agents, isRefreshing = false, onRefresh }: AgentList
const timeAgo = formatTimeAgo(agent.lastActivityAt); const timeAgo = formatTimeAgo(agent.lastActivityAt);
const providerLabel = getAgentProviderDefinition(agent.provider).label; const providerLabel = getAgentProviderDefinition(agent.provider).label;
const isRunning = agent.status === "running"; const isRunning = agent.status === "running";
const isSelected = selectedAgentId === agent.id;
return ( return (
<Pressable <Pressable
style={({ pressed }) => [ style={({ pressed }) => [
styles.agentItem, styles.agentItem,
isSelected && styles.agentItemSelected,
pressed && styles.agentItemPressed, pressed && styles.agentItemPressed,
]} ]}
onPress={() => handleAgentPress(agent.serverId, agent.id)} onPress={() => handleAgentPress(agent.serverId, agent.id)}
@@ -157,7 +160,7 @@ export function AgentList({ agents, isRefreshing = false, onRefresh }: AgentList
</Pressable> </Pressable>
); );
}, },
[handleAgentLongPress, handleAgentPress, theme.colors.muted, theme.colors.mutedForeground, theme.colors.primary, theme.colors.primaryForeground] [handleAgentLongPress, handleAgentPress, selectedAgentId, theme.colors.muted, theme.colors.mutedForeground, theme.colors.primary, theme.colors.primaryForeground]
); );
const keyExtractor = useCallback( const keyExtractor = useCallback(
@@ -247,6 +250,12 @@ const styles = StyleSheet.create((theme) => ({
borderRadius: theme.borderRadius.lg, borderRadius: theme.borderRadius.lg,
marginBottom: theme.spacing[2], marginBottom: theme.spacing[2],
backgroundColor: theme.colors.muted, backgroundColor: theme.colors.muted,
borderWidth: 1,
borderColor: "transparent",
},
agentItemSelected: {
backgroundColor: theme.colors.accent,
borderColor: theme.colors.primary,
}, },
agentItemPressed: { agentItemPressed: {
opacity: 0.7, opacity: 0.7,