- 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.
223 lines
6.4 KiB
TypeScript
223 lines
6.4 KiB
TypeScript
import type { DaemonClient } from "@server/client/daemon-client";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
deriveWorkspaceAgentVisibility,
|
|
type WorkspaceAgentVisibility,
|
|
} from "@/screens/workspace/workspace-agent-visibility";
|
|
import { selectSubagentsForParent } from "@/subagents";
|
|
import { buildWorkspaceTabPersistenceKey, useWorkspaceLayoutStore } from "./workspace-layout-store";
|
|
import { useSessionStore, type Agent } from "./session-store";
|
|
|
|
vi.mock("lucide-react-native", () => ({
|
|
Archive: () => null,
|
|
Check: () => null,
|
|
ChevronDown: () => null,
|
|
ChevronRight: () => null,
|
|
}));
|
|
|
|
vi.mock("react-native-unistyles", () => ({
|
|
StyleSheet: {
|
|
create: (factory: unknown) =>
|
|
typeof factory === "function"
|
|
? factory({
|
|
spacing: { 0: 0, 1: 4, 2: 8, 3: 12, 4: 16 },
|
|
borderWidth: { 1: 1 },
|
|
borderRadius: { sm: 4, md: 6, lg: 8, "2xl": 16, full: 999 },
|
|
fontSize: { xs: 11, sm: 13, base: 15 },
|
|
fontWeight: { normal: "400", medium: "500" },
|
|
iconSize: { sm: 14, md: 18 },
|
|
colors: {
|
|
foreground: "#fff",
|
|
foregroundMuted: "#aaa",
|
|
surface0: "#000",
|
|
surface1: "#111",
|
|
surface2: "#222",
|
|
surface3: "#333",
|
|
border: "#444",
|
|
borderAccent: "#555",
|
|
accent: "#0a84ff",
|
|
palette: {
|
|
amber: { 500: "#ffbf00", 700: "#aa8000" },
|
|
blue: { 500: "#0a84ff" },
|
|
red: { 500: "#ff453a" },
|
|
green: { 500: "#30d158" },
|
|
},
|
|
},
|
|
})
|
|
: factory,
|
|
},
|
|
useUnistyles: () => ({ theme: { colors: {} } }),
|
|
withUnistyles: <T>(component: T) => component,
|
|
}));
|
|
|
|
vi.mock("@/components/provider-icons", () => ({
|
|
getProviderIcon: () => null,
|
|
}));
|
|
|
|
vi.mock("@/components/ui/tooltip", () => ({
|
|
Tooltip: ({ children }: { children: unknown }) => children,
|
|
TooltipTrigger: ({ children }: { children: unknown }) => children,
|
|
TooltipContent: () => null,
|
|
}));
|
|
|
|
vi.mock("@/screens/workspace/workspace-tab-presentation", () => ({
|
|
WorkspaceTabIcon: () => null,
|
|
}));
|
|
|
|
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",
|
|
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,
|
|
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,
|
|
workspaceDirectory: WORKSPACE_DIRECTORY,
|
|
});
|
|
}
|
|
|
|
function reconcileWorkspaceTabs(workspaceKey: string, visibility: WorkspaceAgentVisibility): void {
|
|
useWorkspaceLayoutStore.getState().reconcileTabs(workspaceKey, {
|
|
agentsHydrated: true,
|
|
terminalsHydrated: true,
|
|
activeAgentIds: visibility.activeAgentIds,
|
|
autoOpenAgentIds: visibility.autoOpenAgentIds,
|
|
knownAgentIds: visibility.knownAgentIds,
|
|
standaloneTerminalIds: [],
|
|
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"]);
|
|
});
|
|
});
|