Skip to content
Open
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
2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@
# Documentation
mdbook
mdbook-mermaid
mdbook-linkcheck
mdbook-linkcheck2
mdbook-pagetoc

# Terminal bench + browser recording
Expand Down
8 changes: 2 additions & 6 deletions fmt.mk
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,9 @@ else ifeq ($(wildcard flake.nix),)
@echo "flake.nix not found; skipping Nix format check"
else
@echo "Checking flake.nix formatting..."
@tmp_dir=$$(mktemp -d "$${TMPDIR:-/tmp}/fmt-nix-check.XXXXXX"); \
trap "rm -rf $$tmp_dir" EXIT; \
cp flake.nix "$$tmp_dir/flake.nix"; \
(cd "$$tmp_dir" && nix fmt -- flake.nix >/dev/null 2>&1); \
if ! cmp -s flake.nix "$$tmp_dir/flake.nix"; then \
@# Check from the repo flake instead of a temp copy; `nix fmt` evaluates flake metadata.
@if ! nix fmt -- --check flake.nix; then \
echo "flake.nix is not formatted correctly. Run 'make fmt-nix' to fix."; \
diff -u flake.nix "$$tmp_dir/flake.nix" || true; \
exit 1; \
fi
endif
Expand Down
13 changes: 2 additions & 11 deletions scripts/fmt.sh
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,9 @@ check_nix_format() (
exit 0
fi

local tmp_dir
tmp_dir="$(mktemp -d "${TMPDIR:-/tmp}/fmt-nix-check.XXXXXX")"
trap 'rm -rf "$tmp_dir"' EXIT
cp "$flake_path" "$tmp_dir/flake.nix"
(
cd "$tmp_dir"
nix fmt -- flake.nix
)

if ! cmp -s "$flake_path" "$tmp_dir/flake.nix"; then
# Check from the repo flake instead of a temp copy; `nix fmt` evaluates flake metadata.
if ! nix fmt -- --check flake.nix; then
echo "flake.nix is not formatted correctly. Run ./scripts/fmt.sh --nix or make fmt-nix."
diff -u "$flake_path" "$tmp_dir/flake.nix" || true
exit 1
fi
)
Expand Down
195 changes: 105 additions & 90 deletions src/browser/components/ProjectSidebar/ProjectSidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,16 @@ let archiveWorkspaceActionMock = mock(
_options?: { acknowledgedUntrackedPaths?: string[] }
): Promise<ArchiveWorkspaceActionResult> => resolveArchiveResult()
);
let removeWorkspaceActionMock = mock(
(
_workspaceId: string,
_options?: { force?: boolean }
): Promise<{ success: boolean; error?: string }> => Promise.resolve({ success: true })
);
let settingsOpenMock = mock(() => undefined);
let confirmDialogMock = mock(() => Promise.resolve(true));
let confirmDialogMock = mock((_options: ConfirmDialogContextModule.ConfirmDialogOptions) =>
Promise.resolve(true)
);
let archivePopoverShowErrorMock = mock(
(_workspaceId: string, _error: string, _anchor?: { top: number; left: number }) => undefined
);
Expand Down Expand Up @@ -210,7 +218,15 @@ function installProjectSidebarTestDoubles() {
_options?: { acknowledgedUntrackedPaths?: string[] }
): Promise<ArchiveWorkspaceActionResult> => resolveArchiveResult()
);
confirmDialogMock = mock(() => Promise.resolve(true));
removeWorkspaceActionMock = mock(
(
_workspaceId: string,
_options?: { force?: boolean }
): Promise<{ success: boolean; error?: string }> => Promise.resolve({ success: true })
);
confirmDialogMock = mock((_options: ConfirmDialogContextModule.ConfirmDialogOptions) =>
Promise.resolve(true)
);
latestArchiveWorkspaceHandler = null;
latestArchiveConfirmationModalProps = null;
void mock.module("@/browser/assets/logos/mux-logo-dark.svg?react", () => ({
Expand Down Expand Up @@ -423,7 +439,7 @@ function installProjectSidebarTestDoubles() {
setSelectedWorkspace: () => undefined,
preflightArchiveWorkspace: preflightArchiveWorkspaceMock,
archiveWorkspace: archiveWorkspaceActionMock,
removeWorkspace: () => Promise.resolve({ success: true }),
removeWorkspace: removeWorkspaceActionMock,
updateWorkspaceTitle: () => Promise.resolve({ success: true }),
refreshWorkspaceMetadata: () => Promise.resolve(),
pendingNewWorkspaceProject: null,
Expand Down Expand Up @@ -602,6 +618,62 @@ describe("ProjectSidebar multi-project completed-subagent toggles", () => {
mock.restore();
});

function renderVariantGroupSidebar() {
window.localStorage.setItem(EXPANDED_PROJECTS_KEY, JSON.stringify(["/projects/demo-project"]));

const singleProjectRefs = [
{ projectPath: "/projects/demo-project", projectName: "demo-project" },
];
const parentWorkspace = {
...createWorkspace("parent", { title: "Parent workspace" }),
projects: singleProjectRefs,
};
const taskGroup = {
groupId: "variants-demo",
index: 0,
total: 2,
kind: "variants",
label: "frontend",
} as const;
const childOne = {
...createWorkspace("child-1", {
parentWorkspaceId: "parent",
taskStatus: "running",
title: "Split review",
bestOf: taskGroup,
}),
projects: singleProjectRefs,
};
const childTwo = {
...createWorkspace("child-2", {
parentWorkspaceId: "parent",
taskStatus: "queued",
title: "Split review",
bestOf: { ...taskGroup, index: 1, label: "backend" },
}),
projects: singleProjectRefs,
};

projectContextValue = createProjectContextValue({
userProjects: new Map([["/projects/demo-project", { workspaces: [] }]]),
hasAnyProject: true,
resolveNewChatProjectPath: () => "/projects/demo-project",
});

const view = render(
<ProjectSidebar
collapsed={false}
onToggleCollapsed={() => undefined}
sortedWorkspacesByProject={
new Map([["/projects/demo-project", [parentWorkspace, childOne, childTwo]]])
}
workspaceRecency={{ parent: Date.now(), "child-1": Date.now(), "child-2": Date.now() }}
/>
);

return { view, childOne, childTwo };
}

test("filters multi-project rows out entirely when the experiment is disabled", () => {
spyOn(ExperimentsModule, "useExperimentValue").mockImplementation(() => false);

Expand Down Expand Up @@ -856,93 +928,7 @@ describe("ProjectSidebar multi-project completed-subagent toggles", () => {
});

test("renders variants groups with a shared row and labeled members when expanded", async () => {
window.localStorage.setItem(EXPANDED_PROJECTS_KEY, JSON.stringify(["/projects/demo-project"]));

const singleProjectRefs = [
{ projectPath: "/projects/demo-project", projectName: "demo-project" },
];
const parentWorkspace = {
...createWorkspace("parent", { title: "Parent workspace" }),
projects: singleProjectRefs,
};
const taskGroup = {
groupId: "variants-demo",
index: 0,
total: 2,
kind: "variants",
label: "frontend",
} as const;
const childOne = {
...createWorkspace("child-1", {
parentWorkspaceId: "parent",
taskStatus: "running",
title: "Split review",
bestOf: taskGroup,
}),
projects: singleProjectRefs,
};
const childTwo = {
...createWorkspace("child-2", {
parentWorkspaceId: "parent",
taskStatus: "queued",
title: "Split review",
bestOf: { ...taskGroup, index: 1, label: "backend" },
}),
projects: singleProjectRefs,
};

const sortedWorkspacesByProject = new Map([
["/projects/demo-project", [parentWorkspace, childOne, childTwo]],
]);

const projectConfig = { workspaces: [] };
spyOn(ProjectContextModule, "useProjectContext").mockImplementation(() => ({
userProjects: new Map([["/projects/demo-project", projectConfig]]),
systemProjectPath: null,
resolveProjectPath: () => null,
getProjectConfig: () => projectConfig,
loading: false,
refreshProjects: () => Promise.resolve(),
addProject: () => undefined,
removeProject: () => Promise.resolve({ success: true }),
isProjectCreateModalOpen: false,
openProjectCreateModal: () => undefined,
closeProjectCreateModal: () => undefined,
workspaceModalState: {
isOpen: false,
projectPath: null,
projectName: "",
branches: [],
defaultTrunkBranch: undefined,
loadErrorMessage: null,
isLoading: false,
},
openWorkspaceModal: () => Promise.resolve(),
closeWorkspaceModal: () => undefined,
getBranchesForProject: () => Promise.resolve({ branches: [], recommendedTrunk: null }),
getSecrets: () => Promise.resolve([]),
updateSecrets: () => Promise.resolve(),
updateDisplayName: () => resolveVoidResult(),
updateColor: () => resolveVoidResult(),
assignWorkspaceToSubProject: () => resolveVoidResult(),
hasAnyProject: true,
resolveNewChatProjectPath: () => "/projects/demo-project",
}));

const workspaceRecency = {
parent: Date.now(),
"child-1": Date.now(),
"child-2": Date.now(),
};

const view = render(
<ProjectSidebar
collapsed={false}
onToggleCollapsed={() => undefined}
sortedWorkspacesByProject={sortedWorkspacesByProject}
workspaceRecency={workspaceRecency}
/>
);
const { view } = renderVariantGroupSidebar();

const groupRow = view.getByTestId("task-group-variants-demo");
expect(groupRow.textContent).toContain("Variants · Split review");
Expand All @@ -964,6 +950,35 @@ describe("ProjectSidebar multi-project completed-subagent toggles", () => {
expect(childTwoRow.dataset.connectorLayout).toBe("task-group-member");
});

test("deletes every variant from the group action menu", async () => {
const { view, childOne, childTwo } = renderVariantGroupSidebar();

fireEvent.click(view.getByTestId("task-group-actions-variants-demo"));
fireEvent.click(view.getByRole("button", { name: "Delete all variants" }));

await waitFor(() => {
expect(removeWorkspaceActionMock).toHaveBeenCalledTimes(2);
});

expect(confirmDialogMock.mock.calls[0]?.[0].confirmVariant).toBe("destructive");
expect(removeWorkspaceActionMock.mock.calls.map((call) => call[0])).toEqual([
childOne.id,
childTwo.id,
]);
expect(removeWorkspaceActionMock.mock.calls.map((call) => call[1])).toEqual([
{ force: true },
{ force: true },
]);
});

test("opens the variant group action menu on right-click", () => {
const { view } = renderVariantGroupSidebar();

fireEvent.contextMenu(view.getByTestId("task-group-variants-demo"));

expect(view.getByRole("button", { name: "Delete all variants" })).toBeTruthy();
});

test("does not coalesce a best-of group when one candidate still has hidden child tasks", () => {
window.localStorage.setItem(EXPANDED_PROJECTS_KEY, JSON.stringify(["/projects/demo-project"]));

Expand Down
Loading
Loading