fix(app): subagents track display + archive flicker

- Bucket finished subagents as `done` instead of `attention`; drop
  "needs attention" segment from the section header — subagents are
  transient and don't carry an unread state worth surfacing.
- Gate the subagents selector on the existing pending-archive query so
  optimistic archives survive authoritative agent snapshots that wipe
  the local `archivedAt`. Last subagent archived now cleanly hides the
  whole track via the existing empty-rows guard.
This commit is contained in:
Mohamed Boudra
2026-05-09 16:28:09 +07:00
parent 34df55a10a
commit 3eda7dd15b
7 changed files with 253 additions and 80 deletions
@@ -1,10 +1,19 @@
/**
* @vitest-environment jsdom
*/
import React from "react";
import type { DaemonClient } from "@server/client/daemon-client";
import { QueryClient } from "@tanstack/react-query";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { act, renderHook, waitFor } from "@testing-library/react";
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";
import {
__private__,
applyArchivedAgentCloseResults,
usePendingArchiveAgentIds,
} from "./use-archive-agent";
function makeAgent(overrides: Partial<Agent> = {}): Agent {
return {
@@ -92,6 +101,48 @@ describe("useArchiveAgent", () => {
).toBe(false);
});
it("selects pending archive ids for a single server", () => {
const pendingIds = __private__.selectPendingArchiveAgentIds(
{
"server-a:agent-1": true,
"server-a:agent-2": true,
"server-b:agent-3": true,
},
"server-a",
);
expect(Array.from(pendingIds)).toEqual(["agent-1", "agent-2"]);
});
it("subscribes to pending archive ids through react-query", async () => {
const queryClient = new QueryClient();
const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);
const { result } = renderHook(() => usePendingArchiveAgentIds("server-a"), { wrapper });
expect(Array.from(result.current)).toEqual([]);
act(() => {
__private__.setAgentArchiving({
queryClient,
serverId: "server-a",
agentId: "agent-1",
isArchiving: true,
});
__private__.setAgentArchiving({
queryClient,
serverId: "server-b",
agentId: "agent-2",
isArchiving: true,
});
});
await waitFor(() => {
expect(Array.from(result.current)).toEqual(["agent-1"]);
});
});
it("removes an archived agent from cached list payloads", () => {
const payload = {
entries: [{ agent: { id: "agent-1" } }, { agent: { id: "agent-2" } }],
+48 -5
View File
@@ -1,9 +1,10 @@
import { useCallback } from "react";
import { useCallback, useMemo } 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;
const EMPTY_PENDING_ARCHIVE_AGENT_IDS = new Set<string>();
export interface ArchiveAgentInput {
serverId: string;
@@ -67,6 +68,35 @@ function readPendingState(queryClient: QueryClient): ArchiveAgentPendingState {
return queryClient.getQueryData<ArchiveAgentPendingState>(ARCHIVE_AGENT_PENDING_QUERY_KEY) ?? {};
}
function selectPendingArchiveAgentIds(
pendingState: ArchiveAgentPendingState,
serverId: string,
): ReadonlySet<string> {
const normalizedServerId = serverId.trim();
if (!normalizedServerId) {
return EMPTY_PENDING_ARCHIVE_AGENT_IDS;
}
const prefix = `${normalizedServerId}:`;
let agentIds: string[] | null = null;
for (const key of Object.keys(pendingState)) {
if (!key.startsWith(prefix)) {
continue;
}
const agentId = key.slice(prefix.length);
if (!agentId) {
continue;
}
agentIds ??= [];
agentIds.push(agentId);
}
if (!agentIds || agentIds.length === 0) {
return EMPTY_PENDING_ARCHIVE_AGENT_IDS;
}
return new Set(agentIds);
}
function setAgentArchiving(input: SetAgentArchivingInput): void {
const key = toArchiveKey(input);
if (!key) {
@@ -329,16 +359,28 @@ export function clearArchiveAgentPending(input: IsAgentArchivingInput): void {
});
}
export function useArchiveAgent() {
const queryClient = useQueryClient();
const pendingQuery = useQuery({
function useArchiveAgentPendingQuery() {
return useQuery({
queryKey: ARCHIVE_AGENT_PENDING_QUERY_KEY,
queryFn: async (): Promise<ArchiveAgentPendingState> => ({}),
initialData: {} as ArchiveAgentPendingState,
staleTime: Infinity,
gcTime: Infinity,
});
}
export function usePendingArchiveAgentIds(serverId: string): ReadonlySet<string> {
const pendingQuery = useArchiveAgentPendingQuery();
return useMemo(
() => selectPendingArchiveAgentIds(pendingQuery.data ?? {}, serverId),
[pendingQuery.data, serverId],
);
}
export function useArchiveAgent() {
const queryClient = useQueryClient();
const pendingQuery = useArchiveAgentPendingQuery();
const archiveMutation = useMutation({
mutationFn: async (input: ArchiveAgentInput): Promise<{ archivedAt: string }> => {
@@ -434,6 +476,7 @@ export function useArchiveAgent() {
export const __private__ = {
toArchiveKey,
readPendingState,
selectPendingArchiveAgentIds,
setAgentArchiving,
isAgentArchiving,
removeAgentFromListPayload,