diff --git a/apps/web/src/components/app/agents-view-feedback-detail.tsx b/apps/web/src/components/app/agents-view-feedback-detail.tsx new file mode 100644 index 00000000..0e6e61fb --- /dev/null +++ b/apps/web/src/components/app/agents-view-feedback-detail.tsx @@ -0,0 +1,85 @@ +import { type FeedbackDetailState } from "@/components/app/feedback-utils"; +import { + FeedbackDetailPanel, + MobileFeedbackSheet, + MobileReviewSummarySheet, + ReviewSummaryPanel, +} from "@/components/app/feedback-panel"; +import { type Agent } from "@/components/app/types"; + +type FeedbackDetailProps = { + detail: NonNullable; + agents: Agent[]; + connectedAgentId: string | null; + sendTerminalInput: (data: string) => void; + onClose: () => void; + onNavigateItem: (parentAgentId: string, nextItemId: number) => void; +}; + +export function DesktopFeedbackDetail({ + detail, + agents, + connectedAgentId, + sendTerminalInput, + onClose, + onNavigateItem, +}: FeedbackDetailProps): JSX.Element | null { + if ("summaryAgentId" in detail) { + const summaryAgent = agents.find((a) => a.id === detail.summaryAgentId); + return summaryAgent ? ( + + ) : null; + } + + return ( + + onNavigateItem(detail.parentAgentId, nextItemId) + } + /> + ); +} + +export function MobileFeedbackDetail({ + detail, + agents, + connectedAgentId, + sendTerminalInput, + onClose, + onNavigateItem, +}: FeedbackDetailProps): JSX.Element | null { + if ("summaryAgentId" in detail) { + const summaryAgent = agents.find((a) => a.id === detail.summaryAgentId); + return summaryAgent ? ( + + ) : null; + } + + return ( + + onNavigateItem(detail.parentAgentId, nextItemId) + } + /> + ); +} diff --git a/apps/web/src/components/app/agents-view.tsx b/apps/web/src/components/app/agents-view.tsx index 5e334b6c..b71eb5e9 100644 --- a/apps/web/src/components/app/agents-view.tsx +++ b/apps/web/src/components/app/agents-view.tsx @@ -1,11 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { - Routes, - Route, - useMatch, - useNavigate, - useParams, -} from "react-router-dom"; +import { Routes, Route, useParams } from "react-router-dom"; import { PanelLeftOpen, PanelRightOpen } from "lucide-react"; import { ChangesTab } from "@/components/app/changes-tab"; @@ -23,12 +17,9 @@ import { import { CreateAgentDialog } from "@/components/app/create-agent-dialog"; import { DeleteAgentDialog } from "@/components/app/delete-agent-dialog"; import { - type FeedbackDetailState, - FeedbackDetailPanel, - MobileFeedbackSheet, - MobileReviewSummarySheet, - ReviewSummaryPanel, -} from "@/components/app/feedback-panel"; + DesktopFeedbackDetail, + MobileFeedbackDetail, +} from "@/components/app/agents-view-feedback-detail"; import { MediaLightbox } from "@/components/app/media-lightbox"; import { MediaSidebar, @@ -52,12 +43,6 @@ import { GlassSidebar } from "@/components/ui/glass-sidebar"; import { uploadAgentMedia } from "@/lib/media-upload"; import { type AgentType, isCliAgentType } from "@/lib/agent-types"; import { type IdeType } from "@/lib/ide-types"; -import { - agentChangesRoute, - agentFeedbackRoute, - agentReviewRoute, - agentRoute, -} from "@/lib/agent-routes"; import { cn } from "@/lib/utils"; import { useAgentActions } from "@/hooks/use-agent-actions"; import { useAgents } from "@/hooks/use-agents"; @@ -65,6 +50,7 @@ import { useMedia } from "@/hooks/use-media"; import { useMediaSidebarState } from "@/hooks/use-media-sidebar-state"; import { useTerminal } from "@/hooks/use-terminal"; import { useAgentFocus } from "@/hooks/use-agent-focus"; +import { useAgentsViewRouting } from "@/hooks/use-agents-view-routing"; import { LaunchTemplateDialog } from "@/components/app/automations-launch-dialog"; import { CommandPalette } from "@/components/app/command-palette"; import { useAgentHotkeys } from "@/hooks/use-agent-hotkeys"; @@ -102,13 +88,7 @@ export function AgentsView({ triggerNavAnimation, onNavigateSection, }: AgentsViewProps): JSX.Element { - const navigate = useNavigate(); const { agentId: routeAgentId } = useParams(); - const feedbackMatch = useMatch("/agents/:agentId/feedback/:itemId"); - const reviewMatch = useMatch("/agents/:agentId/review/:summaryAgentId"); - const changesMatch = useMatch("/agents/:agentId/changes"); - const itemId = feedbackMatch?.params.itemId; - const summaryAgentId = reviewMatch?.params.summaryAgentId; const [sharedConnectedAgentId, setSharedConnectedAgentId] = useState< string | null @@ -133,6 +113,22 @@ export function AgentsView({ routeAgentId ?? null ); + const { + changesMatch, + feedbackDetail, + feedbackDetailRendered, + handleFeedbackTransitionEnd, + closeFeedbackDetail, + openFeedbackDetail, + navigateFeedbackItem, + onTabChange, + } = useAgentsViewRouting({ + routeAgentId, + agents, + agentsLoaded, + validatedSelectedAgentId, + }); + const [createOpen, setCreateOpen] = useState(false); const [requestedCreateType, setRequestedCreateType] = useState(null); @@ -147,22 +143,6 @@ export function AgentsView({ readExpandedAgentId() ); - const feedbackItemId = - itemId !== undefined && Number.isInteger(Number(itemId)) - ? Number(itemId) - : null; - const feedbackDetail = routeAgentId - ? summaryAgentId - ? { parentAgentId: routeAgentId, summaryAgentId } - : feedbackItemId !== null - ? { parentAgentId: routeAgentId, itemId: feedbackItemId } - : null - : null; - const feedbackDetailStaleRef = - useRef | null>(null); - if (feedbackDetail) feedbackDetailStaleRef.current = feedbackDetail; - const feedbackDetailRendered = - feedbackDetail ?? feedbackDetailStaleRef.current; const pendingAutoAttachAgentIdRef = useRef(null); const sidebarAgentId = sharedConnectedAgentId ?? validatedSelectedAgentId; const agentIds = useMemo(() => agents.map((a) => a.id), [agents]); @@ -264,18 +244,6 @@ export function AgentsView({ useAgentFocus(focusedAgentId, "authenticated"); - const onTabChange = useCallback( - (tab: "terminal" | "changes") => { - if (!routeAgentId) return; - navigate( - tab === "changes" - ? agentChangesRoute(routeAgentId) - : agentRoute(routeAgentId), - { replace: true } - ); - }, - [navigate, routeAgentId] - ); const { diffStats: focusedDiffStats } = useAgentDiffStats( focusedAgentId ?? "", !!focusedAgentId @@ -318,13 +286,6 @@ export function AgentsView({ }, [agents, validatedSelectedAgentId]); const prevSelectedExpansionTargetRef = useRef(null); - useEffect(() => { - if (!routeAgentId) return; - if (!agentsLoaded) return; - if (validatedSelectedAgentId) return; - navigate("/agents", { replace: true }); - }, [agentsLoaded, navigate, routeAgentId, validatedSelectedAgentId]); - useEffect(() => { if (!selectedExpansionTarget) { prevSelectedExpansionTargetRef.current = null; @@ -339,25 +300,6 @@ export function AgentsView({ ); }, [selectedExpansionTarget]); - useEffect(() => { - if (!routeAgentId) return; - if (!agentsLoaded) return; - if (itemId !== undefined && feedbackItemId === null) { - navigate(agentRoute(routeAgentId), { replace: true }); - } - }, [agentsLoaded, feedbackItemId, itemId, navigate, routeAgentId]); - - useEffect(() => { - if (!routeAgentId || !summaryAgentId) return; - if (!agentsLoaded) return; - const summaryAgentExists = agents.some( - (agent) => agent.id === summaryAgentId - ); - if (!summaryAgentExists) { - navigate(agentRoute(routeAgentId), { replace: true }); - } - }, [agents, agentsLoaded, navigate, routeAgentId, summaryAgentId]); - useEffect(() => { if (!validatedSelectedAgentId) return; if (connectedAgentId === validatedSelectedAgentId) { @@ -430,36 +372,6 @@ export function AgentsView({ openCreateDialog, }); - const closeFeedbackDetail = useCallback(() => { - if (validatedSelectedAgentId) { - navigate(agentRoute(validatedSelectedAgentId), { replace: true }); - return; - } - navigate("/agents", { replace: true }); - }, [navigate, validatedSelectedAgentId]); - - const openFeedbackDetail = useCallback( - (state: FeedbackDetailState) => { - if (!state) { - closeFeedbackDetail(); - return; - } - if ("summaryAgentId" in state) { - navigate(agentReviewRoute(state.parentAgentId, state.summaryAgentId)); - return; - } - navigate(agentFeedbackRoute(state.parentAgentId, state.itemId)); - }, - [closeFeedbackDetail, navigate] - ); - - const navigateFeedbackItem = useCallback( - (parentAgentId: string, nextItemId: number) => { - navigate(agentFeedbackRoute(parentAgentId, nextItemId)); - }, - [navigate] - ); - const toggleAgentDetails = useCallback((agentId: string) => { setExpandedAgentId((current) => (current === agentId ? null : agentId)); }, []); @@ -573,11 +485,7 @@ export function AgentsView({ ? "grid-rows-[minmax(0,1fr)_minmax(0,1fr)]" : "grid-rows-[minmax(0,1fr)_0fr]" )} - onTransitionEnd={(e) => { - if (e.propertyName === "grid-template-rows" && !feedbackDetail) { - feedbackDetailStaleRef.current = null; - } - }} + onTransitionEnd={handleFeedbackTransitionEnd} >
@@ -699,39 +607,14 @@ export function AgentsView({ )} > {feedbackDetailRendered ? ( - "summaryAgentId" in feedbackDetailRendered ? ( - (() => { - const summaryAgent = agents.find( - (a) => a.id === feedbackDetailRendered.summaryAgentId - ); - return summaryAgent ? ( - - ) : null; - })() - ) : ( - - navigateFeedbackItem( - feedbackDetailRendered.parentAgentId, - nextItemId - ) - } - /> - ) + ) : null}
) : null} @@ -783,31 +666,14 @@ export function AgentsView({
{isMobile && feedbackDetail ? ( - "summaryAgentId" in feedbackDetail ? ( - (() => { - const summaryAgent = agents.find( - (a) => a.id === feedbackDetail.summaryAgentId - ); - return summaryAgent ? ( - - ) : null; - })() - ) : ( - - navigateFeedbackItem(feedbackDetail.parentAgentId, nextItemId) - } - /> - ) + ) : null} {isMobile ? ( diff --git a/apps/web/src/hooks/use-agents-view-routing.ts b/apps/web/src/hooks/use-agents-view-routing.ts new file mode 100644 index 00000000..6fc17d42 --- /dev/null +++ b/apps/web/src/hooks/use-agents-view-routing.ts @@ -0,0 +1,139 @@ +import { useCallback, useEffect, useRef, type TransitionEvent } from "react"; +import { useMatch, useNavigate } from "react-router-dom"; + +import { type FeedbackDetailState } from "@/components/app/feedback-utils"; +import { type Agent } from "@/components/app/types"; +import { + agentChangesRoute, + agentFeedbackRoute, + agentReviewRoute, + agentRoute, +} from "@/lib/agent-routes"; + +type UseAgentsViewRoutingOptions = { + routeAgentId: string | undefined; + agents: Agent[]; + agentsLoaded: boolean; + validatedSelectedAgentId: string | null; +}; + +export function useAgentsViewRouting({ + routeAgentId, + agents, + agentsLoaded, + validatedSelectedAgentId, +}: UseAgentsViewRoutingOptions) { + const navigate = useNavigate(); + const feedbackMatch = useMatch("/agents/:agentId/feedback/:itemId"); + const reviewMatch = useMatch("/agents/:agentId/review/:summaryAgentId"); + const changesMatch = useMatch("/agents/:agentId/changes"); + const itemId = feedbackMatch?.params.itemId; + const summaryAgentId = reviewMatch?.params.summaryAgentId; + + const feedbackItemId = + itemId !== undefined && Number.isInteger(Number(itemId)) + ? Number(itemId) + : null; + const feedbackDetail: FeedbackDetailState = routeAgentId + ? summaryAgentId + ? { parentAgentId: routeAgentId, summaryAgentId } + : feedbackItemId !== null + ? { parentAgentId: routeAgentId, itemId: feedbackItemId } + : null + : null; + const feedbackDetailStaleRef = + useRef | null>(null); + if (feedbackDetail) feedbackDetailStaleRef.current = feedbackDetail; + const feedbackDetailRendered = + feedbackDetail ?? feedbackDetailStaleRef.current; + + useEffect(() => { + if (!routeAgentId) return; + if (!agentsLoaded) return; + if (validatedSelectedAgentId) return; + navigate("/agents", { replace: true }); + }, [agentsLoaded, navigate, routeAgentId, validatedSelectedAgentId]); + + useEffect(() => { + if (!routeAgentId) return; + if (!agentsLoaded) return; + if (itemId !== undefined && feedbackItemId === null) { + navigate(agentRoute(routeAgentId), { replace: true }); + } + }, [agentsLoaded, feedbackItemId, itemId, navigate, routeAgentId]); + + useEffect(() => { + if (!routeAgentId || !summaryAgentId) return; + if (!agentsLoaded) return; + const summaryAgentExists = agents.some( + (agent) => agent.id === summaryAgentId + ); + if (!summaryAgentExists) { + navigate(agentRoute(routeAgentId), { replace: true }); + } + }, [agents, agentsLoaded, navigate, routeAgentId, summaryAgentId]); + + const onTabChange = useCallback( + (tab: "terminal" | "changes") => { + if (!routeAgentId) return; + navigate( + tab === "changes" + ? agentChangesRoute(routeAgentId) + : agentRoute(routeAgentId), + { replace: true } + ); + }, + [navigate, routeAgentId] + ); + + const closeFeedbackDetail = useCallback(() => { + if (validatedSelectedAgentId) { + navigate(agentRoute(validatedSelectedAgentId), { replace: true }); + return; + } + navigate("/agents", { replace: true }); + }, [navigate, validatedSelectedAgentId]); + + const openFeedbackDetail = useCallback( + (state: FeedbackDetailState) => { + if (!state) { + closeFeedbackDetail(); + return; + } + if ("summaryAgentId" in state) { + navigate(agentReviewRoute(state.parentAgentId, state.summaryAgentId)); + return; + } + navigate(agentFeedbackRoute(state.parentAgentId, state.itemId)); + }, + [closeFeedbackDetail, navigate] + ); + + const navigateFeedbackItem = useCallback( + (parentAgentId: string, nextItemId: number) => { + navigate(agentFeedbackRoute(parentAgentId, nextItemId)); + }, + [navigate] + ); + + const hasFeedbackDetail = !!feedbackDetail; + const handleFeedbackTransitionEnd = useCallback( + (e: TransitionEvent) => { + if (e.propertyName === "grid-template-rows" && !hasFeedbackDetail) { + feedbackDetailStaleRef.current = null; + } + }, + [hasFeedbackDetail] + ); + + return { + changesMatch: !!changesMatch, + feedbackDetail, + feedbackDetailRendered, + handleFeedbackTransitionEnd, + closeFeedbackDetail, + openFeedbackDetail, + navigateFeedbackItem, + onTabChange, + }; +}