diff --git a/packages/app/src/components/agent-list.tsx b/packages/app/src/components/agent-list.tsx index cae5d2584..42f4a7189 100644 --- a/packages/app/src/components/agent-list.tsx +++ b/packages/app/src/components/agent-list.tsx @@ -22,6 +22,7 @@ import { buildHostAgentDetailRoute } from "@/utils/host-routes"; import { resolveWorkspaceIdByExecutionDirectory } from "@/utils/workspace-execution"; import { prepareWorkspaceTab } from "@/utils/workspace-navigation"; import type { Agent } from "@/stores/session-store"; +import { useArchiveAgent } from "@/hooks/use-archive-agent"; interface AgentListProps { agents: AggregatedAgent[]; @@ -280,6 +281,7 @@ export function AgentList({ const insets = useSafeAreaInsets(); const [actionAgent, setActionAgent] = useState(null); const isMobile = useIsCompactFormFactor(); + const { archiveAgent } = useArchiveAgent(); const actionClient = useSessionStore((state) => actionAgent?.serverId ? (state.sessions[actionAgent.serverId]?.client ?? null) : null, @@ -320,20 +322,23 @@ export function AgentList({ [isActionSheetVisible, onAgentSelect], ); - const handleAgentLongPress = useCallback((agent: AggregatedAgent) => { - const isRunning = agent.status === "running" || agent.status === "initializing"; - if (isRunning) { - setActionAgent(agent); - return; - } + const handleAgentLongPress = useCallback( + (agent: AggregatedAgent) => { + const isRunning = agent.status === "running" || agent.status === "initializing"; + if (isRunning) { + setActionAgent(agent); + return; + } - const client = useSessionStore.getState().sessions[agent.serverId]?.client ?? null; - if (!client) { - setActionAgent(agent); - return; - } - void client.archiveAgent(agent.id); - }, []); + const client = useSessionStore.getState().sessions[agent.serverId]?.client ?? null; + if (!client) { + setActionAgent(agent); + return; + } + void archiveAgent({ serverId: agent.serverId, agentId: agent.id }).catch(() => {}); + }, + [archiveAgent], + ); const handleCloseActionSheet = useCallback(() => { setActionAgent(null); @@ -344,9 +349,9 @@ export function AgentList({ return; } // Timeout errors are swallowed — the daemon will still process the archive - void actionClient.archiveAgent(actionAgent.id).catch(() => {}); + void archiveAgent({ serverId: actionAgent.serverId, agentId: actionAgent.id }).catch(() => {}); setActionAgent(null); - }, [actionAgent, actionClient]); + }, [actionAgent, actionClient, archiveAgent]); const flatItems = useMemo((): FlatListItem[] => { const order = ["Today", "Yesterday", "This week", "This month", "Older"] as const; diff --git a/packages/app/src/components/composer.test.tsx b/packages/app/src/components/composer.test.tsx index 7ce27f38e..57dca6a48 100644 --- a/packages/app/src/components/composer.test.tsx +++ b/packages/app/src/components/composer.test.tsx @@ -344,7 +344,7 @@ vi.mock("@/contexts/toast-context", () => ({ useToast: () => ({ error: vi.fn() }), })); -vi.mock("@/utils/scroll-jank-investigation", () => ({ +vi.mock("@/utils/scroll-jank", () => ({ markScrollInvestigationRender: markScrollInvestigationRenderMock, markScrollInvestigationEvent: vi.fn(), })); diff --git a/packages/app/src/hooks/agent-history-query-key.ts b/packages/app/src/hooks/agent-history-query-key.ts new file mode 100644 index 000000000..1e1044cd5 --- /dev/null +++ b/packages/app/src/hooks/agent-history-query-key.ts @@ -0,0 +1,3 @@ +export function agentHistoryQueryKey(serverId: string | null) { + return ["agentHistory", serverId] as const; +} diff --git a/packages/app/src/hooks/use-agent-history.ts b/packages/app/src/hooks/use-agent-history.ts index f132b258f..b65064c0b 100644 --- a/packages/app/src/hooks/use-agent-history.ts +++ b/packages/app/src/hooks/use-agent-history.ts @@ -8,6 +8,7 @@ import { useCallback, useMemo } from "react"; import type { AggregatedAgent } from "@/hooks/use-aggregated-agents"; import { useHostRuntimeClient, useHostRuntimeIsConnected, useHosts } from "@/runtime/host-runtime"; import { buildAgentDirectoryState } from "@/utils/agent-directory-sync"; +import { agentHistoryQueryKey } from "./agent-history-query-key"; const AGENT_HISTORY_PAGE_LIMIT = 200; const AGENT_HISTORY_SORT: NonNullable = [ @@ -30,10 +31,6 @@ type AgentHistoryPage = { pageInfo: FetchAgentHistoryPageInfo; }; -export function agentHistoryQueryKey(serverId: string | null) { - return ["agentHistory", serverId] as const; -} - async function fetchAgentHistoryPage(input: { client: DaemonClient; serverId: string; diff --git a/packages/app/src/hooks/use-archive-agent.test.ts b/packages/app/src/hooks/use-archive-agent.test.ts index 1bd2de9cb..12601e95a 100644 --- a/packages/app/src/hooks/use-archive-agent.test.ts +++ b/packages/app/src/hooks/use-archive-agent.test.ts @@ -3,6 +3,7 @@ import { QueryClient } from "@tanstack/react-query"; import { beforeEach, describe, expect, it } from "vitest"; import type { Agent } from "@/stores/session-store"; import { useSessionStore } from "@/stores/session-store"; +import { agentHistoryQueryKey } from "./agent-history-query-key"; import { __private__, applyArchivedAgentCloseResults } from "./use-archive-agent"; function makeAgent(overrides: Partial = {}): Agent { @@ -112,6 +113,17 @@ describe("useArchiveAgent", () => { queryClient.setQueryData(["allAgents", "server-a"], { entries: [{ agent: { id: "agent-1" } }, { agent: { id: "agent-2" } }], }); + queryClient.setQueryData(agentHistoryQueryKey("server-a"), { + pages: [ + { + agents: [ + { id: "agent-1", archivedAt: null }, + { id: "agent-2", archivedAt: null }, + ], + }, + ], + pageParams: [null], + }); applyArchivedAgentCloseResults({ queryClient, @@ -131,6 +143,17 @@ describe("useArchiveAgent", () => { expect(queryClient.getQueryData(["allAgents", "server-a"])).toEqual({ entries: [{ agent: { id: "agent-2" } }], }); + expect(queryClient.getQueryData(agentHistoryQueryKey("server-a"))).toEqual({ + pages: [ + { + agents: [ + { id: "agent-1", archivedAt: new Date("2026-04-01T04:00:00.000Z") }, + { id: "agent-2", archivedAt: null }, + ], + }, + ], + pageParams: [null], + }); }); it("can apply archived agent close results without invalidating cached lists", () => { @@ -143,6 +166,14 @@ describe("useArchiveAgent", () => { queryClient.setQueryData(["allAgents", "server-a"], { entries: [{ agent: { id: "agent-1" } }, { agent: { id: "agent-2" } }], }); + queryClient.setQueryData(agentHistoryQueryKey("server-a"), { + pages: [ + { + agents: [{ id: "agent-1", archivedAt: null }], + }, + ], + pageParams: [null], + }); applyArchivedAgentCloseResults({ queryClient, @@ -153,5 +184,14 @@ describe("useArchiveAgent", () => { expect(queryClient.getQueryState(["sidebarAgentsList", "server-a"])?.isInvalidated).toBe(false); expect(queryClient.getQueryState(["allAgents", "server-a"])?.isInvalidated).toBe(false); + expect(queryClient.getQueryState(agentHistoryQueryKey("server-a"))?.isInvalidated).toBe(false); + expect(queryClient.getQueryData(agentHistoryQueryKey("server-a"))).toEqual({ + pages: [ + { + agents: [{ id: "agent-1", archivedAt: new Date("2026-04-01T04:00:00.000Z") }], + }, + ], + pageParams: [null], + }); }); }); diff --git a/packages/app/src/hooks/use-archive-agent.ts b/packages/app/src/hooks/use-archive-agent.ts index 5fc56a78c..f68b0e104 100644 --- a/packages/app/src/hooks/use-archive-agent.ts +++ b/packages/app/src/hooks/use-archive-agent.ts @@ -1,6 +1,7 @@ import { useCallback } from "react"; import { useMutation, useQuery, useQueryClient, type QueryClient } from "@tanstack/react-query"; import { useSessionStore } from "@/stores/session-store"; +import { agentHistoryQueryKey } from "./agent-history-query-key"; export const ARCHIVE_AGENT_PENDING_QUERY_KEY = ["archive-agent-pending"] as const; @@ -32,6 +33,7 @@ interface AgentsListQueryData { interface ArchivedAgentListCacheSnapshot { sidebarAgentsList: AgentsListQueryData | undefined; allAgents: AgentsListQueryData | undefined; + agentHistory: AgentHistoryQueryData | undefined; } interface ArchiveAgentMutationContext { @@ -39,6 +41,19 @@ interface ArchiveAgentMutationContext { lists: ArchivedAgentListCacheSnapshot; } +interface AgentHistoryQueryAgent { + id?: string | null; + archivedAt?: Date | null; +} + +interface AgentHistoryQueryPage { + agents?: AgentHistoryQueryAgent[]; +} + +interface AgentHistoryQueryData { + pages?: AgentHistoryQueryPage[]; +} + function toArchiveKey(input: ArchiveAgentInput): string { const serverId = input.serverId.trim(); const agentId = input.agentId.trim(); @@ -121,6 +136,54 @@ function removeAgentFromCachedLists(queryClient: QueryClient, input: ArchiveAgen ); } +function markAgentArchivedInHistoryPayload( + payload: T, + input: ArchiveAgentInput & { archivedAt: string }, +): T { + if (!payload || !Array.isArray(payload.pages) || !input.agentId) { + return payload; + } + + const archivedAt = new Date(input.archivedAt); + if (Number.isNaN(archivedAt.getTime())) { + return payload; + } + + let changed = false; + const pages = payload.pages.map((page) => { + if (!Array.isArray(page.agents)) { + return page; + } + + let pageChanged = false; + const agents = page.agents.map((agent) => { + if (agent.id !== input.agentId) { + return agent; + } + pageChanged = true; + changed = true; + return { + ...agent, + archivedAt, + }; + }); + + return pageChanged ? { ...page, agents } : page; + }); + + return changed ? ({ ...payload, pages } as T) : payload; +} + +function markAgentArchivedInHistoryCache( + queryClient: QueryClient, + input: ArchiveAgentInput & { archivedAt: string }, +): void { + queryClient.setQueryData( + agentHistoryQueryKey(input.serverId), + (current) => markAgentArchivedInHistoryPayload(current, input), + ); +} + function getStoredAgentSnapshot(input: ArchiveAgentInput) { return useSessionStore.getState().sessions[input.serverId]?.agents.get(input.agentId); } @@ -161,13 +224,16 @@ function getArchivedAgentListCacheSnapshot( serverId, ]), allAgents: queryClient.getQueryData(["allAgents", serverId]), + agentHistory: queryClient.getQueryData( + agentHistoryQueryKey(serverId), + ), }; } -function restoreCachedListSnapshot( +function restoreCachedQuerySnapshot( queryClient: QueryClient, - queryKey: readonly [string, string], - snapshot: AgentsListQueryData | undefined, + queryKey: readonly unknown[], + snapshot: T | undefined, ): void { if (snapshot === undefined) { queryClient.removeQueries({ queryKey, exact: true }); @@ -181,12 +247,13 @@ function restoreArchivedAgentListCacheSnapshot( serverId: string, snapshot: ArchivedAgentListCacheSnapshot, ): void { - restoreCachedListSnapshot( + restoreCachedQuerySnapshot( queryClient, ["sidebarAgentsList", serverId], snapshot.sidebarAgentsList, ); - restoreCachedListSnapshot(queryClient, ["allAgents", serverId], snapshot.allAgents); + restoreCachedQuerySnapshot(queryClient, ["allAgents", serverId], snapshot.allAgents); + restoreCachedQuerySnapshot(queryClient, agentHistoryQueryKey(serverId), snapshot.agentHistory); } function markAgentArchivedInStore(input: ArchiveAgentInput & { archivedAt: string }): void { @@ -235,6 +302,11 @@ export function applyArchivedAgentCloseResults(input: ApplyArchivedAgentCloseRes serverId: input.serverId, agentId: result.agentId, }); + markAgentArchivedInHistoryCache(input.queryClient, { + serverId: input.serverId, + agentId: result.agentId, + archivedAt: result.archivedAt, + }); } if (input.invalidateQueries ?? true) { @@ -244,6 +316,9 @@ export function applyArchivedAgentCloseResults(input: ApplyArchivedAgentCloseRes void input.queryClient.invalidateQueries({ queryKey: ["allAgents", input.serverId], }); + void input.queryClient.invalidateQueries({ + queryKey: agentHistoryQueryKey(input.serverId), + }); } } @@ -324,6 +399,9 @@ export function useArchiveAgent() { void queryClient.invalidateQueries({ queryKey: ["allAgents", input.serverId], }); + void queryClient.invalidateQueries({ + queryKey: agentHistoryQueryKey(input.serverId), + }); }, }); @@ -359,4 +437,5 @@ export const __private__ = { setAgentArchiving, isAgentArchiving, removeAgentFromListPayload, + markAgentArchivedInHistoryPayload, };