Skip to content

Commit 1d34bf9

Browse files
authored
🤖 fix: align variant sub-agent connectors (#3199)
## Summary Fixes sidebar connector alignment for expanded variants/best-of sub-agent groups by rendering grouped members on the same indentation grid as the task-group header icon. ## Background Grouped sub-agents use a task-group header row with an extra disclosure chevron before the group icon. Expanded members were only indented one depth level below that header, which left the parent-to-child connector rail visually offset from the grouped parent row. ## Implementation - Added a shared task-group member depth helper in the sidebar layout utilities. - Applied that helper when rendering expanded task-group members from `ProjectSidebar`. - Added tests that assert grouped member depth/layout propagation and rendered connector geometry. ## Validation - `bun test src/browser/components/sidebarItemLayout.test.ts src/browser/components/AgentListItem/AgentListItem.test.tsx src/browser/components/ProjectSidebar/ProjectSidebar.test.tsx` - `make typecheck` - `make fmt-check` - `make lint` - `make static-check` ## Risks Low-to-medium risk, scoped to left-sidebar task-group sub-agent row indentation and connector geometry. --- _Generated with `mux` • Model: `openai:gpt-5.5` • Thinking: `high` • Cost: `$8.04`_ <!-- mux-attribution: model=openai:gpt-5.5 thinking=high costs=8.04 -->
1 parent cab13fc commit 1d34bf9

5 files changed

Lines changed: 63 additions & 4 deletions

File tree

src/browser/components/AgentListItem/AgentListItem.test.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,10 @@ function installAgentListItemTestDoubles() {
168168
}),
169169
}));
170170

171+
void mock.module("../WorkspaceHeartbeatModal", () => ({
172+
WorkspaceHeartbeatModal: () => null,
173+
}));
174+
171175
void mock.module("@/browser/hooks/useContextMenuPosition", () => ({
172176
...actualContextMenuPosition,
173177
useContextMenuPosition: () => ({
@@ -218,6 +222,7 @@ function renderWorkspaceItem(
218222
isArchiving?: boolean;
219223
depth?: number;
220224
rowRenderMeta?: AgentRowRenderMeta;
225+
subAgentConnectorLayout?: "default" | "task-group-member";
221226
completedChildrenExpanded?: boolean;
222227
onToggleCompletedChildren?: (workspaceId: string) => void;
223228
} = {}
@@ -232,6 +237,7 @@ function renderWorkspaceItem(
232237
isArchiving={options.isArchiving}
233238
depth={options.depth ?? options.rowRenderMeta?.depth}
234239
rowRenderMeta={options.rowRenderMeta}
240+
subAgentConnectorLayout={options.subAgentConnectorLayout}
235241
completedChildrenExpanded={options.completedChildrenExpanded}
236242
onToggleCompletedChildren={options.onToggleCompletedChildren}
237243
onSelectWorkspace={() => undefined}
@@ -329,6 +335,31 @@ describe("AgentListItem", () => {
329335
expect(elbow.getAttribute("style")).toContain("width: 8px");
330336
});
331337

338+
test("anchors task-group member connectors to the task-group rail", () => {
339+
const { view } = renderWorkspaceItem({
340+
depth: 2.5,
341+
subAgentConnectorLayout: "task-group-member",
342+
rowRenderMeta: {
343+
depth: 1,
344+
rowKind: "subagent",
345+
connectorPosition: "single",
346+
connectorStartsAtParent: true,
347+
sharedTrunkActiveThroughRow: false,
348+
sharedTrunkActiveBelowRow: false,
349+
ancestorTrunks: [],
350+
hasHiddenCompletedChildren: false,
351+
visibleCompletedChildrenCount: 0,
352+
},
353+
});
354+
355+
const topSegment = view.getByTestId("subagent-connector-top-segment");
356+
const elbow = view.getByTestId("subagent-connector-elbow");
357+
358+
expect(topSegment.getAttribute("style")).toContain("left: 38px");
359+
expect(elbow.getAttribute("style")).toContain("left: 38px");
360+
expect(elbow.getAttribute("style")).toContain("width: 1px");
361+
});
362+
332363
test("does not render a heartbeat icon fallback when completed children indicator is shown", () => {
333364
mockWorkspaceHeartbeatsEnabled = true;
334365

src/browser/components/ProjectSidebar/ProjectSidebar.test.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ interface MockAgentListItemProps {
9494
};
9595
depth?: number;
9696
rowRenderMeta?: AgentRowRenderMeta;
97+
subAgentConnectorLayout?: "default" | "task-group-member";
9798
completedChildrenExpanded?: boolean;
9899
onToggleCompletedChildren?: (workspaceId: string) => void;
99100
onArchiveWorkspace?: (workspaceId: string, button: HTMLElement) => Promise<void>;
@@ -230,6 +231,7 @@ function installProjectSidebarTestDoubles() {
230231
data-testid={agentItemTestId(metadata.id)}
231232
data-depth={String(props.depth ?? -1)}
232233
data-row-kind={props.rowRenderMeta?.rowKind ?? "unknown"}
234+
data-connector-layout={props.subAgentConnectorLayout ?? "default"}
233235
data-completed-expanded={String(props.completedChildrenExpanded ?? false)}
234236
>
235237
<span>{displayTitle}</span>
@@ -862,6 +864,13 @@ describe("ProjectSidebar multi-project completed-subagent toggles", () => {
862864
expect(view.getByText("frontend · Split review")).toBeTruthy();
863865
expect(view.getByText("backend · Split review")).toBeTruthy();
864866
});
867+
868+
const childOneRow = view.getByTestId(agentItemTestId("child-1"));
869+
const childTwoRow = view.getByTestId(agentItemTestId("child-2"));
870+
expect(childOneRow.dataset.depth).toBe("2.5");
871+
expect(childOneRow.dataset.connectorLayout).toBe("task-group-member");
872+
expect(childTwoRow.dataset.depth).toBe("2.5");
873+
expect(childTwoRow.dataset.connectorLayout).toBe("task-group-member");
865874
});
866875

867876
test("does not coalesce a best-of group when one candidate still has hidden child tasks", () => {

src/browser/components/ProjectSidebar/ProjectSidebar.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import { ProjectDeleteConfirmationModal } from "../ProjectDeleteConfirmationModa
7070
import { useSettings } from "@/browser/contexts/SettingsContext";
7171

7272
import { AgentListItem, type WorkspaceSelection } from "../AgentListItem/AgentListItem";
73+
import { getTaskGroupMemberDepth } from "../sidebarItemLayout";
7374
import { TaskGroupListItem } from "./TaskGroupListItem";
7475
import { TitleEditProvider, useTitleEdit } from "@/browser/contexts/WorkspaceTitleEditContext";
7576
import { useConfirmDialog } from "@/browser/contexts/ConfirmDialogContext";
@@ -2325,7 +2326,7 @@ const ProjectSidebarInner: React.FC<ProjectSidebarProps> = ({
23252326
member,
23262327
sectionId,
23272328
rowMetaByWorkspaceId.get(member.id) ?? null,
2328-
depth + 1,
2329+
getTaskGroupMemberDepth(depth),
23292330
`task-group-member:${taskGroupId}:${member.id}`,
23302331
"task-group-member"
23312332
)

src/browser/components/sidebarItemLayout.test.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
getSidebarItemPaddingLeft,
77
getSubAgentChildStatusCenterX,
88
getSubAgentParentRailX,
9+
getTaskGroupMemberDepth,
910
} from "./sidebarItemLayout";
1011

1112
describe("sidebarItemLayout", () => {
@@ -24,7 +25,15 @@ describe("sidebarItemLayout", () => {
2425
});
2526

2627
test("keeps grouped members on their dedicated shared rail", () => {
27-
expect(getSubAgentParentRailX(2, "task-group-member")).toBe(30);
28-
expect(getAncestorRailX(2, "task-group-member")).toBe(32);
28+
const groupDepth = 1;
29+
const memberDepth = getTaskGroupMemberDepth(groupDepth);
30+
31+
expect(memberDepth).toBe(2.5);
32+
// The task-group header has a disclosure chevron before its group icon, so
33+
// expanded variants/best-of members use a half-step offset that keeps both
34+
// the connector rail and child status dot under the group icon.
35+
expect(getSubAgentParentRailX(memberDepth, "task-group-member")).toBe(38);
36+
expect(getSubAgentChildStatusCenterX(memberDepth)).toBe(38);
37+
expect(getAncestorRailX(memberDepth, "task-group-member")).toBe(36);
2938
});
3039
});

src/browser/components/sidebarItemLayout.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,23 @@ const SIDEBAR_BASE_PADDING_LEFT_PX = 10;
44
const SIDEBAR_DEPTH_INDENT_PX = 8;
55
export const SIDEBAR_LEADING_SLOT_SIZE_PX = 16;
66
const SIDEBAR_LEADING_SLOT_CENTER_OFFSET_PX = SIDEBAR_LEADING_SLOT_SIZE_PX / 2;
7-
const TASK_GROUP_MEMBER_PARENT_RAIL_OFFSET_PX = 4;
7+
const TASK_GROUP_MEMBER_DEPTH_OFFSET = 1.5;
8+
const TASK_GROUP_MEMBER_PARENT_RAIL_OFFSET_PX = SIDEBAR_LEADING_SLOT_CENTER_OFFSET_PX;
89
const TASK_GROUP_MEMBER_ANCESTOR_RAIL_OFFSET_PX = 6;
910

1011
export function getSidebarItemPaddingLeft(depth?: number): number {
1112
const safeDepth = typeof depth === "number" && Number.isFinite(depth) ? Math.max(0, depth) : 0;
1213
return SIDEBAR_BASE_PADDING_LEFT_PX + Math.min(32, safeDepth) * SIDEBAR_DEPTH_INDENT_PX;
1314
}
1415

16+
export function getTaskGroupMemberDepth(taskGroupDepth: number): number {
17+
// Expanded variants/best-of members sit under a task-group header that has a
18+
// disclosure chevron before its group icon. One-and-a-half grid steps puts
19+
// member status dots under that group icon while keeping member labels aligned
20+
// with the task-group title.
21+
return taskGroupDepth + TASK_GROUP_MEMBER_DEPTH_OFFSET;
22+
}
23+
1524
export function getSidebarLeadingSlotCenterX(depth: number): number {
1625
return getSidebarItemPaddingLeft(depth) + SIDEBAR_LEADING_SLOT_CENTER_OFFSET_PX;
1726
}

0 commit comments

Comments
 (0)