Preserve ambient placement for reconciliation and background opens while routing user-directed tab creation through the focused visible pane. Defer terminal reconciliation during creates so a push cannot preempt the intended pane placement.
316 lines
9.4 KiB
TypeScript
316 lines
9.4 KiB
TypeScript
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
buildWorkspaceTabSnapshot,
|
|
deriveWorkspaceAgentVisibility,
|
|
type WorkspaceAgentVisibility,
|
|
} from "@/workspace-tabs/agent-visibility";
|
|
import { buildWorkspaceTabPersistenceKey } from "@/workspace-tabs/model";
|
|
import { selectSubagentsForParent } from "@/subagents/select";
|
|
import { findPaneById, findPaneContainingTab } from "./workspace-layout-actions";
|
|
import { useWorkspaceLayoutStore } from "./workspace-layout-store";
|
|
import { useSessionStore, type Agent } from "./session-store";
|
|
|
|
vi.mock("@react-native-async-storage/async-storage", () => {
|
|
const storage = new Map<string, string>();
|
|
return {
|
|
default: {
|
|
getItem: vi.fn(async (key: string) => storage.get(key) ?? null),
|
|
setItem: vi.fn(async (key: string, value: string) => {
|
|
storage.set(key, value);
|
|
}),
|
|
removeItem: vi.fn(async (key: string) => {
|
|
storage.delete(key);
|
|
}),
|
|
},
|
|
};
|
|
});
|
|
|
|
const SERVER_ID = "server-1";
|
|
const WORKSPACE_ID = "ws-main";
|
|
const WORKSPACE_DIRECTORY = "/repo/worktree";
|
|
|
|
const AGENT_TIMESTAMP = new Date("2026-04-21T10:00:00.000Z");
|
|
|
|
const AGENT_DEFAULTS: Agent = {
|
|
serverId: SERVER_ID,
|
|
id: "agent",
|
|
provider: "codex",
|
|
status: "idle",
|
|
activeTurn: null,
|
|
createdAt: AGENT_TIMESTAMP,
|
|
updatedAt: AGENT_TIMESTAMP,
|
|
lastUserMessageAt: null,
|
|
lastActivityAt: AGENT_TIMESTAMP,
|
|
capabilities: {
|
|
supportsStreaming: true,
|
|
supportsSessionPersistence: true,
|
|
supportsDynamicModes: true,
|
|
supportsMcpServers: true,
|
|
supportsReasoningStream: true,
|
|
supportsToolInvocations: true,
|
|
},
|
|
currentModeId: null,
|
|
availableModes: [],
|
|
pendingPermissions: [],
|
|
persistence: null,
|
|
runtimeInfo: undefined,
|
|
lastUsage: undefined,
|
|
lastError: null,
|
|
title: "Agent",
|
|
cwd: WORKSPACE_DIRECTORY,
|
|
workspaceId: WORKSPACE_ID,
|
|
model: null,
|
|
features: undefined,
|
|
thinkingOptionId: undefined,
|
|
requiresAttention: false,
|
|
attentionReason: null,
|
|
attentionTimestamp: null,
|
|
archivedAt: null,
|
|
parentAgentId: null,
|
|
labels: {},
|
|
projectPlacement: null,
|
|
};
|
|
|
|
function makeAgent(input: Partial<Agent> & Pick<Agent, "id">): Agent {
|
|
return { ...AGENT_DEFAULTS, ...input };
|
|
}
|
|
|
|
function initializeAgents(agents: Agent[]): void {
|
|
useSessionStore.getState().initializeSession(SERVER_ID, null as unknown as DaemonClient);
|
|
useSessionStore
|
|
.getState()
|
|
.setAgents(SERVER_ID, new Map(agents.map((agent) => [agent.id, agent])));
|
|
}
|
|
|
|
function appendAgent(agent: Agent): void {
|
|
useSessionStore.getState().setAgents(SERVER_ID, (agents) => {
|
|
const nextAgents = new Map(agents);
|
|
nextAgents.set(agent.id, agent);
|
|
return nextAgents;
|
|
});
|
|
}
|
|
|
|
function deriveVisibilityFromSession(): WorkspaceAgentVisibility {
|
|
const sessionAgents = useSessionStore.getState().sessions[SERVER_ID]?.agents ?? new Map();
|
|
return deriveWorkspaceAgentVisibility({
|
|
sessionAgents,
|
|
workspaceId: WORKSPACE_ID,
|
|
});
|
|
}
|
|
|
|
function reconcileWorkspaceTabs(workspaceKey: string, visibility: WorkspaceAgentVisibility): void {
|
|
useWorkspaceLayoutStore.getState().reconcileTabs(
|
|
workspaceKey,
|
|
buildWorkspaceTabSnapshot({
|
|
agentVisibility: visibility,
|
|
agentsHydrated: true,
|
|
terminalsHydrated: true,
|
|
knownTerminalIds: [],
|
|
standaloneTerminalIds: [],
|
|
hasActivePendingTerminalCreate: false,
|
|
hasActivePendingDraftCreate: false,
|
|
}),
|
|
);
|
|
}
|
|
|
|
function getWorkspaceTabIds(workspaceKey: string): string[] {
|
|
return useWorkspaceLayoutStore
|
|
.getState()
|
|
.getWorkspaceTabs(workspaceKey)
|
|
.map((tab) => tab.tabId);
|
|
}
|
|
|
|
afterEach(() => {
|
|
useSessionStore.getState().clearSession(SERVER_ID);
|
|
useWorkspaceLayoutStore.setState({
|
|
layoutByWorkspace: {},
|
|
splitSizesByWorkspace: {},
|
|
pinnedAgentIdsByWorkspace: {},
|
|
hiddenAgentIdsByWorkspace: {},
|
|
});
|
|
});
|
|
|
|
describe("workspace subagents integration", () => {
|
|
it("keeps a child ingested before its parent out of auto-tabs, then exposes it in the parent section", () => {
|
|
const workspaceKey = buildWorkspaceTabPersistenceKey({
|
|
serverId: SERVER_ID,
|
|
workspaceId: WORKSPACE_ID,
|
|
});
|
|
expect(workspaceKey).toBeTruthy();
|
|
|
|
const child = makeAgent({
|
|
id: "child-agent",
|
|
parentAgentId: "parent-agent",
|
|
title: "Child agent",
|
|
});
|
|
const parent = makeAgent({
|
|
id: "parent-agent",
|
|
title: "Parent agent",
|
|
});
|
|
|
|
initializeAgents([child]);
|
|
|
|
reconcileWorkspaceTabs(workspaceKey!, deriveVisibilityFromSession());
|
|
|
|
expect(getWorkspaceTabIds(workspaceKey!)).toEqual([]);
|
|
|
|
appendAgent(parent);
|
|
|
|
reconcileWorkspaceTabs(workspaceKey!, deriveVisibilityFromSession());
|
|
|
|
expect(getWorkspaceTabIds(workspaceKey!)).toEqual(["agent_parent-agent"]);
|
|
expect(
|
|
selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "parent-agent",
|
|
},
|
|
new Set(),
|
|
).map((row) => row.id),
|
|
).toEqual(["child-agent"]);
|
|
});
|
|
|
|
it("moves a detached child out of the parent section and back into normal workspace tabs", () => {
|
|
const workspaceKey = buildWorkspaceTabPersistenceKey({
|
|
serverId: SERVER_ID,
|
|
workspaceId: WORKSPACE_ID,
|
|
});
|
|
expect(workspaceKey).toBeTruthy();
|
|
|
|
const parent = makeAgent({
|
|
id: "parent-agent",
|
|
title: "Parent agent",
|
|
});
|
|
const child = makeAgent({
|
|
id: "child-agent",
|
|
parentAgentId: "parent-agent",
|
|
title: "Child agent",
|
|
});
|
|
|
|
initializeAgents([parent, child]);
|
|
reconcileWorkspaceTabs(workspaceKey!, deriveVisibilityFromSession());
|
|
|
|
expect(getWorkspaceTabIds(workspaceKey!)).toEqual(["agent_parent-agent"]);
|
|
expect(
|
|
selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "parent-agent",
|
|
},
|
|
new Set(),
|
|
).map((row) => row.id),
|
|
).toEqual(["child-agent"]);
|
|
|
|
appendAgent({ ...child, parentAgentId: null, labels: {} });
|
|
reconcileWorkspaceTabs(workspaceKey!, deriveVisibilityFromSession());
|
|
|
|
expect(getWorkspaceTabIds(workspaceKey!)).toEqual(["agent_parent-agent", "agent_child-agent"]);
|
|
expect(
|
|
selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "parent-agent",
|
|
},
|
|
new Set(),
|
|
),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it("auto-opens a cross-workspace child while retaining it in the parent section", () => {
|
|
const workspaceKey = buildWorkspaceTabPersistenceKey({
|
|
serverId: SERVER_ID,
|
|
workspaceId: WORKSPACE_ID,
|
|
});
|
|
expect(workspaceKey).toBeTruthy();
|
|
|
|
const parent = makeAgent({
|
|
id: "parent-agent",
|
|
workspaceId: "ws-parent",
|
|
title: "Parent agent",
|
|
});
|
|
const child = makeAgent({
|
|
id: "child-agent",
|
|
parentAgentId: parent.id,
|
|
title: "Cross-workspace child",
|
|
});
|
|
|
|
initializeAgents([parent, child]);
|
|
reconcileWorkspaceTabs(workspaceKey!, deriveVisibilityFromSession());
|
|
|
|
expect(getWorkspaceTabIds(workspaceKey!)).toEqual(["agent_child-agent"]);
|
|
expect(
|
|
selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: parent.id,
|
|
},
|
|
new Set(),
|
|
).map((row) => row.id),
|
|
).toEqual([child.id]);
|
|
});
|
|
|
|
it("opens a subagent in the explorer pane when focused", () => {
|
|
const workspaceKey = buildWorkspaceTabPersistenceKey({
|
|
serverId: SERVER_ID,
|
|
workspaceId: WORKSPACE_ID,
|
|
});
|
|
expect(workspaceKey).toBeTruthy();
|
|
|
|
const parent = makeAgent({
|
|
id: "parent-agent",
|
|
title: "Parent agent",
|
|
});
|
|
const child = makeAgent({
|
|
id: "child-agent",
|
|
parentAgentId: parent.id,
|
|
title: "Child agent",
|
|
});
|
|
|
|
initializeAgents([parent, child]);
|
|
reconcileWorkspaceTabs(workspaceKey!, deriveVisibilityFromSession());
|
|
|
|
const store = useWorkspaceLayoutStore.getState();
|
|
const tabId = store.openTabInExplorerPaneFocused(workspaceKey!, {
|
|
target: { kind: "agent", agentId: child.id },
|
|
parentTabId: `agent_${parent.id}`,
|
|
});
|
|
|
|
const state = useWorkspaceLayoutStore.getState();
|
|
const layout = state.layoutByWorkspace[workspaceKey!];
|
|
const explorerPaneId = state.explorerPaneIdByWorkspace[workspaceKey!];
|
|
|
|
expect(explorerPaneId).toBeTruthy();
|
|
expect(findPaneContainingTab(layout.root, tabId!)?.id).toBe(explorerPaneId);
|
|
expect(findPaneById(layout.root, explorerPaneId!)?.hidden).toBeUndefined();
|
|
expect(layout.focusedPaneId).toBe(explorerPaneId);
|
|
});
|
|
|
|
it("opens a provider subagent in the explorer pane when focused", () => {
|
|
const workspaceKey = buildWorkspaceTabPersistenceKey({
|
|
serverId: SERVER_ID,
|
|
workspaceId: WORKSPACE_ID,
|
|
});
|
|
expect(workspaceKey).toBeTruthy();
|
|
|
|
const store = useWorkspaceLayoutStore.getState();
|
|
const tabId = store.openTabInExplorerPaneFocused(workspaceKey!, {
|
|
target: { kind: "provider_subagent", parentAgentId: "parent-agent", subagentId: "task-1" },
|
|
parentTabId: "agent_parent-agent",
|
|
});
|
|
|
|
const state = useWorkspaceLayoutStore.getState();
|
|
const layout = state.layoutByWorkspace[workspaceKey!];
|
|
const explorerPaneId = state.explorerPaneIdByWorkspace[workspaceKey!];
|
|
|
|
expect(explorerPaneId).toBeTruthy();
|
|
expect(findPaneContainingTab(layout.root, tabId!)?.id).toBe(explorerPaneId);
|
|
expect(findPaneById(layout.root, explorerPaneId!)?.hidden).toBeUndefined();
|
|
expect(layout.focusedPaneId).toBe(explorerPaneId);
|
|
});
|
|
});
|