diff --git a/ui/src/components/SidebarAgents.test.tsx b/ui/src/components/SidebarAgents.test.tsx
index 1630a66f128..0e38989b2f6 100644
--- a/ui/src/components/SidebarAgents.test.tsx
+++ b/ui/src/components/SidebarAgents.test.tsx
@@ -7,6 +7,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { Agent, ResourceMemberships } from "@paperclipai/shared";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { SidebarAgents } from "./SidebarAgents";
+import { queryKeys } from "../lib/queryKeys";
import { TooltipProvider } from "@/components/ui/tooltip";
const mockAgentsApi = vi.hoisted(() => ({
@@ -267,6 +268,7 @@ describe("SidebarAgents", () => {
currentRoot.unmount();
});
}
+ vi.useRealTimers();
queryClient.clear();
container.remove();
document.body.innerHTML = "";
@@ -302,6 +304,23 @@ describe("SidebarAgents", () => {
await flushReact();
}
+ async function renderSidebarAgentsWithFakeTimers() {
+ const currentRoot = createRoot(container);
+ root = currentRoot;
+
+ await act(async () => {
+ currentRoot.render(
+
+
+ ,
+ );
+ });
+ await act(async () => {
+ await Promise.resolve();
+ await vi.advanceTimersByTimeAsync(0);
+ });
+ }
+
async function renderRailSidebarAgents() {
mockSidebarState.collapsed = true;
const currentRoot = createRoot(container);
@@ -645,6 +664,49 @@ describe("SidebarAgents", () => {
expect(seeAllAgentsLink(container)?.getAttribute("href")).toBe("/agents/all");
});
+ it("keeps formerly live agents visible for the streamlined linger window", async () => {
+ vi.useFakeTimers({ now: new Date("2026-01-01T00:00:00Z") });
+ mockAgentsApi.list.mockResolvedValue([
+ makeAgent({ id: "agent-a", name: "Alpha", urlKey: "alpha" }),
+ makeAgent({ id: "agent-b", name: "Bravo", urlKey: "bravo" }),
+ makeAgent({ id: "agent-c", name: "Charlie", urlKey: "charlie" }),
+ makeAgent({ id: "agent-d", name: "Delta", urlKey: "delta" }),
+ ]);
+ mockHeartbeatsApi.liveRunsForCompany.mockResolvedValue([
+ { id: "run-1", agentId: "agent-a", status: "running" },
+ ]);
+
+ await renderSidebarAgentsWithFakeTimers();
+
+ let labels = agentLinkLabels(container);
+ expect(labels).toHaveLength(1);
+ expect(labels[0]).toContain("Alpha");
+ expect(labels[0]).toContain("1 live");
+
+ mockHeartbeatsApi.liveRunsForCompany.mockResolvedValue([]);
+ await act(async () => {
+ queryClient.setQueryData(queryKeys.liveRuns("company-1"), []);
+ });
+ await act(async () => {
+ await Promise.resolve();
+ await vi.advanceTimersByTimeAsync(0);
+ });
+
+ labels = agentLinkLabels(container);
+ expect(labels).toEqual(["Alpha"]);
+ expect(labels.join(" ")).not.toContain("live");
+
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(120_000);
+ });
+ expect(agentLinkLabels(container)).toEqual(["Alpha"]);
+
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(1);
+ });
+ expect(agentLinkLabels(container)).toEqual(["Alpha", "Bravo", "Charlie"]);
+ });
+
it("shows up to 3 recently-active agents plus a See all link when none are running", async () => {
mockAgentsApi.list.mockResolvedValue(
Array.from({ length: 7 }, (_, index) =>
diff --git a/ui/src/components/SidebarAgents.tsx b/ui/src/components/SidebarAgents.tsx
index 7e681da39b5..8df41e7419b 100644
--- a/ui/src/components/SidebarAgents.tsx
+++ b/ui/src/components/SidebarAgents.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useMemo, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Link, useLocation } from "@/lib/router";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
@@ -63,6 +63,7 @@ import type { Agent } from "@paperclipai/shared";
* recently-active agents plus a "See all agents" link (IA Phase 5).
*/
const RECENT_AGENT_LIMIT = 3;
+const LIVE_AGENT_LINGER_MS = 120_000;
const AGENT_SORT_CHOICES: SidebarSectionRadioChoice[] = [
{ value: "top", label: "Top" },
@@ -298,6 +299,8 @@ function SidebarAgentItem({
export function SidebarAgents({ streamlined = false }: { streamlined?: boolean } = {}) {
const [open, setOpen] = useState(true);
const [pendingAgentIds, setPendingAgentIds] = useState>(() => new Set());
+ const [, setLiveLingerVersion] = useState(0);
+ const lastSeenLiveAtRef = useRef