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
18 changes: 17 additions & 1 deletion desktop/src/features/agents/ui/AgentsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
DropdownMenuTrigger,
} from "@/shared/ui/dropdown-menu";
import { PageHeader } from "@/shared/ui/PageHeader";
import { scheduleAfterModalClose } from "@/shared/ui/scheduleAfterModalClose";
import { getInheritedAgentDefaults } from "./bakedEnvHelpers";

export function AgentsView() {
Expand All @@ -45,6 +46,9 @@ export function AgentsView() {
const agents = useManagedAgentActions();
const personas = usePersonaActions();
const teamImportInputRef = React.useRef<HTMLInputElement | null>(null);
const cancelCatalogImportHandoffRef = React.useRef<(() => void) | null>(
null,
);
const aiDefaultsTriggerRef = React.useRef<HTMLButtonElement>(null);
const fullAiDefaultsTriggerRef = React.useRef<HTMLButtonElement>(null);
const compactActionsTriggerRef = React.useRef<HTMLButtonElement>(null);
Expand Down Expand Up @@ -126,6 +130,12 @@ export function AgentsView() {
});
}, []);

React.useEffect(() => {
return () => {
cancelCatalogImportHandoffRef.current?.();
};
}, []);

return (
<>
<div className="flex-1 overflow-y-auto overflow-x-hidden overscroll-contain px-4 py-7 sm:px-6 sm:py-8">
Expand Down Expand Up @@ -501,7 +511,13 @@ export function AgentsView() {
personas.clearFeedback("catalog");
}}
onImportFile={(fileBytes, fileName) => {
void personas.handleImportSnapshotFile(fileBytes, fileName);
cancelCatalogImportHandoffRef.current?.();
cancelCatalogImportHandoffRef.current = scheduleAfterModalClose(
() => {
cancelCatalogImportHandoffRef.current = null;
void personas.handleImportSnapshotFile(fileBytes, fileName);
},
);
}}
onOpenChange={personas.setIsCatalogDialogOpen}
onSelectPersona={async (persona, active) => {
Expand Down
3 changes: 3 additions & 0 deletions desktop/src/shared/ui/modalMotion.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/** Matches `data-[state=closed]:duration-150` on overlay and content. */
export const MODAL_CLOSE_DURATION_MS = 150;

export const MODAL_OVERLAY_MOTION_CLASS =
"transition-none duration-200 ease-out data-[state=closed]:duration-150 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 motion-reduce:animate-none";

Expand Down
76 changes: 76 additions & 0 deletions desktop/src/shared/ui/scheduleAfterModalClose.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import assert from "node:assert/strict";
import { afterEach, describe, it } from "node:test";

import { MODAL_CLOSE_DURATION_MS } from "./modalMotion.ts";
import { scheduleAfterModalClose } from "./scheduleAfterModalClose.ts";

const originalWindow = globalThis.window;

afterEach(() => {
if (originalWindow === undefined) delete globalThis.window;
else globalThis.window = originalWindow;
});

function installTimeoutStub() {
const tasks = new Map();
let nextId = 0;
globalThis.window = {
setTimeout(callback, delay) {
const id = ++nextId;
tasks.set(id, { callback, delay });
return id;
},
clearTimeout(id) {
tasks.delete(id);
},
};
return {
flush(id) {
const entry = tasks.get(id);
assert.ok(entry, `missing timeout ${id}`);
tasks.delete(id);
entry.callback();
},
delay(id) {
return tasks.get(id)?.delay;
},
pendingCount() {
return tasks.size;
},
};
}

describe("scheduleAfterModalClose", () => {
it("does not run the task in the same turn as catalog close", () => {
const timeouts = installTimeoutStub();
let ran = false;
scheduleAfterModalClose(() => {
ran = true;
});
assert.equal(ran, false);
assert.equal(timeouts.pendingCount(), 1);
});

it("waits the closed-dialog duration, then runs", () => {
const timeouts = installTimeoutStub();
let ran = false;
const cancel = scheduleAfterModalClose(() => {
ran = true;
});
assert.equal(timeouts.delay(1), MODAL_CLOSE_DURATION_MS);
timeouts.flush(1);
assert.equal(ran, true);
cancel();
});

it("cancel prevents a late import after leaving Agents", () => {
const timeouts = installTimeoutStub();
let ran = false;
const cancel = scheduleAfterModalClose(() => {
ran = true;
});
cancel();
assert.equal(timeouts.pendingCount(), 0);
assert.equal(ran, false);
});
});
13 changes: 13 additions & 0 deletions desktop/src/shared/ui/scheduleAfterModalClose.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { MODAL_CLOSE_DURATION_MS } from "./modalMotion";

/**
* Run `task` after a Radix dialog close animation. Opening a second dialog
* in the same turn as unmounting the first leaves the new dialog painted
* but inert (GitHub #6076).
*/
export function scheduleAfterModalClose(task: () => void): () => void {
const timeoutId = window.setTimeout(task, MODAL_CLOSE_DURATION_MS);
return () => {
window.clearTimeout(timeoutId);
};
}
20 changes: 19 additions & 1 deletion desktop/tests/e2e/agents.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,25 @@ test("the new agent card opens unified create, catalog, and import flows", async
mimeType: "application/json",
name: "imported.agent.json",
});
await expect(page.getByTestId("agent-snapshot-import-dialog")).toBeVisible();
const importDialog = page.getByTestId("agent-snapshot-import-dialog");
await expect(importDialog).toBeVisible();
await expect(page.getByTestId("persona-catalog-dialog")).toHaveCount(0);
await waitForAnimations(page);
await importDialog.getByTestId("agent-snapshot-import-confirm").click();
await expect(async () => {
const log = await page.evaluate(
() =>
(
window as Window & {
__BUZZ_E2E_COMMAND_LOG__?: { command: string }[];
}
).__BUZZ_E2E_COMMAND_LOG__ ?? [],
);
expect(
log.filter((entry) => entry.command === "confirm_agent_snapshot_import")
.length,
).toBe(1);
}).toPass({ timeout: 5000 });
});

test("embedded create keeps its draft when discard is cancelled", async ({
Expand Down