refactor(app): move agent tab visibility policy (#851)

This commit is contained in:
Mohamed Boudra
2026-05-09 23:28:35 +08:00
committed by GitHub
parent bb5ac5b1d0
commit 263886fc1c
5 changed files with 78 additions and 23 deletions
@@ -0,0 +1,368 @@
import { describe, expect, it } from "vitest";
import type { Agent } from "@/stores/session-store";
import {
buildWorkspaceTabSnapshot,
deriveWorkspaceAgentVisibility,
shouldPruneWorkspaceAgentTab,
workspaceAgentVisibilityEqual,
} from "@/workspace-tabs/agent-visibility";
function makeAgent(input: {
id: string;
cwd: string;
parentAgentId?: string | null;
archivedAt?: Date | null;
createdAt?: Date;
lastActivityAt?: Date;
}): Agent {
const createdAt = input.createdAt ?? new Date("2026-03-04T00:00:00.000Z");
const lastActivityAt = input.lastActivityAt ?? createdAt;
return {
serverId: "srv",
id: input.id,
provider: "codex",
status: "idle",
createdAt,
updatedAt: createdAt,
lastUserMessageAt: null,
lastActivityAt,
capabilities: {
supportsStreaming: true,
supportsSessionPersistence: true,
supportsDynamicModes: true,
supportsMcpServers: true,
supportsReasoningStream: true,
supportsToolInvocations: true,
},
currentModeId: null,
availableModes: [],
pendingPermissions: [],
persistence: null,
runtimeInfo: {
provider: "codex",
sessionId: null,
},
title: null,
cwd: input.cwd,
model: null,
thinkingOptionId: null,
parentAgentId: input.parentAgentId ?? null,
labels: {},
requiresAttention: false,
attentionReason: null,
attentionTimestamp: null,
archivedAt: input.archivedAt ?? null,
};
}
describe("workspace agent visibility", () => {
it("keeps subagents active and known while excluding them from auto-open", () => {
const workspaceDirectory = "/repo/worktree";
const parent = makeAgent({
id: "parent-agent",
cwd: workspaceDirectory,
});
const child = makeAgent({
id: "child-agent",
cwd: workspaceDirectory,
parentAgentId: "parent-agent",
});
const result = deriveWorkspaceAgentVisibility({
sessionAgents: new Map<string, Agent>([
[parent.id, parent],
[child.id, child],
]),
workspaceDirectory,
});
expect(result.activeAgentIds).toEqual(new Set(["parent-agent", "child-agent"]));
expect(result.autoOpenAgentIds).toEqual(new Set(["parent-agent"]));
expect(result.knownAgentIds).toEqual(new Set(["parent-agent", "child-agent"]));
});
it("keeps archived subagents known but excludes them from active and auto-open", () => {
const workspaceDirectory = "/repo/worktree";
const archivedChild = makeAgent({
id: "archived-child",
cwd: workspaceDirectory,
parentAgentId: "parent-agent",
archivedAt: new Date("2026-03-04T00:01:00.000Z"),
});
const result = deriveWorkspaceAgentVisibility({
sessionAgents: new Map<string, Agent>([[archivedChild.id, archivedChild]]),
workspaceDirectory,
});
expect(result.activeAgentIds).toEqual(new Set<string>());
expect(result.autoOpenAgentIds).toEqual(new Set<string>());
expect(result.knownAgentIds).toEqual(new Set(["archived-child"]));
});
it("excludes a child from auto-open even when its snapshot arrives before the parent", () => {
const workspaceDirectory = "/repo/worktree";
const child = makeAgent({
id: "child-agent",
cwd: workspaceDirectory,
parentAgentId: "parent-agent",
});
const parent = makeAgent({
id: "parent-agent",
cwd: workspaceDirectory,
});
const result = deriveWorkspaceAgentVisibility({
sessionAgents: new Map<string, Agent>([
[child.id, child],
[parent.id, parent],
]),
workspaceDirectory,
});
expect(result.activeAgentIds).toEqual(new Set(["child-agent", "parent-agent"]));
expect(result.autoOpenAgentIds).toEqual(new Set(["parent-agent"]));
expect(result.knownAgentIds).toEqual(new Set(["child-agent", "parent-agent"]));
});
it("keeps archived agents out of activeAgentIds but present in knownAgentIds", () => {
const workspaceDirectory = "/repo/worktree";
const visible = makeAgent({
id: "visible-agent",
cwd: workspaceDirectory,
createdAt: new Date("2026-03-04T00:00:00.000Z"),
});
const archived = makeAgent({
id: "archived-agent",
cwd: workspaceDirectory,
archivedAt: new Date("2026-03-04T00:01:00.000Z"),
createdAt: new Date("2026-03-04T00:01:00.000Z"),
});
const otherWorkspace = makeAgent({
id: "other-workspace-agent",
cwd: "/repo/other",
});
const sessionAgents = new Map<string, Agent>([
[visible.id, visible],
[archived.id, archived],
[otherWorkspace.id, otherWorkspace],
]);
const result = deriveWorkspaceAgentVisibility({
sessionAgents,
workspaceDirectory,
});
expect(result.activeAgentIds).toEqual(new Set(["visible-agent"]));
expect(result.autoOpenAgentIds).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("treats lazy historical details as known without making them active", () => {
const workspaceDirectory = "/repo/worktree";
const active = makeAgent({ id: "active-agent", cwd: workspaceDirectory });
const historicalDetail = makeAgent({
id: "historical-agent",
cwd: workspaceDirectory,
archivedAt: new Date("2026-03-04T00:01:00.000Z"),
});
const result = deriveWorkspaceAgentVisibility({
sessionAgents: new Map([[active.id, active]]),
agentDetails: new Map([[historicalDetail.id, historicalDetail]]),
workspaceDirectory,
});
expect(result.activeAgentIds).toEqual(new Set(["active-agent"]));
expect(result.knownAgentIds).toEqual(new Set(["active-agent", "historical-agent"]));
});
it("prunes archived agent tabs so archiving on one client closes tabs on all clients", () => {
const activeAgentIds = new Set<string>();
expect(
shouldPruneWorkspaceAgentTab({
agentId: "archived-agent",
agentsHydrated: true,
activeAgentIds,
}),
).toBe(true);
});
it("prunes pinned archived agent tabs because archive state is authoritative", () => {
expect(
shouldPruneWorkspaceAgentTab({
agentId: "archived-agent",
agentsHydrated: true,
activeAgentIds: new Set<string>(),
}),
).toBe(true);
});
it("does not prune active agent tabs", () => {
const activeAgentIds = new Set(["active-agent"]);
expect(
shouldPruneWorkspaceAgentTab({
agentId: "active-agent",
agentsHydrated: true,
activeAgentIds,
}),
).toBe(false);
});
it("prunes agent tabs once agents are hydrated and the agent is missing from activeAgentIds", () => {
expect(
shouldPruneWorkspaceAgentTab({
agentId: "missing-agent",
agentsHydrated: true,
activeAgentIds: new Set<string>(),
}),
).toBe(true);
});
it("matches workspace agents when cwd and route workspace differ only by trailing slash", () => {
const sessionAgents = new Map<string, Agent>([
[
"slash-agent",
makeAgent({
id: "slash-agent",
cwd: "/Users/moboudra/.paseo/worktrees/1luy0po7/normal-squid/",
}),
],
]);
const result = deriveWorkspaceAgentVisibility({
sessionAgents,
workspaceDirectory: "/Users/moboudra/.paseo/worktrees/1luy0po7/normal-squid",
});
expect(result.activeAgentIds).toEqual(new Set(["slash-agent"]));
expect(result.autoOpenAgentIds).toEqual(new Set(["slash-agent"]));
expect(result.knownAgentIds.has("slash-agent")).toBe(true);
});
it("matches workspace agents using the workspace directory even when the route uses a numeric workspace id", () => {
const sessionAgents = new Map<string, Agent>([
[
"recent-agent",
makeAgent({
id: "recent-agent",
cwd: "/tmp/workspace-lifecycle-main",
}),
],
]);
const result = deriveWorkspaceAgentVisibility({
sessionAgents,
workspaceDirectory: "/tmp/workspace-lifecycle-main",
});
expect(result.activeAgentIds).toEqual(new Set(["recent-agent"]));
expect(result.autoOpenAgentIds).toEqual(new Set(["recent-agent"]));
expect(result.knownAgentIds).toEqual(new Set(["recent-agent"]));
});
it("builds the tab reconciliation snapshot without callers unpacking agent visibility", () => {
const agentVisibility = {
activeAgentIds: new Set(["active-agent"]),
autoOpenAgentIds: new Set(["root-agent"]),
knownAgentIds: new Set(["active-agent", "archived-agent"]),
};
expect(
buildWorkspaceTabSnapshot({
agentVisibility,
agentsHydrated: true,
terminalsHydrated: true,
knownTerminalIds: ["terminal-1", "script-terminal"],
standaloneTerminalIds: ["terminal-1"],
hasActivePendingDraftCreate: false,
}),
).toEqual({
agentsHydrated: true,
terminalsHydrated: true,
activeAgentIds: agentVisibility.activeAgentIds,
autoOpenAgentIds: agentVisibility.autoOpenAgentIds,
knownAgentIds: agentVisibility.knownAgentIds,
knownTerminalIds: ["terminal-1", "script-terminal"],
standaloneTerminalIds: ["terminal-1"],
hasActivePendingDraftCreate: false,
});
});
describe("workspaceAgentVisibilityEqual", () => {
it("returns true for identical sets", () => {
const a = {
activeAgentIds: new Set(["a", "b"]),
autoOpenAgentIds: new Set(["a"]),
knownAgentIds: new Set(["a", "b", "c"]),
};
const b = {
activeAgentIds: new Set(["a", "b"]),
autoOpenAgentIds: new Set(["a"]),
knownAgentIds: new Set(["a", "b", "c"]),
};
expect(workspaceAgentVisibilityEqual(a, b)).toBe(true);
});
it("returns false when activeAgentIds differ", () => {
const a = {
activeAgentIds: new Set(["a"]),
autoOpenAgentIds: new Set(["a"]),
knownAgentIds: new Set(["a"]),
};
const b = {
activeAgentIds: new Set(["b"]),
autoOpenAgentIds: new Set(["a"]),
knownAgentIds: new Set(["a"]),
};
expect(workspaceAgentVisibilityEqual(a, b)).toBe(false);
});
it("returns false when autoOpenAgentIds differ", () => {
const a = {
activeAgentIds: new Set(["a", "b"]),
autoOpenAgentIds: new Set(["a"]),
knownAgentIds: new Set(["a", "b"]),
};
const b = {
activeAgentIds: new Set(["a", "b"]),
autoOpenAgentIds: new Set(["b"]),
knownAgentIds: new Set(["a", "b"]),
};
expect(workspaceAgentVisibilityEqual(a, b)).toBe(false);
});
it("returns false when knownAgentIds differ", () => {
const a = {
activeAgentIds: new Set(["a"]),
autoOpenAgentIds: new Set(["a"]),
knownAgentIds: new Set(["a"]),
};
const b = {
activeAgentIds: new Set(["a"]),
autoOpenAgentIds: 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>(),
autoOpenAgentIds: new Set<string>(),
knownAgentIds: new Set<string>(),
};
const b = {
activeAgentIds: new Set<string>(),
autoOpenAgentIds: new Set<string>(),
knownAgentIds: new Set<string>(),
};
expect(workspaceAgentVisibilityEqual(a, b)).toBe(true);
});
});
});
@@ -0,0 +1,113 @@
import type { Agent } from "@/stores/session-store";
import type { WorkspaceTabSnapshot } from "@/stores/workspace-layout-actions";
import { shouldAutoOpenAgentTab } from "@/subagents/policies";
import { normalizeWorkspacePath } from "@/utils/workspace-identity";
function normalizeWorkspaceId(value: string | null | undefined): string {
return normalizeWorkspacePath(value) ?? "";
}
export interface WorkspaceAgentVisibility {
activeAgentIds: Set<string>;
autoOpenAgentIds: Set<string>;
knownAgentIds: Set<string>;
}
export function deriveWorkspaceAgentVisibility(input: {
sessionAgents: Map<string, Agent> | undefined;
agentDetails?: Map<string, Agent> | undefined;
workspaceDirectory: string | null | undefined;
}): WorkspaceAgentVisibility {
const { sessionAgents, agentDetails, workspaceDirectory } = input;
const normalizedWorkspaceDirectory = normalizeWorkspaceId(workspaceDirectory);
if ((!sessionAgents && !agentDetails) || !normalizedWorkspaceDirectory) {
return {
activeAgentIds: new Set<string>(),
autoOpenAgentIds: new Set<string>(),
knownAgentIds: new Set<string>(),
};
}
const activeAgentIds = new Set<string>();
const autoOpenAgentIds = new Set<string>();
const knownAgentIds = new Set<string>();
for (const agent of sessionAgents?.values() ?? []) {
if (normalizeWorkspaceId(agent.cwd) !== normalizedWorkspaceDirectory) {
continue;
}
knownAgentIds.add(agent.id);
if (!agent.archivedAt) {
activeAgentIds.add(agent.id);
if (shouldAutoOpenAgentTab(agent)) {
autoOpenAgentIds.add(agent.id);
}
}
}
for (const agent of agentDetails?.values() ?? []) {
if (normalizeWorkspaceId(agent.cwd) !== normalizedWorkspaceDirectory) {
continue;
}
knownAgentIds.add(agent.id);
}
return { activeAgentIds, autoOpenAgentIds, knownAgentIds };
}
export function buildWorkspaceTabSnapshot(input: {
agentVisibility: WorkspaceAgentVisibility;
agentsHydrated: boolean;
terminalsHydrated: boolean;
knownTerminalIds: Iterable<string>;
standaloneTerminalIds: Iterable<string>;
hasActivePendingDraftCreate: boolean;
}): WorkspaceTabSnapshot {
return {
agentsHydrated: input.agentsHydrated,
terminalsHydrated: input.terminalsHydrated,
activeAgentIds: input.agentVisibility.activeAgentIds,
autoOpenAgentIds: input.agentVisibility.autoOpenAgentIds,
knownAgentIds: input.agentVisibility.knownAgentIds,
knownTerminalIds: input.knownTerminalIds,
standaloneTerminalIds: input.standaloneTerminalIds,
hasActivePendingDraftCreate: input.hasActivePendingDraftCreate,
};
}
export function workspaceAgentVisibilityEqual(
a: WorkspaceAgentVisibility,
b: WorkspaceAgentVisibility,
): boolean {
return (
setsEqual(a.activeAgentIds, b.activeAgentIds) &&
setsEqual(a.autoOpenAgentIds, b.autoOpenAgentIds) &&
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;
}
// Prune agent tabs that are no longer active once agents are hydrated.
// Archived agents get pruned so that archiving on one client closes the tab on all clients.
export function shouldPruneWorkspaceAgentTab(input: {
agentId: string;
agentsHydrated: boolean;
activeAgentIds: Set<string>;
}): boolean {
if (!input.agentId.trim()) {
return false;
}
if (!input.agentsHydrated) {
return false;
}
return !input.activeAgentIds.has(input.agentId);
}