Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions scripts/check_react_compiler_coverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ const HOT_COMPONENTS: Record<string, readonly string[]> = {
"src/browser/features/Messages/ReasoningMessage.tsx": ["ReasoningMessage"],
"src/browser/features/Messages/MarkdownCore.tsx": ["MarkdownCore"],
"src/browser/features/Messages/ChatBarrier/RetryBarrier.tsx": ["RetryBarrierContent"],
// Shared with the VS Code webview dock, which is not compiled itself (#5109).
"src/browser/components/SubAgentTasksDecoration/SubAgentTasksDecoration.tsx": [
"SubAgentTasksContent",
],
"src/browser/components/ProjectSidebar/ProjectSidebar.tsx": ["ProjectSidebarInner"],
"src/browser/components/AgentListItem/AgentListItem.tsx": ["AgentListItemInner"],
"src/browser/components/AppLoader/AppLoader.tsx": [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
import { cleanup, fireEvent, render } from "@testing-library/react";
import { installDom } from "../../../../tests/ui/dom";

import { DEFAULT_RUNTIME_CONFIG } from "@/common/constants/workspace";
import type { FrontendWorkspaceMetadata } from "@/common/types/workspace";
import { SubAgentTasksContent } from "./SubAgentTasksDecoration";

function task(id: string, options: Partial<FrontendWorkspaceMetadata>): FrontendWorkspaceMetadata {
return {
id,
name: id,
projectName: "xum",
projectPath: "/repo/xum",
namedWorkspacePath: `/tmp/${id}`,
runtimeConfig: DEFAULT_RUNTIME_CONFIG,
parentWorkspaceId: "parent",
...options,
};
}

// The content component takes all of its data as props, so the VS Code webview can render the
// tray without WorkspaceContext or WorkspaceStore (#5109).
describe("SubAgentTasksContent", () => {
let cleanupDom: (() => void) | null = null;

beforeEach(() => {
cleanupDom = installDom();
});

afterEach(() => {
cleanup();
cleanupDom?.();
cleanupDom = null;
});

const running = task("child-running", { title: "Explorer", taskStatus: "running" });
const reported = task("child-reported", { title: "Reviewer", taskStatus: "reported" });

test("renders nothing without sub-agents or workflow runs", () => {
const view = render(
<SubAgentTasksContent
subAgents={[]}
workflowGroups={[]}
descendantActivity={new Map()}
expanded={true}
onToggle={() => undefined}
onNavigate={() => undefined}
/>
);
expect(view.container.textContent).toBe("");
});

test("summarizes from props and navigates from an expanded row", () => {
const onToggle = mock(() => undefined);
const onNavigate = mock((_workspaceId: string) => undefined);
const props = {
subAgents: [
{ workspace: running, depth: 1 },
{ workspace: reported, depth: 1 },
],
workflowGroups: [],
// An armed monitor keeps the reported child active and shows it as Monitoring.
descendantActivity: new Map([
[reported.id, { hasActiveBashMonitor: true, isLiveActive: false }],
]),
onToggle,
onNavigate,
};
const view = render(<SubAgentTasksContent {...props} expanded={false} />);
expect(view.container.textContent).toContain("2 sub-agents · 2 active");
expect(view.queryByText("Reviewer")).toBeNull();
fireEvent.click(view.getByRole("button", { name: /2 sub-agents/ }));
expect(onToggle).toHaveBeenCalledTimes(1);

view.rerender(<SubAgentTasksContent {...props} expanded={true} />);
expect(view.getByRole("button", { name: /Reviewer/ }).textContent).toContain("Monitoring");
fireEvent.click(view.getByRole("button", { name: /Explorer/ }));
expect(onNavigate.mock.calls).toEqual([[running.id]]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
} from "@/browser/utils/ui/workspaceFiltering";
import { cn } from "@/common/lib/utils";

interface DescendantSubAgent {
export interface DescendantSubAgent {
workspace: FrontendWorkspaceMetadata;
depth: number;
}
Expand Down Expand Up @@ -682,49 +682,74 @@ export function SubAgentTasksDecoration(props: { workspaceId: string }) {
};
}, [api, nestedGapKey]);

if (subAgents.length === 0 && workflowGroups.length === 0) {
return (
<SubAgentTasksContent
subAgents={subAgents}
workflowGroups={workflowGroups}
descendantActivity={descendantActivity}
expanded={expanded}
onToggle={() => setExpanded(!expanded)}
onNavigate={navigateToWorkspace}
/>
);
}

/**
* The tray itself, driven by props so hosts without WorkspaceContext/WorkspaceStore (the VS Code
* webview, #5109) can feed it. SubAgentTasksDecoration is the desktop container.
*/
export function SubAgentTasksContent(props: {
subAgents: readonly DescendantSubAgent[];
workflowGroups: readonly WorkflowAgentGroup[];
descendantActivity: ReadonlyMap<string, DescendantActivityHints>;
expanded: boolean;
onToggle: () => void;
onNavigate: (workspaceId: string) => void;
}) {
if (props.subAgents.length === 0 && props.workflowGroups.length === 0) {
return null;
}

const activeCount = subAgents.filter(({ workspace }) =>
isSubAgentActive(workspace, descendantActivity.get(workspace.id))
const activeCount = props.subAgents.filter(({ workspace }) =>
isSubAgentActive(workspace, props.descendantActivity.get(workspace.id))
).length;
const SummaryIcon = subAgents.length === 0 ? Workflow : Bot;
const SummaryIcon = props.subAgents.length === 0 ? Workflow : Bot;

return (
<ChatInputDecoration
expanded={expanded}
onToggle={() => setExpanded(!expanded)}
expanded={props.expanded}
onToggle={props.onToggle}
dataComponent="SubAgentTasksDecoration"
contentClassName="max-h-52 space-y-1 overflow-y-auto py-2"
summary={
<>
<SummaryIcon className="text-muted group-hover:text-secondary size-3.5 shrink-0 transition-colors" />
<span className="text-muted group-hover:text-secondary min-w-0 truncate transition-colors">
{subAgents.length > 0 && (
{props.subAgents.length > 0 && (
<>
<span className="font-medium">{subAgents.length}</span> sub-agent
{subAgents.length === 1 ? "" : "s"}
<span className="font-medium">{props.subAgents.length}</span> sub-agent
{props.subAgents.length === 1 ? "" : "s"}
{activeCount > 0 ? ` · ${activeCount} active` : " · inactive"}
</>
)}
{subAgents.length > 0 && workflowGroups.length > 0 && " · "}
{workflowGroups.length > 0 && formatWorkflowSummary(workflowGroups, descendantActivity)}
{props.subAgents.length > 0 && props.workflowGroups.length > 0 && " · "}
{props.workflowGroups.length > 0 &&
formatWorkflowSummary(props.workflowGroups, props.descendantActivity)}
</span>
</>
}
renderExpanded={() => (
<>
{subAgents.map(({ workspace, depth }) => (
{props.subAgents.map(({ workspace, depth }) => (
<AgentRow
key={workspace.id}
workspace={workspace}
hints={descendantActivity.get(workspace.id)}
hints={props.descendantActivity.get(workspace.id)}
indentLevel={depth - 1}
onNavigate={navigateToWorkspace}
onNavigate={props.onNavigate}
/>
))}
{workflowGroups.map((group) => (
{props.workflowGroups.map((group) => (
<div key={group.runId} className="space-y-1">
<div className="text-muted flex min-w-0 items-center gap-2 px-2 pt-1.5 pb-0.5 text-[10px]">
<Workflow className="size-3.5 shrink-0" />
Expand All @@ -736,11 +761,11 @@ export function SubAgentTasksDecoration(props: { workspaceId: string }) {
<AgentRow
key={workspace.id}
workspace={workspace}
hints={descendantActivity.get(workspace.id)}
hints={props.descendantActivity.get(workspace.id)}
// Nest workers one level under their run header; nested runs keep
// their relative depth without inheriting the outer tree's offset.
indentLevel={Math.max(0, depth - group.workers[0].depth) + 1}
onNavigate={navigateToWorkspace}
onNavigate={props.onNavigate}
/>
))}
</div>
Expand Down
Loading