fix: show cron bindings before deleting sessions

This commit is contained in:
chengyongru 2026-06-12 17:02:29 +08:00
parent 8335554894
commit 2248527971
2 changed files with 85 additions and 9 deletions

View File

@ -43,7 +43,7 @@ import type {
} from "@/lib/types";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { fetchSettings, fetchWorkspaces } from "@/lib/api";
import { fetchSessionAutomations, fetchSettings, fetchWorkspaces } from "@/lib/api";
import {
createRuntimeHost,
getHostApi,
@ -548,7 +548,6 @@ function Shell({
key: string;
label: string;
automations?: SessionAutomationJob[];
confirmAutomations?: boolean;
} | null>(null);
const [pendingRename, setPendingRename] = useState<{
key: string;
@ -1273,6 +1272,7 @@ function Shell({
const onConfirmDelete = useCallback(async () => {
if (!pendingDelete) return;
const key = pendingDelete.key;
const hasAutomations = (pendingDelete.automations?.length ?? 0) > 0;
const deletingActive = activeKey === key;
const currentIndex = sessions.findIndex((s) => s.key === key);
const fallbackKey = deletingActive
@ -1281,13 +1281,12 @@ function Shell({
try {
const result = await deleteChat(
key,
pendingDelete.confirmAutomations ? { deleteAutomations: true } : undefined,
hasAutomations ? { deleteAutomations: true } : undefined,
);
if (result.blocked_by_automations) {
setPendingDelete({
...pendingDelete,
automations: result.automations ?? [],
confirmAutomations: true,
});
return;
}
@ -1304,6 +1303,16 @@ function Shell({
}
}, [pendingDelete, deleteChat, activeKey, navigate, sessions]);
const onRequestDelete = useCallback(async (key: string, label: string) => {
let automations: SessionAutomationJob[] = [];
try {
automations = (await fetchSessionAutomations(token, key)).jobs;
} catch {
// Delete remains protected by the backend block; prefetch only improves the first prompt.
}
setPendingDelete({ key, label, automations });
}, [token]);
const headerTitle = activeSession
? sidebarState.title_overrides[activeSession.key] ||
activeSession.title ||
@ -1340,8 +1349,7 @@ function Shell({
loading,
onNewChat,
onSelect: onSelectChat,
onRequestDelete: (key: string, label: string) =>
setPendingDelete({ key, label }),
onRequestDelete,
onTogglePin,
onRequestRename,
onToggleArchive,
@ -1566,7 +1574,7 @@ function Shell({
<DeleteConfirm
open={!!pendingDelete}
title={pendingDelete?.label ?? ""}
automations={pendingDelete?.confirmAutomations ? pendingDelete.automations : undefined}
automations={pendingDelete?.automations}
onCancel={() => setPendingDelete(null)}
onConfirm={onConfirmDelete}
/>

View File

@ -146,8 +146,9 @@ vi.mock("@/hooks/useSessions", async (importOriginal) => {
refresh: refreshSpy,
createChat: createChatSpy,
forkChat: async () => "fork-chat",
deleteChat: async (key: string) => {
await deleteChatSpy(key);
deleteChat: async (key: string, options?: { deleteAutomations?: boolean }) => {
if (options === undefined) await deleteChatSpy(key);
else await deleteChatSpy(key, options);
setSessions((prev: ChatSummary[]) => prev.filter((s) => s.key !== key));
return { deleted: true };
},
@ -434,6 +435,73 @@ describe("App layout", () => {
expect(document.body.style.pointerEvents).not.toBe("none");
}, 15_000);
it("shows bound automations in the first delete confirmation", async () => {
mockSessions = [
{
key: "websocket:chat-a",
channel: "websocket",
chatId: "chat-a",
createdAt: "2026-04-16T10:00:00Z",
updatedAt: "2026-04-16T10:00:00Z",
preview: "First chat",
},
{
key: "websocket:chat-b",
channel: "websocket",
chatId: "chat-b",
createdAt: "2026-04-16T11:00:00Z",
updatedAt: "2026-04-16T11:00:00Z",
preview: "Second chat",
},
];
mockFetchRoutes({
"/api/sessions/websocket%3Achat-a/automations": {
jobs: [
{
id: "job-1",
name: "Daily repo check",
enabled: true,
schedule: { kind: "every", every_ms: 86_400_000 },
payload: { message: "Check the repo" },
state: { next_run_at_ms: Date.UTC(2026, 3, 17, 10, 0, 0) },
},
],
},
});
render(<App />);
await waitFor(() => expect(connectSpy).toHaveBeenCalled());
const sidebar = screen.getByRole("navigation", { name: "Sidebar navigation" });
await waitFor(() =>
expect(
within(sidebar).getByRole("button", { name: /^First chat$/ }),
).toBeInTheDocument(),
);
fireEvent.pointerDown(screen.getByLabelText("Chat actions for First chat"), {
button: 0,
});
fireEvent.click(await screen.findByRole("menuitem", { name: "Delete" }));
await waitFor(() =>
expect(screen.getByText("Daily repo check")).toBeInTheDocument(),
);
expect(
screen.getByText("This chat has scheduled automations. Deleting it will also delete them."),
).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Delete chat and automations" }));
await waitFor(() =>
expect(deleteChatSpy).toHaveBeenCalledWith("websocket:chat-a", {
deleteAutomations: true,
}),
);
expect(deleteChatSpy).toHaveBeenCalledTimes(1);
expect(screen.queryByText("Daily repo check")).not.toBeInTheDocument();
}, 15_000);
it("keeps the mobile session action menu inside the sidebar sheet", async () => {
mockSessions = [
{