From b4aeee55290c60bb5510a408ba949b815ef2b0cc Mon Sep 17 00:00:00 2001 From: richarddancin Date: Mon, 27 Jul 2026 21:53:48 +0800 Subject: [PATCH 1/3] feat(studio): add agent deletion controls --- frontend/src/ui/AgentWorkspace.css | 147 +++++++++++++++- frontend/src/ui/AgentWorkspace.tsx | 224 ++++++++++++++++++++++++- frontend/tests/agentWorkspace.test.mjs | 24 +-- 3 files changed, 378 insertions(+), 17 deletions(-) diff --git a/frontend/src/ui/AgentWorkspace.css b/frontend/src/ui/AgentWorkspace.css index 705453b2..86216916 100644 --- a/frontend/src/ui/AgentWorkspace.css +++ b/frontend/src/ui/AgentWorkspace.css @@ -197,6 +197,62 @@ overflow-y: auto; } +.aw-selection-toolbar { + flex: 0 0 auto; + min-height: 32px; + display: flex; + align-items: center; + gap: 8px; + margin-top: 10px; +} + +.aw-selection-toolbar button { + min-height: 30px; + padding: 0 10px; + border: 1px solid hsl(var(--border)); + border-radius: 8px; + background: hsl(var(--background)); + color: hsl(var(--foreground)); + cursor: pointer; + font: inherit; + font-size: 12px; + font-weight: 580; +} + +.aw-selection-toolbar button:hover:not(:disabled) { + background: hsl(var(--secondary) / 0.55); +} + +.aw-selection-toolbar button:disabled { + cursor: default; + opacity: 0.42; +} + +.aw-selection-toolbar.is-active { + padding: 6px 8px; + border: 1px solid hsl(var(--border)); + border-radius: 9px; + background: hsl(var(--secondary) / 0.3); +} + +.aw-selection-count { + flex: 1; + min-width: 0; + color: hsl(var(--muted-foreground)); + font-size: 12px; + font-weight: 550; + white-space: nowrap; +} + +.aw-selection-toolbar .aw-selection-danger { + border-color: hsl(var(--destructive) / 0.28); + color: hsl(var(--destructive)); +} + +.aw-selection-toolbar .aw-selection-danger:hover:not(:disabled) { + background: hsl(var(--destructive) / 0.08); +} + .aw-delete-error { margin-top: 8px; padding: 8px 10px; @@ -262,6 +318,44 @@ box-shadow: inset 0 -2px 0 hsl(var(--foreground) / 0.44); } +.aw-agent-item.is-selecting { + gap: 10px; +} + +.aw-agent-item.is-selected-for-delete { + border-color: hsl(var(--foreground) / 0.36); + background: hsl(var(--secondary) / 0.44); +} + +.aw-agent-item.is-selection-disabled { + opacity: 0.58; +} + +.aw-select-marker { + width: 16px; + height: 16px; + flex: 0 0 16px; + display: inline-grid; + place-items: center; + border: 1px solid hsl(var(--border)); + border-radius: 5px; + background: hsl(var(--background)); +} + +.aw-select-marker.is-checked { + border-color: hsl(var(--foreground)); + background: hsl(var(--foreground)); +} + +.aw-select-marker.is-checked::after { + content: ""; + width: 7px; + height: 4px; + border-bottom: 1.6px solid hsl(var(--background)); + border-left: 1.6px solid hsl(var(--background)); + transform: rotate(-45deg) translateY(-1px); +} + .aw-agent-copy { min-width: 0; display: flex; @@ -502,6 +596,54 @@ justify-content: center; } +.aw-head-actions { + flex: 0 0 auto; + display: flex; + align-items: center; + gap: 8px; +} + +.aw-head-delete { + min-height: 34px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 0 12px; + border: 1px solid hsl(var(--destructive) / 0.24); + border-radius: 999px; + background: hsl(var(--destructive) / 0.07); + color: hsl(var(--destructive)); + cursor: pointer; + font: inherit; + font-size: 12px; + font-weight: 620; +} + +.aw-head-delete:hover:not(:disabled) { + background: hsl(var(--destructive) / 0.12); +} + +.aw-head-delete:disabled { + cursor: default; + opacity: 0.46; +} + +.aw-head-delete svg { + width: 14px; + height: 14px; +} + +.aw-head-delete--draft { + border-color: hsl(var(--border)); + background: transparent; + color: hsl(var(--foreground)); +} + +.aw-head-delete--draft:hover:not(:disabled) { + background: hsl(var(--secondary) / 0.54); +} + .aw-head-delete.studio-update-action { border: 1px solid hsl(var(--destructive) / 0.34); background: rgba(255, 255, 255, 0.76); @@ -516,11 +658,6 @@ color: hsl(var(--destructive-foreground)); } -.aw-head-delete svg { - width: 14px; - height: 14px; -} - .aw-agent-title-row { gap: 8px; } diff --git a/frontend/src/ui/AgentWorkspace.tsx b/frontend/src/ui/AgentWorkspace.tsx index 0dc6af4a..3fa0d22f 100644 --- a/frontend/src/ui/AgentWorkspace.tsx +++ b/frontend/src/ui/AgentWorkspace.tsx @@ -341,6 +341,9 @@ export function AgentWorkspace({ const [draggingAgentId, setDraggingAgentId] = useState(""); const [dropAgentId, setDropAgentId] = useState(""); const [dropPlacement, setDropPlacement] = useState<"before" | "after">("before"); + const [selectionMode, setSelectionMode] = useState(false); + const [selectedAgentIds, setSelectedAgentIds] = useState>(() => new Set()); + const [selectedDraftIds, setSelectedDraftIds] = useState>(() => new Set()); const [deletingAgents, setDeletingAgents] = useState(false); const [deleteError, setDeleteError] = useState(""); const suppressAgentClickRef = useRef(false); @@ -473,6 +476,16 @@ export function AgentWorkspace({ const selectedEvaluationGroup = evaluationGroups.find( (group) => group.id === activeEvaluationGroupId, ); + const deletableListedAgents = listedAgents.filter((agent) => agent.canDelete === true); + const selectedDeletableAgents = listedAgents.filter( + (agent) => selectedAgentIds.has(agent.id) && agent.canDelete === true, + ); + const selectedDeletableDrafts = filteredDrafts.filter((item) => + selectedDraftIds.has(item.id), + ); + const deletableItemCount = deletableListedAgents.length + filteredDrafts.length; + const selectedDeleteCount = + selectedDeletableAgents.length + selectedDeletableDrafts.length; const draft = useMemo( () => selectedPendingTask?.agentDraft ?? @@ -557,6 +570,26 @@ export function AgentWorkspace({ }; }, [selectedAgent?.region, selectedAgent?.runtimeId]); + useEffect(() => { + const selectableIds = new Set( + listedAgents + .filter((agent) => agent.canDelete === true) + .map((agent) => agent.id), + ); + setSelectedAgentIds((current) => { + const next = new Set([...current].filter((id) => selectableIds.has(id))); + return next.size === current.size ? current : next; + }); + }, [listedAgents]); + + useEffect(() => { + const selectableIds = new Set(filteredDrafts.map((item) => item.id)); + setSelectedDraftIds((current) => { + const next = new Set([...current].filter((id) => selectableIds.has(id))); + return next.size === current.size ? current : next; + }); + }, [filteredDrafts]); + const visibleCases = cases.filter((item) => { if (item.kind !== caseFilter) return false; const keyword = caseQuery.trim().toLowerCase(); @@ -617,6 +650,82 @@ export function AgentWorkspace({ onAgentOrderChange(next); }; + const toggleAgentSelection = (agent: AgentEntry) => { + if (agent.canDelete !== true) return; + setDeleteError(""); + setSelectedAgentIds((current) => { + const next = new Set(current); + if (next.has(agent.id)) { + next.delete(agent.id); + } else { + next.add(agent.id); + } + return next; + }); + }; + + const toggleDraftSelection = (draftItem: WorkspaceAgentDraft) => { + setDeleteError(""); + setSelectedDraftIds((current) => { + const next = new Set(current); + if (next.has(draftItem.id)) { + next.delete(draftItem.id); + } else { + next.add(draftItem.id); + } + return next; + }); + }; + + const selectAllListedAgents = () => { + setDeleteError(""); + setSelectedAgentIds(new Set(deletableListedAgents.map((agent) => agent.id))); + setSelectedDraftIds(new Set(filteredDrafts.map((item) => item.id))); + }; + + const clearAgentSelection = () => { + setDeleteError(""); + setSelectedAgentIds(new Set()); + setSelectedDraftIds(new Set()); + setSelectionMode(false); + }; + + const deleteSelectedItems = async () => { + if (selectedDeleteCount === 0 || deletingAgents) return; + const runtimeCount = selectedDeletableAgents.length; + const draftCount = selectedDeletableDrafts.length; + const confirmText = runtimeCount === 1 && draftCount === 0 + ? `确定删除 Agent "${selectedDeletableAgents[0].label}"?该 Runtime 将被永久删除。` + : runtimeCount === 0 && draftCount === 1 + ? `确定删除草稿 "${selectedDeletableDrafts[0].draft.name || "未命名 Agent"}"?` + : `确定删除选中的 ${selectedDeleteCount} 个项目?${runtimeCount > 0 ? `${runtimeCount} 个 Runtime 将被永久删除。` : ""}`; + if (!window.confirm(confirmText)) return; + setDeletingAgents(true); + setDeleteError(""); + try { + if (selectedDeletableAgents.length > 0) { + if (!onDeleteAgents) throw new Error("当前页面不支持删除已部署 Agent。"); + await onDeleteAgents(selectedDeletableAgents); + } + if (selectedDeletableDrafts.length > 0) { + onDeleteDrafts?.(selectedDeletableDrafts); + } + setSelectedAgentIds(new Set()); + setSelectedDraftIds(new Set()); + setSelectionMode(false); + if (selectedDeletableAgents.some((agent) => agent.id === activeAgentId)) { + setActiveAgentId(""); + } + if (selectedDeletableDrafts.some((item) => item.id === activeDraftId)) { + setActiveDraftId(""); + } + } catch (cause) { + setDeleteError(cause instanceof Error ? cause.message : String(cause)); + } finally { + setDeletingAgents(false); + } + }; + const deleteSingleAgent = async (agent: AgentEntry) => { if (!onDeleteAgents || agent.canDelete !== true || deletingAgents) return; if (!window.confirm(`确定删除 Agent "${agent.label}"?该 Runtime 将被永久删除。`)) { @@ -726,6 +835,50 @@ export function AgentWorkspace({ {view === "library" ? "新建 Agent" : "新建评测组"} + {view === "library" && (onDeleteAgents || onDeleteDrafts) && ( +
+ {selectionMode ? ( + <> + + 已选 {selectedDeleteCount} 个 + + + + + + ) : ( + + )} +
+ )} {view === "library" && deleteError && (
{deleteError}
)} @@ -772,18 +925,35 @@ export function AgentWorkspace({ candidate.runtimeId === item.deploymentTarget.runtimeId), ) .sort((left, right) => right.startedAt - left.startedAt)[0]; + const isSelectedForDelete = selectedDraftIds.has(item.id); return ( + )} + {selectedAgent?.canDelete && ( + + )} + + )}