diff --git a/desktop/src/__tests__/diagnosticsSettings.test.tsx b/desktop/src/__tests__/diagnosticsSettings.test.tsx index 6c518b2e..f9fa0811 100644 --- a/desktop/src/__tests__/diagnosticsSettings.test.tsx +++ b/desktop/src/__tests__/diagnosticsSettings.test.tsx @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { fireEvent, render, screen, waitFor, within } from '@testing-library/react' import '@testing-library/jest-dom' import { Settings } from '../pages/Settings' @@ -90,6 +90,7 @@ vi.mock('../components/chat/CodeViewer', () => ({ describe('Settings > Diagnostics tab', () => { beforeEach(() => { + vi.clearAllMocks() diagnosticsApiMock.getStatus.mockResolvedValue({ logDir: '/tmp/claude/cc-haha/diagnostics', diagnosticsPath: '/tmp/claude/cc-haha/diagnostics/diagnostics.jsonl', @@ -168,6 +169,36 @@ describe('Settings > Diagnostics tab', () => { expect(await screen.findByText('/tmp/claude/cc-haha/diagnostics/exports/cc-haha-diagnostics.tar.gz')).toBeInTheDocument() }) + it('asks with the shared confirm dialog before clearing diagnostics', async () => { + const confirmSpy = vi.spyOn(window, 'confirm').mockImplementation(() => { + throw new Error('window.confirm should not be used') + }) + + try { + render() + + fireEvent.click(screen.getByText('Diagnostics')) + fireEvent.click(await screen.findByRole('button', { name: /Clear Logs/i })) + + const dialog = await screen.findByRole('dialog', { name: 'Clear Logs' }) + expect(within(dialog).getByText('Clear all local diagnostic logs and exported bundles?')).toBeInTheDocument() + + fireEvent.click(within(dialog).getByRole('button', { name: /Cancel/i })) + expect(diagnosticsApiMock.clear).not.toHaveBeenCalled() + + fireEvent.click(screen.getByRole('button', { name: /Clear Logs/i })) + const confirmDialog = await screen.findByRole('dialog', { name: 'Clear Logs' }) + fireEvent.click(within(confirmDialog).getByRole('button', { name: /Clear Logs/i })) + + await waitFor(() => { + expect(diagnosticsApiMock.clear).toHaveBeenCalledTimes(1) + }) + expect(confirmSpy).not.toHaveBeenCalled() + } finally { + confirmSpy.mockRestore() + } + }) + it('copies the recent error summary with the legacy clipboard fallback', async () => { const originalClipboard = navigator.clipboard const originalExecCommand = document.execCommand diff --git a/desktop/src/components/controls/PermissionModeSelector.test.tsx b/desktop/src/components/controls/PermissionModeSelector.test.tsx index 19fa1c54..8998cca0 100644 --- a/desktop/src/components/controls/PermissionModeSelector.test.tsx +++ b/desktop/src/components/controls/PermissionModeSelector.test.tsx @@ -154,6 +154,7 @@ describe('PermissionModeSelector', () => { fireEvent.click(screen.getByRole('button', { name: 'Ask permissions' })) fireEvent.click(screen.getByRole('menuitem', { name: /Bypass permissions/ })) + expect(screen.getByRole('dialog', { name: 'Enable bypass mode' })).toBeInTheDocument() expect(screen.getByText('C:\\Users\\LinTan\\MyScript\\test5')).toBeInTheDocument() expect(screen.queryByText('C:\\Users\\LinTan')).not.toBeInTheDocument() }) diff --git a/desktop/src/components/controls/PermissionModeSelector.tsx b/desktop/src/components/controls/PermissionModeSelector.tsx index f695b7d6..72fffb56 100644 --- a/desktop/src/components/controls/PermissionModeSelector.tsx +++ b/desktop/src/components/controls/PermissionModeSelector.tsx @@ -1,6 +1,5 @@ import { useState, useRef, useEffect } from 'react' import DOMPurify from 'dompurify' -import { createPortal } from 'react-dom' import { useSettingsStore } from '../../stores/settingsStore' import { useChatStore } from '../../stores/chatStore' import { useSessionStore } from '../../stores/sessionStore' @@ -10,6 +9,7 @@ import type { PermissionMode } from '../../types/settings' import { useMobileViewport } from '../../hooks/useMobileViewport' import { isTauriRuntime } from '../../lib/desktopRuntime' import { MobileBottomSheet } from '../shared/MobileBottomSheet' +import { ActionDialog } from '../shared/ActionDialog' const MODE_ICONS: Record = { default: 'verified_user', @@ -213,73 +213,60 @@ export function PermissionModeSelector({ workDir: workDirProp, compact = false, ) )} - {/* Bypass confirmation dialog */} - {confirmDialog && createPortal( -
setConfirmDialog(false)}> -
e.stopPropagation()} - > - {/* Header */} -
-
- warning -
-
-
{t('permMode.enableBypassTitle')}
-
{t('permMode.enableBypassSubtitle')}
-
-
- - {/* Body */} -
-

-

- folder - {workDir} -
-
    -
  • - check - {t('permMode.permReadWrite')} -
  • -
  • - check - {t('permMode.permShell')} -
  • -
  • - check - {t('permMode.permPackages')} -
  • -
-
- - {/* Actions */} -
- - + setConfirmDialog(false)} + title={t('permMode.enableBypassTitle')} + width={420} + body={( +
+

+ {t('permMode.enableBypassSubtitle')} +

+

+

+ folder + {workDir}
+
    +
  • + check + {t('permMode.permReadWrite')} +
  • +
  • + check + {t('permMode.permShell')} +
  • +
  • + check + {t('permMode.permPackages')} +
  • +
-
, - document.body, - )} + )} + actions={[ + { + label: t('common.cancel'), + onClick: () => setConfirmDialog(false), + variant: 'secondary', + }, + { + label: t('permMode.enableBypassBtn'), + onClick: () => { + if (isControlled) { + onChange?.('bypassPermissions') + } else if (activeTabId) { + setSessionPermissionMode(activeTabId, 'bypassPermissions') + } + setConfirmDialog(false) + }, + variant: 'danger', + }, + ]} + />
) } diff --git a/desktop/src/components/layout/TabBar.test.tsx b/desktop/src/components/layout/TabBar.test.tsx index 6ada6b08..424de3b0 100644 --- a/desktop/src/components/layout/TabBar.test.tsx +++ b/desktop/src/components/layout/TabBar.test.tsx @@ -940,6 +940,7 @@ describe('TabBar', () => { fireEvent.click(screen.getByText('Close All')) expect(screen.getByText('Sessions Running')).toBeInTheDocument() + expect(screen.getByRole('dialog', { name: 'Sessions Running' })).toBeInTheDocument() expect(screen.getByText('2 sessions still running')).toBeInTheDocument() expect(useTabStore.getState().tabs.map((tab) => tab.sessionId)).toEqual(['tab-running', 'tab-thinking', 'tab-idle']) expect(disconnectSession).not.toHaveBeenCalled() diff --git a/desktop/src/components/layout/TabBar.tsx b/desktop/src/components/layout/TabBar.tsx index cebcaaa3..70d502b9 100644 --- a/desktop/src/components/layout/TabBar.tsx +++ b/desktop/src/components/layout/TabBar.tsx @@ -16,6 +16,7 @@ import { useTranslation } from '../../i18n' import { WindowControls, showWindowControls } from './WindowControls' import { OpenProjectMenu } from './OpenProjectMenu' import { Folder, FolderOpen, Globe, SquareTerminal } from 'lucide-react' +import { ActionDialog } from '../shared/ActionDialog' const TAB_WIDTH = 180 const DRAG_START_THRESHOLD = 4 @@ -487,47 +488,43 @@ export function TabBar() {
)} - {pendingCloseRequest && ( -
-
-

- {pendingCloseRequest.runningSessionIds.length > 1 - ? t('tabs.closeAllConfirmTitle') - : t('tabs.closeConfirmTitle')} -

-

- {pendingCloseRequest.runningSessionIds.length > 1 - ? t('tabs.closeAllConfirmMessage', { count: pendingCloseRequest.runningSessionIds.length }) - : t('tabs.closeConfirmMessage')} -

-
- - - -
-
-
- )} + setPendingCloseRequest(null)} + title={pendingCloseRequest && pendingCloseRequest.runningSessionIds.length > 1 + ? t('tabs.closeAllConfirmTitle') + : t('tabs.closeConfirmTitle')} + body={pendingCloseRequest && pendingCloseRequest.runningSessionIds.length > 1 + ? t('tabs.closeAllConfirmMessage', { count: pendingCloseRequest.runningSessionIds.length }) + : t('tabs.closeConfirmMessage')} + actions={[ + { + label: t('common.cancel'), + onClick: () => setPendingCloseRequest(null), + variant: 'secondary', + }, + { + label: t('tabs.closeConfirmKeep'), + onClick: () => { + if (!pendingCloseRequest) return + closeTabsWithPolicy(pendingCloseRequest.tabs, pendingCloseRequest.runningSessionIds, false) + setPendingCloseRequest(null) + }, + variant: 'secondary', + }, + { + label: pendingCloseRequest && pendingCloseRequest.runningSessionIds.length > 1 + ? t('tabs.closeAllConfirmStop') + : t('tabs.closeConfirmStop'), + onClick: () => { + if (!pendingCloseRequest) return + closeTabsWithPolicy(pendingCloseRequest.tabs, pendingCloseRequest.runningSessionIds, true) + setPendingCloseRequest(null) + }, + variant: 'danger', + }, + ]} + /> ) } diff --git a/desktop/src/components/shared/ActionDialog.tsx b/desktop/src/components/shared/ActionDialog.tsx new file mode 100644 index 00000000..c3fdb07f --- /dev/null +++ b/desktop/src/components/shared/ActionDialog.tsx @@ -0,0 +1,66 @@ +import type { ReactNode } from 'react' +import { Button } from './Button' +import { Modal } from './Modal' + +type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost' + +export type ActionDialogAction = { + label: string + onClick: () => void | Promise + variant?: ButtonVariant + loading?: boolean + disabled?: boolean +} + +type ActionDialogProps = { + open: boolean + onClose: () => void + title: string + body: ReactNode + actions: ActionDialogAction[] + width?: number + loading?: boolean +} + +export function ActionDialog({ + open, + onClose, + title, + body, + actions, + width = 460, + loading = false, +}: ActionDialogProps) { + const busy = loading || actions.some((action) => action.loading) + + return ( + {} : onClose} + title={title} + width={width} + footer={( + <> + {actions.map((action) => ( + + ))} + + )} + > + {typeof body === 'string' ? ( +

+ {body} +

+ ) : body} +
+ ) +} diff --git a/desktop/src/components/shared/ConfirmDialog.tsx b/desktop/src/components/shared/ConfirmDialog.tsx index 82bc4043..15665f55 100644 --- a/desktop/src/components/shared/ConfirmDialog.tsx +++ b/desktop/src/components/shared/ConfirmDialog.tsx @@ -1,6 +1,5 @@ -import { Modal } from './Modal' -import { Button } from './Button' import type { ReactNode } from 'react' +import { ActionDialog } from './ActionDialog' type ConfirmDialogProps = { open: boolean @@ -26,27 +25,25 @@ export function ConfirmDialog({ loading = false, }: ConfirmDialogProps) { return ( - {} : onClose} + onClose={onClose} title={title} - width={460} - footer={( - <> - - - - )} - > - {typeof body === 'string' ? ( -

- {body} -

- ) : body} -
+ body={body} + loading={loading} + actions={[ + { + label: cancelLabel, + onClick: onClose, + variant: 'secondary', + }, + { + label: confirmLabel, + onClick: onConfirm, + variant: confirmVariant, + loading, + }, + ]} + /> ) } diff --git a/desktop/src/components/shared/ConfirmPopover.tsx b/desktop/src/components/shared/ConfirmPopover.tsx new file mode 100644 index 00000000..0a70e5e8 --- /dev/null +++ b/desktop/src/components/shared/ConfirmPopover.tsx @@ -0,0 +1,33 @@ +import { Button } from './Button' + +type ConfirmPopoverProps = { + message: string + confirmLabel: string + onConfirm: () => void + onCancel: () => void + cancelLabel: string + confirmVariant?: 'primary' | 'danger' +} + +export function ConfirmPopover({ + message, + confirmLabel, + onConfirm, + onCancel, + cancelLabel, + confirmVariant = 'primary', +}: ConfirmPopoverProps) { + return ( +
+

{message}

+
+ + +
+
+ ) +} diff --git a/desktop/src/components/tasks/TaskRow.tsx b/desktop/src/components/tasks/TaskRow.tsx index 8a1f538b..3d1b112d 100644 --- a/desktop/src/components/tasks/TaskRow.tsx +++ b/desktop/src/components/tasks/TaskRow.tsx @@ -5,6 +5,7 @@ import { useTranslation } from '../../i18n' import { describeCron } from '../../lib/cronDescribe' import { TaskRunsPanel } from './TaskRunsPanel' import { NewTaskModal } from './NewTaskModal' +import { ConfirmPopover } from '../shared/ConfirmPopover' type Props = { task: CronTask @@ -193,7 +194,7 @@ export function TaskRow({ task, showLogs, onToggleLogs }: Props) { onConfirm={handleDelete} onCancel={() => { setConfirmAction(null); setShowMenu(false) }} cancelLabel={t('common.cancel')} - variant="error" + confirmVariant="danger" /> )} @@ -216,38 +217,3 @@ export function TaskRow({ task, showLogs, onToggleLogs }: Props) { ) } - -// ─── Confirm Popover ───────────────────────────────────────────────────────── - -function ConfirmPopover({ message, confirmLabel, onConfirm, onCancel, cancelLabel, variant = 'brand' }: { - message: string - confirmLabel: string - onConfirm: () => void - onCancel: () => void - cancelLabel: string - variant?: 'brand' | 'error' -}) { - return ( -
-

{message}

-
- - -
-
- ) -} diff --git a/desktop/src/i18n/locales/en.ts b/desktop/src/i18n/locales/en.ts index a1c3c029..838dc840 100644 --- a/desktop/src/i18n/locales/en.ts +++ b/desktop/src/i18n/locales/en.ts @@ -472,6 +472,7 @@ export const en = { 'settings.adapters.dingtalkAuthFailed': 'DingTalk QR authorization failed.', 'settings.adapters.dingtalkAuthExpired': 'DingTalk QR authorization expired. Generate a new QR code.', 'settings.adapters.dingtalkUnbindFailed': 'Failed to unbind DingTalk.', + 'settings.adapters.dingtalkUnbindBotConfirm': 'Unbind this DingTalk bot account? You will need to scan again before DingTalk can receive or send messages.', 'settings.adapters.dingtalkClientId': 'Client ID', 'settings.adapters.dingtalkClientIdPlaceholder': 'Filled by QR auth, or enter manually', 'settings.adapters.dingtalkClientSecret': 'Client Secret', @@ -490,6 +491,7 @@ export const en = { 'settings.adapters.wechatWaiting': 'Waiting for WeChat confirmation...', 'settings.adapters.wechatBindSuccess': 'WeChat bound successfully.', 'settings.adapters.wechatUnbindAccount': 'Unbind WeChat account', + 'settings.adapters.wechatUnbindAccountConfirm': 'Unbind this WeChat account? You will need to scan again before WeChat can receive or send messages.', 'settings.adapters.wechatUnbound': 'WeChat account unbound.', 'settings.adapters.wechatUnbindFailed': 'Failed to unbind WeChat.', 'settings.adapters.wechatAllowedUsersHint': 'QR binding enables the account only. Users still need to send a pairing code, or you can add allowed WeChat user IDs here.', diff --git a/desktop/src/i18n/locales/zh.ts b/desktop/src/i18n/locales/zh.ts index ad231190..0a1b3d97 100644 --- a/desktop/src/i18n/locales/zh.ts +++ b/desktop/src/i18n/locales/zh.ts @@ -474,6 +474,7 @@ export const zh: Record = { 'settings.adapters.dingtalkAuthFailed': '钉钉扫码授权失败。', 'settings.adapters.dingtalkAuthExpired': '钉钉扫码授权已过期,请重新生成二维码。', 'settings.adapters.dingtalkUnbindFailed': '钉钉解绑失败。', + 'settings.adapters.dingtalkUnbindBotConfirm': '确定解除这个钉钉机器人绑定吗?解除后需要重新扫码,钉钉才能继续收发消息。', 'settings.adapters.dingtalkClientId': 'Client ID', 'settings.adapters.dingtalkClientIdPlaceholder': '扫码后自动填写,也可手动输入', 'settings.adapters.dingtalkClientSecret': 'Client Secret', @@ -492,6 +493,7 @@ export const zh: Record = { 'settings.adapters.wechatWaiting': '等待微信扫码确认...', 'settings.adapters.wechatBindSuccess': '微信绑定成功。', 'settings.adapters.wechatUnbindAccount': '解除微信绑定', + 'settings.adapters.wechatUnbindAccountConfirm': '确定解除这个微信账号绑定吗?解除后需要重新扫码,微信才能继续收发消息。', 'settings.adapters.wechatUnbound': '微信绑定已解除。', 'settings.adapters.wechatUnbindFailed': '微信解绑失败。', 'settings.adapters.wechatAllowedUsersHint': '微信扫码只绑定账号能力;用户仍需发送配对码,或在这里额外填写允许的微信用户 ID。', diff --git a/desktop/src/pages/AdapterSettings.test.tsx b/desktop/src/pages/AdapterSettings.test.tsx index 7185a093..b0f31028 100644 --- a/desktop/src/pages/AdapterSettings.test.tsx +++ b/desktop/src/pages/AdapterSettings.test.tsx @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from 'vitest' -import { cleanup, render, screen } from '@testing-library/react' +import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react' import '@testing-library/jest-dom' import { AdapterSettings } from './AdapterSettings' @@ -9,12 +9,27 @@ import type { AdapterFileConfig } from '../types/adapter' const FEISHU_CREATE_BOT_URL = 'https://open.feishu.cn/page/openclaw?form=multiAgent' -function renderAdapterSettings(config: AdapterFileConfig) { +function renderAdapterSettings( + config: AdapterFileConfig, + overrides: Partial> = {}, +) { useSettingsStore.setState({ locale: 'en' }) useAdapterStore.setState({ config, isLoading: false, fetchConfig: vi.fn(async () => {}), + updateConfig: vi.fn(async () => {}), + unbindWechatAccount: vi.fn(async () => {}), + unbindDingtalkBot: vi.fn(async () => {}), + removePairedUser: vi.fn(async () => {}), + beginDingtalkRegistration: vi.fn(async () => ({ + deviceCode: 'device-code', + verificationUriComplete: 'https://example.com/auth', + intervalSeconds: 1, + expiresInSeconds: 60, + })), + pollDingtalkRegistration: vi.fn(async () => ({ status: 'PENDING' })), + ...overrides, } as Partial>) render() @@ -52,3 +67,51 @@ describe('AdapterSettings Feishu onboarding', () => { expect(screen.queryByText('Need a Feishu bot?')).not.toBeInTheDocument() }) }) + +describe('AdapterSettings account unbind confirmation', () => { + it('confirms before unbinding a WeChat account', async () => { + const unbindWechatAccount = vi.fn(async () => {}) + renderAdapterSettings( + { wechat: { accountId: 'wx-account' } }, + { unbindWechatAccount }, + ) + + fireEvent.click(screen.getByRole('tab', { name: 'WeChat' })) + fireEvent.click(screen.getByRole('button', { name: 'Unbind WeChat account' })) + + expect(unbindWechatAccount).not.toHaveBeenCalled() + const dialog = screen.getByRole('dialog', { name: 'Unbind WeChat account' }) + expect(within(dialog).getByText(/You will need to scan again/)).toBeInTheDocument() + + fireEvent.click(within(dialog).getByRole('button', { name: 'Cancel' })) + expect(unbindWechatAccount).not.toHaveBeenCalled() + + fireEvent.click(screen.getByRole('button', { name: 'Unbind WeChat account' })) + fireEvent.click(within(screen.getByRole('dialog', { name: 'Unbind WeChat account' })).getByRole('button', { name: 'Unbind WeChat account' })) + + await waitFor(() => { + expect(unbindWechatAccount).toHaveBeenCalledTimes(1) + }) + }) + + it('confirms before unbinding a DingTalk bot account', async () => { + const unbindDingtalkBot = vi.fn(async () => {}) + renderAdapterSettings( + { dingtalk: { clientId: 'dt-client' } }, + { unbindDingtalkBot }, + ) + + fireEvent.click(screen.getByRole('tab', { name: 'DingTalk' })) + fireEvent.click(screen.getByRole('button', { name: 'Unbind bot account' })) + + expect(unbindDingtalkBot).not.toHaveBeenCalled() + const dialog = screen.getByRole('dialog', { name: 'Unbind bot account' }) + expect(within(dialog).getByText(/You will need to scan again/)).toBeInTheDocument() + + fireEvent.click(within(dialog).getByRole('button', { name: 'Unbind bot account' })) + + await waitFor(() => { + expect(unbindDingtalkBot).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/desktop/src/pages/AdapterSettings.tsx b/desktop/src/pages/AdapterSettings.tsx index 5578336b..6f7215aa 100644 --- a/desktop/src/pages/AdapterSettings.tsx +++ b/desktop/src/pages/AdapterSettings.tsx @@ -9,6 +9,7 @@ import QRCode from 'qrcode' type ImTab = 'feishu' | 'wechat' | 'dingtalk' | 'telegram' type ImPlatform = 'telegram' | 'feishu' | 'wechat' | 'dingtalk' +type AdapterUnbindTarget = 'wechatAccount' | 'dingtalkBot' const FEISHU_CREATE_BOT_URL = 'https://open.feishu.cn/page/openclaw?form=multiAgent' @@ -82,6 +83,7 @@ export function AdapterSettings() { const [pairingCode, setPairingCode] = useState(null) const [isGenerating, setIsGenerating] = useState(false) const [pendingUnbind, setPendingUnbind] = useState<{ platform: ImPlatform; userId: string | number } | null>(null) + const [pendingAdapterUnbind, setPendingAdapterUnbind] = useState(null) const [isUnbinding, setIsUnbinding] = useState(false) useEffect(() => { @@ -330,6 +332,7 @@ export function AdapterSettings() { } finally { setIsUnbindingWechatAccount(false) setIsWechatBinding(false) + setPendingAdapterUnbind(null) } }, [unbindWechatAccount, fetchConfig, t]) @@ -346,6 +349,7 @@ export function AdapterSettings() { setDtAuthError(err instanceof Error ? err.message : t('settings.adapters.dingtalkUnbindFailed')) } finally { setIsUnbindingDtBot(false) + setPendingAdapterUnbind(null) } }, [unbindDingtalkBot, fetchConfig, t]) @@ -606,7 +610,7 @@ export function AdapterSettings() { {config.wechat?.accountId ? t('settings.adapters.wechatRebind') : t('settings.adapters.wechatBind')} {config.wechat?.accountId && ( - )} @@ -656,7 +660,7 @@ export function AdapterSettings() { {t('settings.adapters.dingtalkStartAuth')} {(config.dingtalk?.clientId || dtClientId) && ( - )} @@ -791,6 +795,26 @@ export function AdapterSettings() { confirmVariant="danger" loading={isUnbinding} /> + { + if (isUnbindingWechatAccount || isUnbindingDtBot) return + setPendingAdapterUnbind(null) + }} + onConfirm={pendingAdapterUnbind === 'wechatAccount' ? handleUnbindWechatAccount : handleUnbindDingtalkBot} + title={pendingAdapterUnbind === 'wechatAccount' + ? t('settings.adapters.wechatUnbindAccount') + : t('settings.adapters.dingtalkUnbindBot')} + body={pendingAdapterUnbind === 'wechatAccount' + ? t('settings.adapters.wechatUnbindAccountConfirm') + : t('settings.adapters.dingtalkUnbindBotConfirm')} + confirmLabel={pendingAdapterUnbind === 'wechatAccount' + ? t('settings.adapters.wechatUnbindAccount') + : t('settings.adapters.dingtalkUnbindBot')} + cancelLabel={t('common.cancel')} + confirmVariant="danger" + loading={isUnbindingWechatAccount || isUnbindingDtBot} + /> ) } diff --git a/desktop/src/pages/DiagnosticsSettings.tsx b/desktop/src/pages/DiagnosticsSettings.tsx index ae52b09e..f825ef5a 100644 --- a/desktop/src/pages/DiagnosticsSettings.tsx +++ b/desktop/src/pages/DiagnosticsSettings.tsx @@ -6,6 +6,7 @@ import { useTranslation } from '../i18n' import { formatBytes } from '../lib/formatBytes' import { useUIStore } from '../stores/uiStore' import { DoctorPanel } from '../components/doctor/DoctorPanel' +import { ConfirmDialog } from '../components/shared/ConfirmDialog' export function DiagnosticsSettings() { const t = useTranslation() @@ -15,6 +16,7 @@ export function DiagnosticsSettings() { const [isLoading, setIsLoading] = useState(true) const [isExporting, setIsExporting] = useState(false) const [isClearing, setIsClearing] = useState(false) + const [clearConfirmOpen, setClearConfirmOpen] = useState(false) const [lastExportPath, setLastExportPath] = useState(null) const load = useCallback(async () => { @@ -90,13 +92,13 @@ export function DiagnosticsSettings() { } const handleClear = async () => { - if (!window.confirm(t('settings.diagnostics.confirmClear'))) return setIsClearing(true) try { await diagnosticsApi.clear() setEvents([]) setStatus(await diagnosticsApi.getStatus()) setLastExportPath(null) + setClearConfirmOpen(false) addToast({ type: 'success', message: t('settings.diagnostics.cleared') }) } catch (error) { addToast({ @@ -152,7 +154,7 @@ export function DiagnosticsSettings() { content_copy {t('settings.diagnostics.copySummary')} - @@ -186,6 +188,20 @@ export function DiagnosticsSettings() { )} + + { + if (!isClearing) setClearConfirmOpen(false) + }} + onConfirm={handleClear} + title={t('settings.diagnostics.clearLogs')} + body={t('settings.diagnostics.confirmClear')} + confirmLabel={t('settings.diagnostics.clearLogs')} + cancelLabel={t('common.cancel')} + confirmVariant="danger" + loading={isClearing} + /> ) } diff --git a/desktop/src/pages/McpSettings.tsx b/desktop/src/pages/McpSettings.tsx index 4a29cb0a..ed87c98c 100644 --- a/desktop/src/pages/McpSettings.tsx +++ b/desktop/src/pages/McpSettings.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { Button } from '../components/shared/Button' import { DirectoryPicker } from '../components/shared/DirectoryPicker' import { Input } from '../components/shared/Input' -import { Modal } from '../components/shared/Modal' +import { ConfirmDialog } from '../components/shared/ConfirmDialog' import { useTranslation } from '../i18n' import { useUIStore } from '../stores/uiStore' import { useMcpStore } from '../stores/mcpStore' @@ -667,28 +667,20 @@ export function McpSettings() { } const deleteModal = ( - { if (isDeleting) return setPendingDeleteServer(null) }} title={t('settings.mcp.form.deleteTitle')} - footer={( - <> - - - - )} - > -

- {pendingDeleteServer ? t('settings.mcp.form.deleteConfirmBody', { name: pendingDeleteServer.name }) : ''} -

-
+ body={pendingDeleteServer ? t('settings.mcp.form.deleteConfirmBody', { name: pendingDeleteServer.name }) : ''} + confirmLabel={t('settings.mcp.form.confirmDelete')} + cancelLabel={t('settings.mcp.form.cancel')} + confirmVariant="danger" + loading={isDeleting} + onConfirm={confirmDelete} + /> ) const handleSave = async () => {