fix(app): reduce workspace screen re-renders and fix archived agent handling

Replace greedy agents Map subscription with a Zustand selector that
derives workspace agent visibility (ID sets only) with custom equality,
so the workspace screen only re-renders when agents are added, removed,
or archived — not on every status/activity update.

Fix tab pruning to check against all known agents (including archived)
instead of only active agents, so archived agent tabs survive
reconciliation. Remove the auto-unarchive effect that called
refreshAgent() as a workaround. Hide input area for archived agents.
This commit is contained in:
Mohamed Boudra
2026-03-20 21:07:51 +07:00
parent 72ea9b7a72
commit 48924626df
4 changed files with 95 additions and 152 deletions
+1 -1
View File
@@ -802,7 +802,7 @@ function AgentPanelBody({
</ReanimatedAnimated.View> </ReanimatedAnimated.View>
</View> </View>
{agentId && !isArchivingCurrentAgent ? ( {agentId && !isArchivingCurrentAgent && !agent?.archivedAt ? (
<AgentInputArea <AgentInputArea
agentId={agentId} agentId={agentId}
serverId={serverId} serverId={serverId}
@@ -4,6 +4,7 @@ import {
canOpenAgentTabFromRoute, canOpenAgentTabFromRoute,
deriveWorkspaceAgentVisibility, deriveWorkspaceAgentVisibility,
shouldPruneWorkspaceAgentTab, shouldPruneWorkspaceAgentTab,
workspaceAgentVisibilityEqual,
} from "@/screens/workspace/workspace-agent-visibility"; } from "@/screens/workspace/workspace-agent-visibility";
function makeAgent(input: { function makeAgent(input: {
@@ -53,7 +54,7 @@ function makeAgent(input: {
} }
describe("workspace agent visibility", () => { describe("workspace agent visibility", () => {
it("keeps archived agents hidden from visible list but present in workspace lookup", () => { it("keeps archived agents out of activeAgentIds but present in knownAgentIds", () => {
const workspaceId = "/repo/worktree"; const workspaceId = "/repo/worktree";
const visible = makeAgent({ const visible = makeAgent({
id: "visible-agent", id: "visible-agent",
@@ -82,87 +83,46 @@ describe("workspace agent visibility", () => {
workspaceId, workspaceId,
}); });
expect(result.visibleAgents.map((agent) => agent.id)).toEqual(["visible-agent"]); expect(result.activeAgentIds).toEqual(new Set(["visible-agent"]));
expect(result.lookupById.has("visible-agent")).toBe(true); expect(result.knownAgentIds.has("visible-agent")).toBe(true);
expect(result.lookupById.has("archived-agent")).toBe(true); expect(result.knownAgentIds.has("archived-agent")).toBe(true);
expect(result.lookupById.has("other-workspace-agent")).toBe(false); expect(result.knownAgentIds.has("other-workspace-agent")).toBe(false);
}); });
it("allows explicit route open for archived agent once agents are hydrated", () => { it("allows explicit route open for archived agent once agents are hydrated", () => {
const archivedAgent = makeAgent({ const knownAgentIds = new Set(["archived-agent"]);
id: "archived-agent",
cwd: "/repo/worktree",
archivedAt: new Date("2026-03-04T00:01:00.000Z"),
});
const lookup = new Map<string, Agent>([[archivedAgent.id, archivedAgent]]);
expect( expect(
canOpenAgentTabFromRoute({ canOpenAgentTabFromRoute({
agentId: "archived-agent", agentId: "archived-agent",
agentsHydrated: true, agentsHydrated: true,
workspaceAgentLookup: lookup, knownAgentIds,
}) })
).toBe(true); ).toBe(true);
}); });
it("does not prune archived agent tabs when the workspace lookup still contains the agent", () => { it("does not prune archived agent tabs when knownAgentIds contains the agent", () => {
const archivedAgent = makeAgent({ const knownAgentIds = new Set(["archived-agent"]);
id: "archived-agent",
cwd: "/repo/worktree",
archivedAt: new Date("2026-03-04T00:01:00.000Z"),
});
const lookup = new Map<string, Agent>([[archivedAgent.id, archivedAgent]]);
expect( expect(
shouldPruneWorkspaceAgentTab({ shouldPruneWorkspaceAgentTab({
agentId: "archived-agent", agentId: "archived-agent",
agentsHydrated: true, agentsHydrated: true,
workspaceAgentLookup: lookup, knownAgentIds,
}) })
).toBe(false); ).toBe(false);
}); });
it("prunes agent tabs once agents are hydrated and the agent is missing from the workspace lookup", () => { it("prunes agent tabs once agents are hydrated and the agent is missing from knownAgentIds", () => {
expect( expect(
shouldPruneWorkspaceAgentTab({ shouldPruneWorkspaceAgentTab({
agentId: "missing-agent", agentId: "missing-agent",
agentsHydrated: true, agentsHydrated: true,
workspaceAgentLookup: new Map<string, Agent>(), knownAgentIds: new Set<string>(),
}) })
).toBe(true); ).toBe(true);
}); });
it("sorts same-createdAt agents by lastActivityAt descending", () => {
const workspaceId = "/repo/worktree";
const createdAt = new Date("2026-03-04T00:00:00.000Z");
const newerActivity = makeAgent({
id: "newer-activity",
cwd: workspaceId,
createdAt,
lastActivityAt: new Date("2026-03-04T00:05:00.000Z"),
});
const olderActivity = makeAgent({
id: "older-activity",
cwd: workspaceId,
createdAt,
lastActivityAt: new Date("2026-03-04T00:01:00.000Z"),
});
const sessionAgents = new Map<string, Agent>([
[olderActivity.id, olderActivity],
[newerActivity.id, newerActivity],
]);
const result = deriveWorkspaceAgentVisibility({
sessionAgents,
workspaceId,
});
expect(result.visibleAgents.map((agent) => agent.id)).toEqual([
"newer-activity",
"older-activity",
]);
});
it("matches workspace agents when cwd and route workspace differ only by trailing slash", () => { it("matches workspace agents when cwd and route workspace differ only by trailing slash", () => {
const sessionAgents = new Map<string, Agent>([ const sessionAgents = new Map<string, Agent>([
[ [
@@ -179,7 +139,33 @@ describe("workspace agent visibility", () => {
workspaceId: "/Users/moboudra/.paseo/worktrees/1luy0po7/normal-squid", workspaceId: "/Users/moboudra/.paseo/worktrees/1luy0po7/normal-squid",
}); });
expect(result.visibleAgents.map((agent) => agent.id)).toEqual(["slash-agent"]); expect(result.activeAgentIds).toEqual(new Set(["slash-agent"]));
expect(result.lookupById.has("slash-agent")).toBe(true); expect(result.knownAgentIds.has("slash-agent")).toBe(true);
});
describe("workspaceAgentVisibilityEqual", () => {
it("returns true for identical sets", () => {
const a = { activeAgentIds: new Set(["a", "b"]), knownAgentIds: new Set(["a", "b", "c"]) };
const b = { activeAgentIds: new Set(["a", "b"]), knownAgentIds: new Set(["a", "b", "c"]) };
expect(workspaceAgentVisibilityEqual(a, b)).toBe(true);
});
it("returns false when activeAgentIds differ", () => {
const a = { activeAgentIds: new Set(["a"]), knownAgentIds: new Set(["a"]) };
const b = { activeAgentIds: new Set(["b"]), knownAgentIds: new Set(["a"]) };
expect(workspaceAgentVisibilityEqual(a, b)).toBe(false);
});
it("returns false when knownAgentIds differ", () => {
const a = { activeAgentIds: new Set(["a"]), knownAgentIds: new Set(["a"]) };
const b = { activeAgentIds: new Set(["a"]), knownAgentIds: new Set(["a", "b"]) };
expect(workspaceAgentVisibilityEqual(a, b)).toBe(false);
});
it("returns true for empty sets", () => {
const a = { activeAgentIds: new Set<string>(), knownAgentIds: new Set<string>() };
const b = { activeAgentIds: new Set<string>(), knownAgentIds: new Set<string>() };
expect(workspaceAgentVisibilityEqual(a, b)).toBe(true);
});
}); });
}); });
@@ -1,68 +1,66 @@
import type { Agent } from "@/stores/session-store"; import type { Agent } from "@/stores/session-store";
import { normalizeWorkspaceIdentity } from "@/utils/workspace-identity"; import { normalizeWorkspaceIdentity } from "@/utils/workspace-identity";
function sortAgentsByCreatedAtDescending(agents: Agent[]): Agent[] {
const sorted = [...agents];
sorted.sort((left, right) => {
const createdAtDelta = right.createdAt.getTime() - left.createdAt.getTime();
if (createdAtDelta !== 0) {
return createdAtDelta;
}
return right.lastActivityAt.getTime() - left.lastActivityAt.getTime();
});
return sorted;
}
function trimNonEmpty(value: string | null | undefined): string | null {
if (!value) {
return null;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
function normalizeWorkspaceId(value: string | null | undefined): string { function normalizeWorkspaceId(value: string | null | undefined): string {
return normalizeWorkspaceIdentity(value) ?? ""; return normalizeWorkspaceIdentity(value) ?? "";
} }
export interface WorkspaceAgentVisibility {
activeAgentIds: Set<string>;
knownAgentIds: Set<string>;
}
export function deriveWorkspaceAgentVisibility(input: { export function deriveWorkspaceAgentVisibility(input: {
sessionAgents: Map<string, Agent> | undefined; sessionAgents: Map<string, Agent> | undefined;
workspaceId: string; workspaceId: string;
}): { }): WorkspaceAgentVisibility {
visibleAgents: Agent[];
lookupById: Map<string, Agent>;
} {
const { sessionAgents, workspaceId } = input; const { sessionAgents, workspaceId } = input;
const normalizedWorkspaceId = normalizeWorkspaceId(workspaceId); const normalizedWorkspaceId = normalizeWorkspaceId(workspaceId);
if (!sessionAgents || !workspaceId) { if (!sessionAgents || !workspaceId) {
return { return {
visibleAgents: [], activeAgentIds: new Set<string>(),
lookupById: new Map<string, Agent>(), knownAgentIds: new Set<string>(),
}; };
} }
const lookupById = new Map<string, Agent>(); const activeAgentIds = new Set<string>();
const visible: Agent[] = []; const knownAgentIds = new Set<string>();
for (const agent of sessionAgents.values()) { for (const agent of sessionAgents.values()) {
if (normalizeWorkspaceId(agent.cwd) !== normalizedWorkspaceId) { if (normalizeWorkspaceId(agent.cwd) !== normalizedWorkspaceId) {
continue; continue;
} }
lookupById.set(agent.id, agent); knownAgentIds.add(agent.id);
if (!agent.archivedAt) { if (!agent.archivedAt) {
visible.push(agent); activeAgentIds.add(agent.id);
} }
} }
return { return { activeAgentIds, knownAgentIds };
visibleAgents: sortAgentsByCreatedAtDescending(visible), }
lookupById,
}; export function workspaceAgentVisibilityEqual(
a: WorkspaceAgentVisibility,
b: WorkspaceAgentVisibility,
): boolean {
return setsEqual(a.activeAgentIds, b.activeAgentIds) && setsEqual(a.knownAgentIds, b.knownAgentIds);
}
function setsEqual(a: Set<string>, b: Set<string>): boolean {
if (a.size !== b.size) {
return false;
}
for (const item of a) {
if (!b.has(item)) {
return false;
}
}
return true;
} }
export function canOpenAgentTabFromRoute(input: { export function canOpenAgentTabFromRoute(input: {
agentId: string; agentId: string;
agentsHydrated: boolean; agentsHydrated: boolean;
workspaceAgentLookup: Map<string, Agent>; knownAgentIds: Set<string>;
}): boolean { }): boolean {
if (!input.agentId.trim()) { if (!input.agentId.trim()) {
return false; return false;
@@ -70,13 +68,13 @@ export function canOpenAgentTabFromRoute(input: {
if (!input.agentsHydrated) { if (!input.agentsHydrated) {
return true; return true;
} }
return input.workspaceAgentLookup.has(input.agentId); return input.knownAgentIds.has(input.agentId);
} }
export function shouldPruneWorkspaceAgentTab(input: { export function shouldPruneWorkspaceAgentTab(input: {
agentId: string; agentId: string;
agentsHydrated: boolean; agentsHydrated: boolean;
workspaceAgentLookup: Map<string, Agent>; knownAgentIds: Set<string>;
}): boolean { }): boolean {
if (!input.agentId.trim()) { if (!input.agentId.trim()) {
return false; return false;
@@ -84,5 +82,5 @@ export function shouldPruneWorkspaceAgentTab(input: {
if (!input.agentsHydrated) { if (!input.agentsHydrated) {
return false; return false;
} }
return !input.workspaceAgentLookup.has(input.agentId); return !input.knownAgentIds.has(input.agentId);
} }
@@ -1,4 +1,5 @@
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useStoreWithEqualityFn } from "zustand/traditional";
import { useIsFocused } from "@react-navigation/native"; import { useIsFocused } from "@react-navigation/native";
import { import {
ActivityIndicator, ActivityIndicator,
@@ -96,6 +97,7 @@ import {
import { import {
deriveWorkspaceAgentVisibility, deriveWorkspaceAgentVisibility,
shouldPruneWorkspaceAgentTab, shouldPruneWorkspaceAgentTab,
workspaceAgentVisibilityEqual,
} from "@/screens/workspace/workspace-agent-visibility"; } from "@/screens/workspace/workspace-agent-visibility";
import { import {
deriveWorkspacePaneState, deriveWorkspacePaneState,
@@ -582,18 +584,15 @@ function WorkspaceScreenContent({
const client = useHostRuntimeClient(normalizedServerId); const client = useHostRuntimeClient(normalizedServerId);
const isConnected = useHostRuntimeIsConnected(normalizedServerId); const isConnected = useHostRuntimeIsConnected(normalizedServerId);
const sessionAgents = useSessionStore( const workspaceAgentVisibility = useStoreWithEqualityFn(
(state) => state.sessions[normalizedServerId]?.agents useSessionStore,
); (state) =>
const workspaceAgentVisibility = useMemo(
() =>
deriveWorkspaceAgentVisibility({ deriveWorkspaceAgentVisibility({
sessionAgents, sessionAgents: state.sessions[normalizedServerId]?.agents,
workspaceId: normalizedWorkspaceId, workspaceId: normalizedWorkspaceId,
}), }),
[normalizedWorkspaceId, sessionAgents] workspaceAgentVisibilityEqual,
); );
const workspaceAgents = workspaceAgentVisibility.visibleAgents;
const terminalsQueryKey = useMemo( const terminalsQueryKey = useMemo(
() => ["terminals", normalizedServerId, normalizedWorkspaceId] as const, () => ["terminals", normalizedServerId, normalizedWorkspaceId] as const,
@@ -823,8 +822,6 @@ function WorkspaceScreenContent({
return () => handler.remove(); return () => handler.remove();
}, [closeToAgent, isExplorerOpen]); }, [closeToAgent, isExplorerOpen]);
const agentsById = workspaceAgentVisibility.lookupById;
const persistenceKey = useMemo( const persistenceKey = useMemo(
() => () =>
buildWorkspaceTabPersistenceKey({ buildWorkspaceTabPersistenceKey({
@@ -853,7 +850,6 @@ function WorkspaceScreenContent({
const reorderWorkspaceTabsInPane = useWorkspaceLayoutStore((state) => state.reorderTabsInPane); const reorderWorkspaceTabsInPane = useWorkspaceLayoutStore((state) => state.reorderTabsInPane);
const pendingByDraftId = useCreateFlowStore((state) => state.pendingByDraftId); const pendingByDraftId = useCreateFlowStore((state) => state.pendingByDraftId);
const consumedOpenIntentsRef = useRef(new Set<string>()); const consumedOpenIntentsRef = useRef(new Set<string>());
const reopenedArchivedOpenIntentsRef = useRef(new Set<string>());
const pendingCloseTabIdsRef = useRef(new Set<string>()); const pendingCloseTabIdsRef = useRef(new Set<string>());
const [resolvedOpenIntentKey, setResolvedOpenIntentKey] = useState<string | null>(null); const [resolvedOpenIntentKey, setResolvedOpenIntentKey] = useState<string | null>(null);
const currentOpenIntentKey = useMemo( const currentOpenIntentKey = useMemo(
@@ -957,43 +953,6 @@ function WorkspaceScreenContent({
} }
}, [currentOpenIntentKey, resolvedOpenIntentKey]); }, [currentOpenIntentKey, resolvedOpenIntentKey]);
useEffect(() => {
if (!client || !isConnected || openIntent?.kind !== "agent" || !currentOpenIntentKey) {
return;
}
const agentId = openIntent.agentId.trim();
if (!agentId) {
return;
}
if (reopenedArchivedOpenIntentsRef.current.has(currentOpenIntentKey)) {
return;
}
const agent = agentsById.get(agentId) ?? null;
if (!agent?.archivedAt) {
return;
}
reopenedArchivedOpenIntentsRef.current.add(currentOpenIntentKey);
void client.refreshAgent(agentId).catch((error) => {
console.warn("[WorkspaceScreen] Failed to reopen archived agent", {
serverId: normalizedServerId,
workspaceId: normalizedWorkspaceId,
agentId,
error,
});
});
}, [
agentsById,
client,
currentOpenIntentKey,
isConnected,
normalizedServerId,
normalizedWorkspaceId,
openIntent,
]);
useEffect(() => { useEffect(() => {
if (!openIntent || !persistenceKey) { if (!openIntent || !persistenceKey) {
return; return;
@@ -1075,12 +1034,12 @@ function WorkspaceScreenContent({
return pending?.serverId === normalizedServerId && pending.lifecycle === "active"; return pending?.serverId === normalizedServerId && pending.lifecycle === "active";
}); });
for (const agent of workspaceAgents) { for (const agentId of workspaceAgentVisibility.activeAgentIds) {
const representedByTarget = uiTabs.some( const representedByTarget = uiTabs.some(
(tab) => tab.target.kind === "agent" && tab.target.agentId === agent.id (tab) => tab.target.kind === "agent" && tab.target.agentId === agentId
); );
const representedByDeterministicTabId = uiTabs.some( const representedByDeterministicTabId = uiTabs.some(
(tab) => tab.tabId === `agent_${agent.id}` (tab) => tab.tabId === `agent_${agentId}`
); );
if ( if (
hasActivePendingDraftCreateInWorkspace && hasActivePendingDraftCreateInWorkspace &&
@@ -1089,7 +1048,7 @@ function WorkspaceScreenContent({
) { ) {
continue; continue;
} }
ensureWorkspaceTab({ kind: "agent", agentId: agent.id }); ensureWorkspaceTab({ kind: "agent", agentId });
} }
for (const terminal of terminals) { for (const terminal of terminals) {
ensureWorkspaceTab({ kind: "terminal", terminalId: terminal.id }); ensureWorkspaceTab({ kind: "terminal", terminalId: terminal.id });
@@ -1104,7 +1063,7 @@ function WorkspaceScreenContent({
shouldPruneWorkspaceAgentTab({ shouldPruneWorkspaceAgentTab({
agentId: tab.target.agentId, agentId: tab.target.agentId,
agentsHydrated: hasHydratedAgents, agentsHydrated: hasHydratedAgents,
workspaceAgentLookup: agentsById, knownAgentIds: workspaceAgentVisibility.knownAgentIds,
}) })
) { ) {
closeWorkspaceTab(persistenceKey, tab.tabId); closeWorkspaceTab(persistenceKey, tab.tabId);
@@ -1126,7 +1085,7 @@ function WorkspaceScreenContent({
terminals, terminals,
terminalsQuery.isSuccess, terminalsQuery.isSuccess,
uiTabs, uiTabs,
workspaceAgents, workspaceAgentVisibility,
]); ]);
const activeTabId = resolvedPaneTabState.activeTabId; const activeTabId = resolvedPaneTabState.activeTabId;
@@ -1180,7 +1139,7 @@ function WorkspaceScreenContent({
emptyWorkspaceSeedRef.current = null; emptyWorkspaceSeedRef.current = null;
return; return;
} }
if (workspaceAgents.length > 0 || terminals.length > 0) { if (workspaceAgentVisibility.activeAgentIds.size > 0 || terminals.length > 0) {
emptyWorkspaceSeedRef.current = null; emptyWorkspaceSeedRef.current = null;
return; return;
} }
@@ -1202,7 +1161,7 @@ function WorkspaceScreenContent({
persistenceKey, persistenceKey,
terminals.length, terminals.length,
tabs.length, tabs.length,
workspaceAgents.length, workspaceAgentVisibility.activeAgentIds.size,
]); ]);
const handleOpenFileFromExplorer = useCallback( const handleOpenFileFromExplorer = useCallback(
@@ -1485,7 +1444,7 @@ function WorkspaceScreenContent({
const handleCopyResumeCommand = useCallback( const handleCopyResumeCommand = useCallback(
async (agentId: string) => { async (agentId: string) => {
if (!agentId) return; if (!agentId) return;
const agent = sessionAgents?.get(agentId) ?? null; const agent = useSessionStore.getState().sessions[normalizedServerId]?.agents?.get(agentId) ?? null;
const providerSessionId = const providerSessionId =
agent?.runtimeInfo?.sessionId ?? agent?.persistence?.sessionId ?? null; agent?.runtimeInfo?.sessionId ?? agent?.persistence?.sessionId ?? null;
if (!agent || !providerSessionId) { if (!agent || !providerSessionId) {
@@ -1510,7 +1469,7 @@ function WorkspaceScreenContent({
toast.error("Copy failed"); toast.error("Copy failed");
} }
}, },
[sessionAgents, toast] [normalizedServerId, toast]
); );
const handleCopyWorkspacePath = useCallback(async () => { const handleCopyWorkspacePath = useCallback(async () => {