diff --git a/packages/ui/src/features/agent-applications/agent-builder/AgentBuilderDockLayout.tsx b/packages/ui/src/features/agent-applications/agent-builder/AgentBuilderDockLayout.tsx index f92599cb7..376914393 100644 --- a/packages/ui/src/features/agent-applications/agent-builder/AgentBuilderDockLayout.tsx +++ b/packages/ui/src/features/agent-applications/agent-builder/AgentBuilderDockLayout.tsx @@ -11,6 +11,11 @@ import { useAgentBuilderStore } from "./agentBuilderStore"; * disabled, the content renders unchanged with no dock or affordance. Hidden by * default; toggled via the edge affordance, the dock's hide button, or * Cmd/Ctrl+I. Panel sizes persist (`autoSaveId`). + * + * The content panel is always mounted in the same tree position; only the dock + * panel + resize handle toggle. Keeping the content's React identity stable + * across show/hide means panes don't remount — so per-pane state (the memory + * file you had open, a sessions filter, scroll position) survives toggling. */ export function AgentBuilderDockLayout({ children }: { children: ReactNode }) { const enabled = useFeatureFlag(AGENT_PLATFORM_FLAG); @@ -43,12 +48,9 @@ export function AgentBuilderDockLayout({ children }: { children: ReactNode }) { return <>{children}; } - // Collapsed: render content unchanged. The open affordance lives in the + // The content panel stays mounted whether the dock is open or not; only the + // dock panel + handle render conditionally. The open affordance lives in the // agents page headers (AgentBuilderHeaderControls); Cmd/Ctrl+Shift+I toggles. - if (!visible) { - return <>{children}; - } - return ( {children} - - - - + {visible ? ( + <> + + + + + + ) : null} ); } diff --git a/packages/ui/src/features/agent-applications/components/AgentApplicationsListView.tsx b/packages/ui/src/features/agent-applications/components/AgentApplicationsListView.tsx index 003353816..5f542f923 100644 --- a/packages/ui/src/features/agent-applications/components/AgentApplicationsListView.tsx +++ b/packages/ui/src/features/agent-applications/components/AgentApplicationsListView.tsx @@ -2,6 +2,7 @@ import { ArrowSquareOutIcon, CaretRightIcon, LockKeyIcon, + MagnifyingGlassIcon, RobotIcon, } from "@phosphor-icons/react"; import type { @@ -10,10 +11,11 @@ import type { } from "@posthog/shared/agent-platform-types"; import { AgentsTabLayout } from "@posthog/ui/features/agents/components/AgentsTabLayout"; import { Badge } from "@posthog/ui/primitives/Badge"; +import { Button } from "@posthog/ui/primitives/Button"; import { openExternalUrl } from "@posthog/ui/shell/openExternal"; import { Flex, Text } from "@radix-ui/themes"; import { Link } from "@tanstack/react-router"; -import { useMemo } from "react"; +import { useMemo, useState } from "react"; import { useAuthStateValue } from "../../auth/store"; import { useAgentAnalytics } from "../hooks/useAgentAnalytics"; import { useAgentApplications } from "../hooks/useAgentApplications"; @@ -24,11 +26,15 @@ import { AgentAnalyticsKpiStrip } from "./AgentAnalyticsView"; import { AgentDetailEmptyState } from "./AgentDetailLayout"; import { AgentFleetLiveSessionsPanel } from "./AgentFleetLiveSessionsPanel"; +type StatusFilter = "all" | "live" | "drafts"; + +/** Agents per page in the fleet list. */ +const PAGE_SIZE = 8; + /** * The Applications tab. Renders the deployed-agent fleet as the primary - * surface, with operational / activity / live-now panels appearing below the - * list only when they have something to say. A quiet fleet still feels like a - * launchpad: just the agents, sectioned LIVE vs DRAFTS. + * surface: a searchable, status-filtered, paged list with the 7-day activity + * strip pinned at the top and operational / live-now panels below. */ export function AgentApplicationsListView() { const region = useAuthStateValue((s) => s.cloudRegion); @@ -56,21 +62,89 @@ export function AgentApplicationsListView() { return map; }, [analytics]); - // Split LIVE vs DRAFT so the operational view foregrounds what's serving - // traffic; drafts dim and section below. - const { liveApps, draftApps } = useMemo(() => { - const live: AgentApplication[] = []; - const draft: AgentApplication[] = []; - for (const app of applications ?? []) { - if (app.live_revision != null) live.push(app); - else draft.push(app); - } - return { liveApps: live, draftApps: draft }; - }, [applications]); + // Live agents sort ahead of drafts so the operational view foregrounds what's + // serving traffic. Search + status filter + paging keep a large fleet + // navigable. + const [query, setQuery] = useState(""); + const [status, setStatus] = useState("all"); + const [page, setPage] = useState(0); + + const allApps = useMemo(() => applications ?? [], [applications]); + const liveCount = useMemo( + () => allApps.filter((a) => a.live_revision != null).length, + [allApps], + ); + const draftCount = allApps.length - liveCount; + + const filtered = useMemo(() => { + const q = query.trim().toLowerCase(); + return allApps + .filter((app) => { + if (status === "live" && app.live_revision == null) return false; + if (status === "drafts" && app.live_revision != null) return false; + if (!q) return true; + return ( + app.name.toLowerCase().includes(q) || + (app.slug?.toLowerCase().includes(q) ?? false) || + (app.description?.toLowerCase().includes(q) ?? false) + ); + }) + .sort( + (a, b) => + Number(b.live_revision != null) - Number(a.live_revision != null), + ); + }, [allApps, status, query]); + + const pageCount = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE)); + const safePage = Math.min(page, pageCount - 1); + const pageItems = filtered.slice( + safePage * PAGE_SIZE, + safePage * PAGE_SIZE + PAGE_SIZE, + ); + + function changeStatus(next: StatusFilter) { + setStatus(next); + setPage(0); + } + + function changeQuery(next: string) { + setQuery(next); + setPage(0); + } + + const statusFilters: { id: StatusFilter; label: string; count: number }[] = [ + { id: "all", label: "All", count: allApps.length }, + { id: "live", label: "Live", count: liveCount }, + { id: "drafts", label: "Drafts", count: draftCount }, + ]; return ( + {hasAnalytics ? ( +
+ + + Activity · last 7 days + + {aiObservabilityUrl ? ( + + ) : null} + + +
+ ) : null} +
{isLoading ? ( @@ -83,25 +157,105 @@ export function AgentApplicationsListView() { : "The agent platform API returned an error." } /> - ) : !applications || applications.length === 0 ? ( + ) : allApps.length === 0 ? ( ) : ( - - - {draftApps.length > 0 ? ( - + +
+ + changeQuery(e.currentTarget.value)} + placeholder="Search agents…" + aria-label="Search agents" + className="h-8 w-full rounded-(--radius-2) border border-border bg-(--color-panel-solid) pr-2 pl-8 text-[12.5px]" + /> +
+ + {statusFilters.map((f) => ( + + ))} + +
+ + {pageItems.length === 0 ? ( + + ) : ( + + {pageItems.map((app) => ( + + ))} + + )} + + {filtered.length > 0 ? ( + + + Showing {safePage * PAGE_SIZE + 1}– + {safePage * PAGE_SIZE + pageItems.length} of{" "} + {filtered.length} + + {pageCount > 1 ? ( + + + + {safePage + 1} / {pageCount} + + + + ) : null} + ) : null}
)} @@ -109,75 +263,12 @@ export function AgentApplicationsListView() { - {hasAnalytics ? ( -
- - - Activity · last 7 days - - {aiObservabilityUrl ? ( - - ) : null} - - -
- ) : null} - ); } -/** A labeled group of agent rows; `dimmed` softens drafts so live agents - * dominate the visual hierarchy. */ -function AgentsSection({ - label, - apps, - statsById, - dimmed, -}: { - label: string; - apps: AgentApplication[]; - statsById: Map; - dimmed?: boolean; -}) { - if (apps.length === 0) return null; - return ( - - - - {label} - - - {apps.length} - - -
- - {apps.map((app) => ( - - ))} - -
-
- ); -} - function ApplicationRow({ application, stats, diff --git a/packages/ui/src/features/agent-applications/components/AgentApprovalsPane.tsx b/packages/ui/src/features/agent-applications/components/AgentApprovalsPane.tsx index f07dd0708..cc5d89b33 100644 --- a/packages/ui/src/features/agent-applications/components/AgentApprovalsPane.tsx +++ b/packages/ui/src/features/agent-applications/components/AgentApprovalsPane.tsx @@ -43,8 +43,8 @@ export function AgentApprovalsPane({ : null; const filters = ( - - + + {APPROVAL_FILTERS.map((f) => (