Surface cross-workspace subagents in their workspaces (#2061)

* fix(workspaces): show provider subagent activity

Provider subagent lifecycle lives outside managed agent snapshots, so workspace aggregation dropped it after the parent turn finished. Count running provider subagents against the delegation root and emit workspace updates when their status changes.

* fix(workspaces): surface cross-workspace subagents
This commit is contained in:
Mohamed Boudra
2026-07-14 14:22:14 +02:00
committed by GitHub
parent c0b801b805
commit 1f5283f5a3
14 changed files with 295 additions and 38 deletions
@@ -215,4 +215,38 @@ describe("workspace subagents integration", () => {
),
).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]);
});
});
@@ -1,5 +0,0 @@
import type { Agent } from "@/stores/session-store";
export function shouldAutoOpenAgentTab(agent: Pick<Agent, "parentAgentId">): boolean {
return !agent.parentAgentId;
}
+1 -1
View File
@@ -3,4 +3,4 @@ export { selectSubagentsForParent, useSubagentsForParent } from "./select";
export { useArchiveSubagent, type UseArchiveSubagentInput } from "./use-archive-subagent";
export { useDetachSubagent, type UseDetachSubagentInput } from "./use-detach-subagent";
export { resolveCloseAgentTabPolicy, type CloseAgentTabPolicy } from "./close-tab-policy";
export { shouldAutoOpenAgentTab } from "./auto-open-tab-policy";
export { isWorkspaceRootAgent } from "./workspace-root-policy";
+1 -1
View File
@@ -6,4 +6,4 @@
// UI surface. Use `@/subagents/policies` only when the caller is
// non-RN infrastructure code; otherwise prefer `@/subagents`.
export { resolveCloseAgentTabPolicy, type CloseAgentTabPolicy } from "./close-tab-policy";
export { shouldAutoOpenAgentTab } from "./auto-open-tab-policy";
export { isWorkspaceRootAgent } from "./workspace-root-policy";
@@ -0,0 +1,17 @@
import type { Agent } from "@/stores/session-store";
import { normalizeWorkspaceOpaqueId } from "@/utils/workspace-identity";
type WorkspaceAgent = Pick<Agent, "parentAgentId" | "workspaceId">;
export function isWorkspaceRootAgent(
agent: WorkspaceAgent,
parentAgent: Pick<Agent, "workspaceId"> | undefined,
): boolean {
if (!agent.parentAgentId) {
return true;
}
const workspaceId = normalizeWorkspaceOpaqueId(agent.workspaceId);
const parentWorkspaceId = normalizeWorkspaceOpaqueId(parentAgent?.workspaceId);
return Boolean(workspaceId && parentWorkspaceId && workspaceId !== parentWorkspaceId);
}
@@ -155,6 +155,52 @@ describe("workspace agent activity index", () => {
});
});
it("treats a cross-workspace subagent as activity in its own workspace", () => {
const index = buildWorkspaceAgentActivityIndex(
new Map([
[
"parent",
agent({
id: "parent",
workspaceId: "workspace-a",
updatedAt: "2026-06-01T10:00:00.000Z",
}),
],
[
"child",
agent({
id: "child",
workspaceId: "workspace-b",
status: "running",
updatedAt: "2026-06-01T10:03:00.000Z",
parentAgentId: "parent",
}),
],
]),
);
expect(index).toEqual(
new Map([
[
"workspace-a",
{
agentId: "parent",
status: "done",
enteredAt: new Date("2026-06-01T10:00:00.000Z"),
},
],
[
"workspace-b",
{
agentId: "child",
status: "running",
enteredAt: new Date("2026-06-01T10:03:00.000Z"),
},
],
]),
);
});
it("preserves the activity index while the same agent remains in the same status", () => {
const previous = buildWorkspaceAgentActivityIndex(
new Map([
@@ -1,4 +1,5 @@
import type { Agent, WorkspaceDescriptor } from "@/stores/session-store";
import { isWorkspaceRootAgent } from "@/subagents/policies";
import { deriveSidebarStateBucket } from "./sidebar-agent-state";
export interface WorkspaceAgentActivity {
@@ -15,7 +16,8 @@ export function buildWorkspaceAgentActivityIndex(
const latestActivityAtByWorkspaceId = new Map<string, Date>();
for (const agent of agents.values()) {
if (agent.archivedAt || agent.parentAgentId || !agent.workspaceId) {
const parentAgent = agent.parentAgentId ? agents.get(agent.parentAgentId) : undefined;
if (agent.archivedAt || !agent.workspaceId || !isWorkspaceRootAgent(agent, parentAgent)) {
continue;
}
@@ -131,6 +131,32 @@ describe("workspace agent visibility", () => {
expect(result.knownAgentIds).toEqual(new Set(["child-agent", "parent-agent"]));
});
it("auto-opens a subagent whose parent belongs to another workspace", () => {
const parent = makeAgent({
id: "parent-agent",
cwd: "/repo",
workspaceId: "ws-parent",
});
const child = makeAgent({
id: "child-agent",
cwd: "/repo/worktree",
workspaceId: WORKSPACE_ID,
parentAgentId: parent.id,
});
const result = deriveWorkspaceAgentVisibility({
sessionAgents: new Map<string, Agent>([
[parent.id, parent],
[child.id, child],
]),
workspaceId: WORKSPACE_ID,
});
expect(result.activeAgentIds).toEqual(new Set(["child-agent"]));
expect(result.autoOpenAgentIds).toEqual(new Set(["child-agent"]));
expect(result.knownAgentIds).toEqual(new Set(["child-agent"]));
});
it("keeps archived agents out of activeAgentIds but present in knownAgentIds", () => {
const visible = makeAgent({
id: "visible-agent",
@@ -1,6 +1,6 @@
import type { Agent } from "@/stores/session-store";
import type { WorkspaceTabSnapshot } from "@/stores/workspace-layout-actions";
import { shouldAutoOpenAgentTab } from "@/subagents/policies";
import { isWorkspaceRootAgent } from "@/subagents/policies";
import { normalizeWorkspaceOpaqueId } from "@/utils/workspace-identity";
export interface WorkspaceAgentVisibility {
@@ -31,6 +31,10 @@ export function deriveWorkspaceAgentVisibility(input: {
const activeAgentIds = new Set<string>();
const autoOpenAgentIds = new Set<string>();
const knownAgentIds = new Set<string>();
const agentsById = new Map<string, Agent>([
...(agentDetails?.entries() ?? []),
...(sessionAgents?.entries() ?? []),
]);
for (const agent of sessionAgents?.values() ?? []) {
if (!agentBelongsToWorkspace(agent, workspaceId)) {
continue;
@@ -38,7 +42,8 @@ export function deriveWorkspaceAgentVisibility(input: {
knownAgentIds.add(agent.id);
if (!agent.archivedAt) {
activeAgentIds.add(agent.id);
if (shouldAutoOpenAgentTab(agent)) {
const parentAgent = agent.parentAgentId ? agentsById.get(agent.parentAgentId) : undefined;
if (isWorkspaceRootAgent(agent, parentAgent)) {
autoOpenAgentIds.add(agent.id);
}
}