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>
</View>
{agentId && !isArchivingCurrentAgent ? (
{agentId && !isArchivingCurrentAgent && !agent?.archivedAt ? (
<AgentInputArea
agentId={agentId}
serverId={serverId}
@@ -4,6 +4,7 @@ import {
canOpenAgentTabFromRoute,
deriveWorkspaceAgentVisibility,
shouldPruneWorkspaceAgentTab,
workspaceAgentVisibilityEqual,
} from "@/screens/workspace/workspace-agent-visibility";
function makeAgent(input: {
@@ -53,7 +54,7 @@ function makeAgent(input: {
}
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 visible = makeAgent({
id: "visible-agent",
@@ -82,87 +83,46 @@ describe("workspace agent visibility", () => {
workspaceId,
});
expect(result.visibleAgents.map((agent) => agent.id)).toEqual(["visible-agent"]);
expect(result.lookupById.has("visible-agent")).toBe(true);
expect(result.lookupById.has("archived-agent")).toBe(true);
expect(result.lookupById.has("other-workspace-agent")).toBe(false);
expect(result.activeAgentIds).toEqual(new Set(["visible-agent"]));
expect(result.knownAgentIds.has("visible-agent")).toBe(true);
expect(result.knownAgentIds.has("archived-agent")).toBe(true);
expect(result.knownAgentIds.has("other-workspace-agent")).toBe(false);
});
it("allows explicit route open for archived agent once agents are hydrated", () => {
const archivedAgent = makeAgent({
id: "archived-agent",
cwd: "/repo/worktree",
archivedAt: new Date("2026-03-04T00:01:00.000Z"),
});
const lookup = new Map<string, Agent>([[archivedAgent.id, archivedAgent]]);
const knownAgentIds = new Set(["archived-agent"]);
expect(
canOpenAgentTabFromRoute({
agentId: "archived-agent",
agentsHydrated: true,
workspaceAgentLookup: lookup,
knownAgentIds,
})
).toBe(true);
});
it("does not prune archived agent tabs when the workspace lookup still contains the agent", () => {
const archivedAgent = makeAgent({
id: "archived-agent",
cwd: "/repo/worktree",
archivedAt: new Date("2026-03-04T00:01:00.000Z"),
});
const lookup = new Map<string, Agent>([[archivedAgent.id, archivedAgent]]);
it("does not prune archived agent tabs when knownAgentIds contains the agent", () => {
const knownAgentIds = new Set(["archived-agent"]);
expect(
shouldPruneWorkspaceAgentTab({
agentId: "archived-agent",
agentsHydrated: true,
workspaceAgentLookup: lookup,
knownAgentIds,
})
).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(
shouldPruneWorkspaceAgentTab({
agentId: "missing-agent",
agentsHydrated: true,
workspaceAgentLookup: new Map<string, Agent>(),
knownAgentIds: new Set<string>(),
})
).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", () => {
const sessionAgents = new Map<string, Agent>([
[
@@ -179,7 +139,33 @@ describe("workspace agent visibility", () => {
workspaceId: "/Users/moboudra/.paseo/worktrees/1luy0po7/normal-squid",
});
expect(result.visibleAgents.map((agent) => agent.id)).toEqual(["slash-agent"]);
expect(result.lookupById.has("slash-agent")).toBe(true);
expect(result.activeAgentIds).toEqual(new Set(["slash-agent"]));
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 { 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 {
return normalizeWorkspaceIdentity(value) ?? "";
}
export interface WorkspaceAgentVisibility {
activeAgentIds: Set<string>;
knownAgentIds: Set<string>;
}
export function deriveWorkspaceAgentVisibility(input: {
sessionAgents: Map<string, Agent> | undefined;
workspaceId: string;
}): {
visibleAgents: Agent[];
lookupById: Map<string, Agent>;
} {
}): WorkspaceAgentVisibility {
const { sessionAgents, workspaceId } = input;
const normalizedWorkspaceId = normalizeWorkspaceId(workspaceId);
if (!sessionAgents || !workspaceId) {
return {
visibleAgents: [],
lookupById: new Map<string, Agent>(),
activeAgentIds: new Set<string>(),
knownAgentIds: new Set<string>(),
};
}
const lookupById = new Map<string, Agent>();
const visible: Agent[] = [];
const activeAgentIds = new Set<string>();
const knownAgentIds = new Set<string>();
for (const agent of sessionAgents.values()) {
if (normalizeWorkspaceId(agent.cwd) !== normalizedWorkspaceId) {
continue;
}
lookupById.set(agent.id, agent);
knownAgentIds.add(agent.id);
if (!agent.archivedAt) {
visible.push(agent);
activeAgentIds.add(agent.id);
}
}
return {
visibleAgents: sortAgentsByCreatedAtDescending(visible),
lookupById,
};
return { activeAgentIds, knownAgentIds };
}
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: {
agentId: string;
agentsHydrated: boolean;
workspaceAgentLookup: Map<string, Agent>;
knownAgentIds: Set<string>;
}): boolean {
if (!input.agentId.trim()) {
return false;
@@ -70,13 +68,13 @@ export function canOpenAgentTabFromRoute(input: {
if (!input.agentsHydrated) {
return true;
}
return input.workspaceAgentLookup.has(input.agentId);
return input.knownAgentIds.has(input.agentId);
}
export function shouldPruneWorkspaceAgentTab(input: {
agentId: string;
agentsHydrated: boolean;
workspaceAgentLookup: Map<string, Agent>;
knownAgentIds: Set<string>;
}): boolean {
if (!input.agentId.trim()) {
return false;
@@ -84,5 +82,5 @@ export function shouldPruneWorkspaceAgentTab(input: {
if (!input.agentsHydrated) {
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 { useStoreWithEqualityFn } from "zustand/traditional";
import { useIsFocused } from "@react-navigation/native";
import {
ActivityIndicator,
@@ -96,6 +97,7 @@ import {
import {
deriveWorkspaceAgentVisibility,
shouldPruneWorkspaceAgentTab,
workspaceAgentVisibilityEqual,
} from "@/screens/workspace/workspace-agent-visibility";
import {
deriveWorkspacePaneState,
@@ -582,18 +584,15 @@ function WorkspaceScreenContent({
const client = useHostRuntimeClient(normalizedServerId);
const isConnected = useHostRuntimeIsConnected(normalizedServerId);
const sessionAgents = useSessionStore(
(state) => state.sessions[normalizedServerId]?.agents
);
const workspaceAgentVisibility = useMemo(
() =>
const workspaceAgentVisibility = useStoreWithEqualityFn(
useSessionStore,
(state) =>
deriveWorkspaceAgentVisibility({
sessionAgents,
sessionAgents: state.sessions[normalizedServerId]?.agents,
workspaceId: normalizedWorkspaceId,
}),
[normalizedWorkspaceId, sessionAgents]
workspaceAgentVisibilityEqual,
);
const workspaceAgents = workspaceAgentVisibility.visibleAgents;
const terminalsQueryKey = useMemo(
() => ["terminals", normalizedServerId, normalizedWorkspaceId] as const,
@@ -823,8 +822,6 @@ function WorkspaceScreenContent({
return () => handler.remove();
}, [closeToAgent, isExplorerOpen]);
const agentsById = workspaceAgentVisibility.lookupById;
const persistenceKey = useMemo(
() =>
buildWorkspaceTabPersistenceKey({
@@ -853,7 +850,6 @@ function WorkspaceScreenContent({
const reorderWorkspaceTabsInPane = useWorkspaceLayoutStore((state) => state.reorderTabsInPane);
const pendingByDraftId = useCreateFlowStore((state) => state.pendingByDraftId);
const consumedOpenIntentsRef = useRef(new Set<string>());
const reopenedArchivedOpenIntentsRef = useRef(new Set<string>());
const pendingCloseTabIdsRef = useRef(new Set<string>());
const [resolvedOpenIntentKey, setResolvedOpenIntentKey] = useState<string | null>(null);
const currentOpenIntentKey = useMemo(
@@ -957,43 +953,6 @@ function WorkspaceScreenContent({
}
}, [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(() => {
if (!openIntent || !persistenceKey) {
return;
@@ -1075,12 +1034,12 @@ function WorkspaceScreenContent({
return pending?.serverId === normalizedServerId && pending.lifecycle === "active";
});
for (const agent of workspaceAgents) {
for (const agentId of workspaceAgentVisibility.activeAgentIds) {
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(
(tab) => tab.tabId === `agent_${agent.id}`
(tab) => tab.tabId === `agent_${agentId}`
);
if (
hasActivePendingDraftCreateInWorkspace &&
@@ -1089,7 +1048,7 @@ function WorkspaceScreenContent({
) {
continue;
}
ensureWorkspaceTab({ kind: "agent", agentId: agent.id });
ensureWorkspaceTab({ kind: "agent", agentId });
}
for (const terminal of terminals) {
ensureWorkspaceTab({ kind: "terminal", terminalId: terminal.id });
@@ -1104,7 +1063,7 @@ function WorkspaceScreenContent({
shouldPruneWorkspaceAgentTab({
agentId: tab.target.agentId,
agentsHydrated: hasHydratedAgents,
workspaceAgentLookup: agentsById,
knownAgentIds: workspaceAgentVisibility.knownAgentIds,
})
) {
closeWorkspaceTab(persistenceKey, tab.tabId);
@@ -1126,7 +1085,7 @@ function WorkspaceScreenContent({
terminals,
terminalsQuery.isSuccess,
uiTabs,
workspaceAgents,
workspaceAgentVisibility,
]);
const activeTabId = resolvedPaneTabState.activeTabId;
@@ -1180,7 +1139,7 @@ function WorkspaceScreenContent({
emptyWorkspaceSeedRef.current = null;
return;
}
if (workspaceAgents.length > 0 || terminals.length > 0) {
if (workspaceAgentVisibility.activeAgentIds.size > 0 || terminals.length > 0) {
emptyWorkspaceSeedRef.current = null;
return;
}
@@ -1202,7 +1161,7 @@ function WorkspaceScreenContent({
persistenceKey,
terminals.length,
tabs.length,
workspaceAgents.length,
workspaceAgentVisibility.activeAgentIds.size,
]);
const handleOpenFileFromExplorer = useCallback(
@@ -1485,7 +1444,7 @@ function WorkspaceScreenContent({
const handleCopyResumeCommand = useCallback(
async (agentId: string) => {
if (!agentId) return;
const agent = sessionAgents?.get(agentId) ?? null;
const agent = useSessionStore.getState().sessions[normalizedServerId]?.agents?.get(agentId) ?? null;
const providerSessionId =
agent?.runtimeInfo?.sessionId ?? agent?.persistence?.sessionId ?? null;
if (!agent || !providerSessionId) {
@@ -1510,7 +1469,7 @@ function WorkspaceScreenContent({
toast.error("Copy failed");
}
},
[sessionAgents, toast]
[normalizedServerId, toast]
);
const handleCopyWorkspacePath = useCallback(async () => {