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

View File

@ -146,8 +146,9 @@ vi.mock("@/hooks/useSessions", async (importOriginal) => {
refresh: refreshSpy, refresh: refreshSpy,
createChat: createChatSpy, createChat: createChatSpy,
forkChat: async () => "fork-chat", forkChat: async () => "fork-chat",
deleteChat: async (key: string) => { deleteChat: async (key: string, options?: { deleteAutomations?: boolean }) => {
await deleteChatSpy(key); if (options === undefined) await deleteChatSpy(key);
else await deleteChatSpy(key, options);
setSessions((prev: ChatSummary[]) => prev.filter((s) => s.key !== key)); setSessions((prev: ChatSummary[]) => prev.filter((s) => s.key !== key));
return { deleted: true }; return { deleted: true };
}, },
@ -434,6 +435,73 @@ describe("App layout", () => {
expect(document.body.style.pointerEvents).not.toBe("none"); expect(document.body.style.pointerEvents).not.toBe("none");
}, 15_000); }, 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 () => { it("keeps the mobile session action menu inside the sidebar sheet", async () => {
mockSessions = [ mockSessions = [
{ {