Organize composer into its own module

This commit is contained in:
Mohamed Boudra
2026-05-26 14:32:45 +07:00
parent 7e792e899a
commit 4231dbbba4
52 changed files with 935 additions and 706 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
export { SubagentsSection } from "./section";
export type { SubagentsSectionProps } from "./section";
export { SubagentsTrack } from "./track";
export type { SubagentsTrackProps } from "./track";
export type { SubagentRow } from "./select";
export { selectSubagentsForParent, useSubagentsForParent } from "./select";
export { useArchiveSubagent, type UseArchiveSubagentInput } from "./use-archive-subagent";
@@ -5,7 +5,7 @@ import React from "react";
import { act } from "@testing-library/react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { SubagentsSection } from "./section";
import { SubagentsTrack } from "./track";
import type { SubagentRow } from "./select";
const { theme } = vi.hoisted(() => ({
@@ -98,8 +98,10 @@ vi.mock("@/components/synced-loader", () => ({
}));
vi.mock("lucide-react-native", () => {
const createIcon = (name: string) => (props: Record<string, unknown>) =>
React.createElement("span", { ...props, "data-icon": name });
const createIcon = (name: string) =>
function IconStub(props: Record<string, unknown>) {
return React.createElement("span", { ...props, "data-icon": name });
};
return {
Archive: createIcon("Archive"),
Check: createIcon("Check"),
@@ -131,13 +133,13 @@ function queryByTestId(testID: string): HTMLElement | null {
function queryRowIds(): string[] {
return Array.from(
document.querySelectorAll<HTMLElement>('[data-testid^="subagents-section-row-"]'),
).map((node) => node.getAttribute("data-testid")?.replace("subagents-section-row-", "") ?? "");
document.querySelectorAll<HTMLElement>('[data-testid^="subagents-track-row-"]'),
).map((node) => node.getAttribute("data-testid")?.replace("subagents-track-row-", "") ?? "");
}
type SubagentAction = (id: string) => void;
describe("SubagentsSection", () => {
describe("SubagentsTrack", () => {
let container: HTMLElement | null = null;
let root: Root | null = null;
@@ -176,7 +178,7 @@ describe("SubagentsSection", () => {
): SubagentAction {
act(() => {
root?.render(
<SubagentsSection
<SubagentsTrack
rows={rows}
onOpenSubagent={onOpenSubagent}
onArchiveSubagent={onArchiveSubagent}
@@ -188,27 +190,27 @@ describe("SubagentsSection", () => {
it("renders nothing when rows is empty", () => {
render([]);
expect(queryByTestId("subagents-section")).toBeNull();
expect(queryByTestId("subagents-section-header")).toBeNull();
expect(queryByTestId("subagents-track")).toBeNull();
expect(queryByTestId("subagents-track-header")).toBeNull();
expect(queryRowIds()).toEqual([]);
});
it("shows only the collapsed header and no rows initially", () => {
render([row({ id: "child-a" }), row({ id: "child-b" })]);
expect(queryByTestId("subagents-section-header")).not.toBeNull();
expect(queryByTestId("subagents-track-header")).not.toBeNull();
expect(queryRowIds()).toEqual([]);
});
it("expands rows in the given order when the header is pressed", () => {
render([row({ id: "child-b" }), row({ id: "child-a" }), row({ id: "child-c" })]);
click(queryByTestId("subagents-section-header")!);
click(queryByTestId("subagents-track-header")!);
expect(queryRowIds()).toEqual(["child-b", "child-a", "child-c"]);
});
it("calls onOpenSubagent with the row id when a row is pressed", () => {
const onOpenSubagent = render([row({ id: "child-a" }), row({ id: "child-b" })]);
click(queryByTestId("subagents-section-header")!);
click(queryByTestId("subagents-section-row-child-b")!);
click(queryByTestId("subagents-track-header")!);
click(queryByTestId("subagents-track-row-child-b")!);
expect(onOpenSubagent).toHaveBeenCalledTimes(1);
expect(onOpenSubagent).toHaveBeenCalledWith("child-b");
});
@@ -216,7 +218,7 @@ describe("SubagentsSection", () => {
describe("header copy", () => {
it("renders '2 subagents' when two rows are not running", () => {
render([row({ id: "child-a" }), row({ id: "child-b" })]);
expect(queryByTestId("subagents-section-header")?.textContent).toBe("2 subagents");
expect(queryByTestId("subagents-track-header")?.textContent).toBe("2 subagents");
});
it("renders '3 subagents · 1 running' with a single running row", () => {
@@ -225,14 +227,12 @@ describe("SubagentsSection", () => {
row({ id: "child-b" }),
row({ id: "child-c" }),
]);
expect(queryByTestId("subagents-section-header")?.textContent).toBe(
"3 subagents · 1 running",
);
expect(queryByTestId("subagents-track-header")?.textContent).toBe("3 subagents · 1 running");
});
it("renders '1 subagent' for a finished row that still requires attention upstream", () => {
render([row({ id: "child-a", requiresAttention: true })]);
expect(queryByTestId("subagents-section-header")?.textContent).toBe("1 subagent");
expect(queryByTestId("subagents-track-header")?.textContent).toBe("1 subagent");
});
it("renders '5 subagents · 2 running' when finished rows require attention upstream", () => {
@@ -243,9 +243,7 @@ describe("SubagentsSection", () => {
row({ id: "d" }),
row({ id: "e" }),
]);
expect(queryByTestId("subagents-section-header")?.textContent).toBe(
"5 subagents · 2 running",
);
expect(queryByTestId("subagents-track-header")?.textContent).toBe("5 subagents · 2 running");
});
});
@@ -255,7 +253,7 @@ describe("SubagentsSection", () => {
row({ id: "b", status: "idle", requiresAttention: false }),
row({ id: "c", status: "idle", requiresAttention: true }),
]);
expect(queryByTestId("subagents-section-header")?.textContent).toBe("3 subagents");
expect(queryByTestId("subagents-track-header")?.textContent).toBe("3 subagents");
});
it("still counts running rows even when they require attention", () => {
@@ -264,7 +262,7 @@ describe("SubagentsSection", () => {
row({ id: "b", status: "running", requiresAttention: true }),
row({ id: "c", status: "idle", requiresAttention: true }),
]);
expect(queryByTestId("subagents-section-header")?.textContent).toBe("3 subagents · 1 running");
expect(queryByTestId("subagents-track-header")?.textContent).toBe("3 subagents · 1 running");
});
it("renders each row through the shared workspace tab icon primitives", () => {
@@ -272,14 +270,14 @@ describe("SubagentsSection", () => {
row({ id: "idle-child", status: "idle", provider: "codex" }),
row({ id: "running-child", status: "running", provider: "claude-code" }),
]);
click(queryByTestId("subagents-section-header")!);
click(queryByTestId("subagents-track-header")!);
const idleRow = queryByTestId("subagents-section-row-idle-child");
const idleRow = queryByTestId("subagents-track-row-idle-child");
expect(idleRow).not.toBeNull();
expect(idleRow!.querySelectorAll('[data-testid="subagents-provider-icon"]').length).toBe(1);
expect(idleRow!.querySelectorAll('[data-testid="subagents-synced-loader"]').length).toBe(0);
const runningRow = queryByTestId("subagents-section-row-running-child");
const runningRow = queryByTestId("subagents-track-row-running-child");
expect(runningRow).not.toBeNull();
expect(runningRow!.querySelectorAll('[data-testid="subagents-synced-loader"]').length).toBe(1);
expect(runningRow!.querySelectorAll('[data-testid="subagents-provider-icon"]').length).toBe(0);
@@ -23,7 +23,7 @@ const foregroundMutedColorMapping = (theme: Theme) => ({
color: theme.colors.foregroundMuted,
});
export interface SubagentsSectionProps {
export interface SubagentsTrackProps {
rows: SubagentRow[];
onOpenSubagent: (id: string) => void;
onArchiveSubagent: (id: string) => void;
@@ -76,11 +76,11 @@ function buildRowPresentation(row: SubagentRow): WorkspaceTabPresentation {
};
}
export function SubagentsSection({
export function SubagentsTrack({
rows,
onOpenSubagent,
onArchiveSubagent,
}: SubagentsSectionProps): ReactElement | null {
}: SubagentsTrackProps): ReactElement | null {
const [expanded, setExpanded] = useState(false);
const toggleExpanded = useCallback(() => {
@@ -108,13 +108,13 @@ export function SubagentsSection({
const headerLabel = formatHeaderLabel(rows);
return (
<View style={styles.outer} testID="subagents-section">
<View style={styles.outer} testID="subagents-track">
<View style={styles.track}>
<View style={surfaceStyle}>
<Pressable
accessibilityRole="button"
accessibilityLabel={headerLabel}
testID="subagents-section-header"
testID="subagents-track-header"
onPress={toggleExpanded}
style={headerStyle}
>
@@ -135,7 +135,7 @@ export function SubagentsSection({
nestedScrollEnabled
>
{rows.map((row) => (
<SubagentsSectionRow
<SubagentsTrackRow
key={row.id}
row={row}
onOpenSubagent={onOpenSubagent}
@@ -150,17 +150,17 @@ export function SubagentsSection({
);
}
interface SubagentsSectionRowProps {
interface SubagentsTrackRowProps {
row: SubagentRow;
onOpenSubagent: (id: string) => void;
onArchiveSubagent: (id: string) => void;
}
function SubagentsSectionRow({
function SubagentsTrackRow({
row,
onOpenSubagent,
onArchiveSubagent,
}: SubagentsSectionRowProps): ReactElement {
}: SubagentsTrackRowProps): ReactElement {
const isCompact = useIsCompactFormFactor();
const [hovered, setHovered] = useState(false);
const presentation = useMemo(() => buildRowPresentation(row), [row]);
@@ -184,7 +184,7 @@ function SubagentsSectionRow({
<Pressable
accessibilityRole="button"
accessibilityLabel={displayLabel}
testID={`subagents-section-row-${row.id}`}
testID={`subagents-track-row-${row.id}`}
onPress={handlePress}
>
{({ pressed }) => (
@@ -227,7 +227,7 @@ function SubagentArchiveButton({
<Pressable
accessibilityRole="button"
accessibilityLabel={`Archive ${displayLabel}`}
testID={`subagents-section-archive-${rowId}`}
testID={`subagents-track-archive-${rowId}`}
onPress={onPress}
style={styles.archiveButton}
hitSlop={8}
@@ -23,8 +23,8 @@ vi.mock("@/utils/confirm-dialog", () => ({
confirmDialog: confirmDialogMock,
}));
vi.mock("./section", () => ({
SubagentsSection: () => null,
vi.mock("./track", () => ({
SubagentsTrack: () => null,
}));
const SERVER_ID = "server-1";