Skip to content

Commit 72abe89

Browse files
committed
feat: add delete confirmation for sessions, skills, and MCP unmount
Replace browser native window.confirm with styled inline confirmation dialogs matching the app theme. Session delete, clear-all sessions, and skill/MCP unmount from agents all now require explicit confirmation.
1 parent bfdb606 commit 72abe89

4 files changed

Lines changed: 186 additions & 37 deletions

File tree

‎src/components/layout/Sidebar.tsx‎

Lines changed: 95 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useState, type KeyboardEvent, type MouseEvent } from 'react'
1+
import { useCallback, useEffect, useState, type KeyboardEvent, type MouseEvent } from 'react'
22
import { useTranslation } from 'react-i18next'
33
import {
44
MessageSquare,
@@ -71,14 +71,46 @@ export function Sidebar({
7171
[activeSessionId, setActiveSession, onNavigate],
7272
)
7373

74+
const [confirmDeleteSessionId, setConfirmDeleteSessionId] = useState<string | null>(null)
75+
const [confirmClearAll, setConfirmClearAll] = useState(false)
76+
7477
const handleDelete = useCallback(
78+
(e: MouseEvent, id: string) => {
79+
e.stopPropagation()
80+
setConfirmDeleteSessionId(id)
81+
},
82+
[],
83+
)
84+
85+
const handleConfirmDelete = useCallback(
7586
(e: MouseEvent, id: string) => {
7687
e.stopPropagation()
7788
deleteSession(id)
89+
setConfirmDeleteSessionId(null)
7890
},
7991
[deleteSession],
8092
)
8193

94+
const handleCancelDelete = useCallback(
95+
(e: MouseEvent) => {
96+
e.stopPropagation()
97+
setConfirmDeleteSessionId(null)
98+
},
99+
[],
100+
)
101+
102+
useEffect(() => {
103+
if (!confirmDeleteSessionId && !confirmClearAll) return
104+
const onKey = (e: globalThis.KeyboardEvent) => {
105+
if (e.key === 'Escape') {
106+
setConfirmDeleteSessionId(null)
107+
setConfirmClearAll(false)
108+
}
109+
}
110+
window.addEventListener('keydown', onKey)
111+
return () => window.removeEventListener('keydown', onKey)
112+
}, [confirmDeleteSessionId, confirmClearAll])
113+
82114
const applyWorkspaceDir = useCallback((dir: string) => {
83115
setWorkspaceDir(dir)
84116
setShowPathInput(false)
@@ -192,17 +224,33 @@ export function Sidebar({
192224
</div>
193225
<div className="flex items-center gap-1.5">
194226
{!isFilesMode && (
195-
<button
196-
onClick={() => {
197-
if (window.confirm(t('sidebar.clearConfirm'))) {
198-
useAppStore.getState().clearSessions()
199-
}
200-
}}
201-
className="flex h-7 w-7 items-center justify-center rounded-md text-text-quaternary transition-all duration-300 hover:bg-red-500/10 hover:text-red-500"
202-
title={t('sidebar.clearTitle')}
203-
>
204-
<Trash2 size={11} strokeWidth={2} />
205-
</button>
227+
confirmClearAll ? (
228+
<div className="flex items-center gap-1">
229+
<button
230+
onClick={() => {
231+
useAppStore.getState().clearSessions()
232+
setConfirmClearAll(false)
233+
}}
234+
className="rounded-md bg-red-500 px-2 py-1 text-[10px] font-black text-white transition-all duration-300 hover:bg-red-600"
235+
>
236+
{t('common.confirm')}
237+
</button>
238+
<button
239+
onClick={() => setConfirmClearAll(false)}
240+
className="rounded-md bg-white/75 px-2 py-1 text-[10px] font-black text-text-tertiary transition-all duration-300 hover:text-text dark:bg-white/10"
241+
>
242+
{t('common.cancel')}
243+
</button>
244+
</div>
245+
) : (
246+
<button
247+
onClick={() => setConfirmClearAll(true)}
248+
className="flex h-7 w-7 items-center justify-center rounded-md text-text-quaternary transition-all duration-300 hover:bg-red-500/10 hover:text-red-500"
249+
title={t('sidebar.clearTitle')}
250+
>
251+
<Trash2 size={11} strokeWidth={2} />
252+
</button>
253+
)
206254
)}
207255
<motion.button
208256
whileHover={{ scale: 1.05, x: -2 }}
@@ -327,27 +375,41 @@ export function Sidebar({
327375
)}
328376
</div>
329377

330-
<div
331-
onClick={(e) => {
332-
e.stopPropagation()
333-
handleDelete(e, session.id)
334-
}}
335-
role="button"
336-
tabIndex={0}
337-
onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => {
338-
if (e.key === 'Enter' || e.key === ' ') {
339-
e.stopPropagation()
340-
handleDelete(e as unknown as MouseEvent, session.id)
341-
}
342-
}}
343-
className={`cursor-pointer rounded-md p-1 transition-all duration-300 ${
344-
isActive
345-
? 'text-accent/60 hover:text-red-500 hover:bg-red-500/10'
346-
: 'text-text-quaternary hover:text-red-500 hover:bg-red-500/10 opacity-0 group-hover:opacity-100'
347-
}`}
348-
>
349-
<Trash2 size={12} strokeWidth={1.5} />
350-
</div>
378+
{confirmDeleteSessionId === session.id ? (
379+
<div className="flex shrink-0 items-center gap-1">
380+
<button
381+
onClick={(e) => handleConfirmDelete(e, session.id)}
382+
className="rounded-md bg-red-500 px-2 py-1 text-[10px] font-black text-white transition-all duration-300 hover:bg-red-600"
383+
>
384+
{t('common.confirm')}
385+
</button>
386+
<button
387+
onClick={handleCancelDelete}
388+
className="rounded-md bg-white/75 px-2 py-1 text-[10px] font-black text-text-tertiary transition-all duration-300 hover:text-text dark:bg-white/10"
389+
>
390+
{t('common.cancel')}
391+
</button>
392+
</div>
393+
) : (
394+
<div
395+
onClick={(e) => handleDelete(e, session.id)}
396+
role="button"
397+
tabIndex={0}
398+
onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => {
399+
if (e.key === 'Enter' || e.key === ' ') {
400+
e.stopPropagation()
401+
handleDelete(e as unknown as MouseEvent, session.id)
402+
}
403+
}}
404+
className={`cursor-pointer rounded-md p-1 transition-all duration-300 ${
405+
isActive
406+
? 'text-accent/60 hover:text-red-500 hover:bg-red-500/10'
407+
: 'text-text-quaternary hover:text-red-500 hover:bg-red-500/10 opacity-0 group-hover:opacity-100'
408+
}`}
409+
>
410+
<Trash2 size={12} strokeWidth={1.5} />
411+
</div>
412+
)}
351413

352414
{isActive && (
353415
<div className="absolute left-0 top-2 bottom-2 z-20 w-1 rounded-r-full bg-accent shadow-[2px_0_8px_rgba(var(--color-accent-rgb),0.3)]" />

‎src/components/workspace/WorkspacePanel.tsx‎

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export function WorkspacePanel() {
2020
const [showCreateAdvanced, setShowCreateAdvanced] = useState(false)
2121
const [showDetailAdvanced, setShowDetailAdvanced] = useState(false)
2222
const [confirmDeleteAgentId, setConfirmDeleteAgentId] = useState<string | null>(null)
23+
const [confirmRemoveSkillName, setConfirmRemoveSkillName] = useState<string | null>(null)
24+
const [confirmRemoveMcpServer, setConfirmRemoveMcpServer] = useState<string | null>(null)
2325
const [busyInstallKey, setBusyInstallKey] = useState<string | null>(null)
2426
const [remoteSkillsLoading, setRemoteSkillsLoading] = useState(false)
2527
const [remoteSkillsError, setRemoteSkillsError] = useState<string | null>(null)
@@ -533,8 +535,7 @@ export function WorkspacePanel() {
533535
key: name,
534536
label: `${skillMeta[name]?.emoji || '✦'} ${name}`,
535537
onRemove: () => {
536-
removeSkillFromAgent(activeAgent.id, name)
537-
setFeedback(t('agentStudio.feedback.skillRemoved', { name }))
538+
setConfirmRemoveSkillName(name)
538539
},
539540
})) : undefined}
540541
/>
@@ -546,8 +547,7 @@ export function WorkspacePanel() {
546547
key: name,
547548
label: name,
548549
onRemove: () => {
549-
removeMCPServerFromAgent(activeAgent.id, name)
550-
setFeedback(t('agentStudio.feedback.mcpRemoved', { name }))
550+
setConfirmRemoveMcpServer(name)
551551
},
552552
})) : undefined}
553553
/>
@@ -705,6 +705,34 @@ export function WorkspacePanel() {
705705
onSubmit={handleCreateAgent}
706706
/>
707707
)}
708+
709+
{(confirmRemoveSkillName || confirmRemoveMcpServer) && (
710+
<ConfirmRemoveModal
711+
title={confirmRemoveSkillName
712+
? t('agentStudio.confirm.removeSkillTitle', { name: confirmRemoveSkillName })
713+
: t('agentStudio.confirm.removeMcpTitle', { name: confirmRemoveMcpServer })
714+
}
715+
message={confirmRemoveSkillName
716+
? t('agentStudio.confirm.removeSkillMessage')
717+
: t('agentStudio.confirm.removeMcpMessage')
718+
}
719+
onConfirm={() => {
720+
if (confirmRemoveSkillName && activeAgent) {
721+
removeSkillFromAgent(activeAgent.id, confirmRemoveSkillName)
722+
setFeedback(t('agentStudio.feedback.skillRemoved', { name: confirmRemoveSkillName }))
723+
setConfirmRemoveSkillName(null)
724+
} else if (confirmRemoveMcpServer && activeAgent) {
725+
removeMCPServerFromAgent(activeAgent.id, confirmRemoveMcpServer)
726+
setFeedback(t('agentStudio.feedback.mcpRemoved', { name: confirmRemoveMcpServer }))
727+
setConfirmRemoveMcpServer(null)
728+
}
729+
}}
730+
onCancel={() => {
731+
setConfirmRemoveSkillName(null)
732+
setConfirmRemoveMcpServer(null)
733+
}}
734+
/>
735+
)}
708736
</aside>
709737
)
710738
}
@@ -820,6 +848,53 @@ function TagPanel({
820848
)
821849
}
822850

851+
function ConfirmRemoveModal({
852+
title,
853+
message,
854+
onConfirm,
855+
onCancel,
856+
}: {
857+
title: string
858+
message: string
859+
onConfirm: () => void
860+
onCancel: () => void
861+
}) {
862+
const { t } = useTranslation()
863+
864+
return (
865+
<div
866+
className="fixed inset-0 z-50 flex items-center justify-center bg-black/30 backdrop-blur-sm"
867+
onClick={onCancel}
868+
>
869+
<div
870+
className="mx-4 w-full max-w-xs rounded-2xl border border-border-light/70 bg-white/95 p-6 shadow-2xl backdrop-blur-xl dark:bg-gray-900/95"
871+
onClick={(e) => e.stopPropagation()}
872+
>
873+
<p className="text-[13px] font-bold tracking-tight text-text">
874+
{title}
875+
</p>
876+
<p className="mt-2 text-[11px] leading-relaxed text-text-tertiary">
877+
{message}
878+
</p>
879+
<div className="mt-5 flex items-center gap-3">
880+
<button
881+
onClick={onConfirm}
882+
className="flex-1 rounded-full bg-red-500 px-4 py-2 text-[11px] font-black text-white transition-all duration-300 hover:bg-red-600"
883+
>
884+
{t('common.confirm')}
885+
</button>
886+
<button
887+
onClick={onCancel}
888+
className="flex-1 rounded-full bg-white/75 px-4 py-2 text-[11px] font-black text-text-tertiary transition-all duration-300 hover:text-text dark:bg-white/10"
889+
>
890+
{t('common.cancel')}
891+
</button>
892+
</div>
893+
</div>
894+
</div>
895+
)
896+
}
897+
823898
function PickerModal({
824899
title,
825900
emptyText,

‎src/i18n/locales/en.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,12 @@ const en = {
511511
instructionsPlaceholder: 'Add extra instructions or style rules for this agent',
512512
modelPlaceholder: 'Optional model override, for example deepseek-chat',
513513
},
514+
confirm: {
515+
removeSkillTitle: 'Remove skill "{{name}}"?',
516+
removeSkillMessage: 'This skill will be unmounted from the current agent.',
517+
removeMcpTitle: 'Remove MCP server "{{name}}"?',
518+
removeMcpMessage: 'This MCP server will be unmounted from the current agent.',
519+
},
514520
feedback: {
515521
skillInstalled: 'Installed skill: {{name}}',
516522
skillRemoved: 'Removed skill: {{name}}',

‎src/i18n/locales/zh.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,12 @@ const zh = {
511511
instructionsPlaceholder: '补充这套智能体的额外指令或风格要求',
512512
modelPlaceholder: '可选覆盖模型,例如 deepseek-chat',
513513
},
514+
confirm: {
515+
removeSkillTitle: '移除 Skill "{{name}}"?',
516+
removeSkillMessage: '该 Skill 将从当前智能体卸载。',
517+
removeMcpTitle: '移除 MCP 服务 "{{name}}"?',
518+
removeMcpMessage: '该 MCP 服务将从当前智能体卸载。',
519+
},
514520
feedback: {
515521
skillInstalled: '已安装 Skill:{{name}}',
516522
skillRemoved: '已移除 Skill:{{name}}',

0 commit comments

Comments
 (0)