Files
EasyCLIProxyAPI/src/pages/PluginInstallDialog.tsx
T
lzt404 2649df000c feat: support DeepSeek Harness Desktop and refine management UI
- Detect and launch DeepSeek Harness Desktop on Windows
- Improve agent client selection and responsive layouts
- Unify dashboard runtime and connection card styling
- Streamline plugin navigation, cards, and installation dialogs
- Update translations and regression tests
2026-10-04 17:31:52 +08:00

53 lines
3.9 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { invoke } from '@tauri-apps/api/core';
import { ExternalLink, X } from 'lucide-react';
import { useDialogFocusTrap } from '../components/useDialogFocusTrap';
import { useI18n } from '../i18n';
import { pluginText } from '../i18n/plugins';
import { pluginStoreApi, type PluginStoreEntry, type PluginStoreInstallResult } from '../services/plugins';
import { buildRepositoryURL } from '../services/pluginResources';
import { isValidManualReleaseTag, supportsPluginVersionSelection } from '../services/pluginReleaseVersions';
export function PluginInstallDialog({ entry, onClose, onInstalled }: {
entry: PluginStoreEntry;
onClose: () => void;
onInstalled: (result: PluginStoreInstallResult) => void;
}) {
const { t, locale } = useI18n();
const pt = (key: Parameters<typeof pluginText>[0]) => pluginText(key, locale);
const [version, setVersion] = useState('');
const [busy, setBusy] = useState(false);
const [error, setError] = useState('');
const pending = useRef(false);
const mounted = useRef(true);
useEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []);
// The dialog remains open throughout installation, including on errors.
const ref = useDialogFocusTrap<HTMLElement>({ onEscape: busy ? undefined : onClose, preventEscape: busy });
const canSelectVersion = supportsPluginVersionSelection(entry.installType);
const validVersion = !version.trim() || isValidManualReleaseTag(version);
const repository = buildRepositoryURL(entry.repository);
const action = entry.installed ? pt('update') : pt('install');
const install = async () => {
if (pending.current || !validVersion) return;
pending.current = true; setBusy(true); setError('');
try {
const result = await pluginStoreApi.install(entry.id, { sourceId: entry.sourceId, version: canSelectVersion ? version.trim() : undefined });
if (mounted.current) onInstalled(result);
} catch (reason) {
if (mounted.current) setError(reason instanceof Error ? reason.message : String(reason));
} finally { pending.current = false; if (mounted.current) setBusy(false); }
};
return createPortal(<div className="config-dialog-backdrop">
<section className="config-dialog plugin-install-dialog" role="dialog" aria-modal="true" aria-labelledby="plugin-install-title" ref={ref} tabIndex={-1}>
<div className="config-dialog-heading"><h2 id="plugin-install-title">{action} · {entry.name || entry.id}</h2><button className="icon-button quiet" aria-label={t('common.close')} disabled={busy} onClick={onClose}><X size={18} /></button></div>
<div className="plugin-install-body">
<dl className="plugin-install-origin"><dt>{pt('source')}</dt><dd>{entry.sourceName || entry.sourceId}<small>{entry.sourceUrl}</small></dd><dt>{pt('repository')}</dt><dd className="plugin-install-repository"><span>{entry.repository || '—'}</span>{repository && <button className="icon-button quiet" aria-label={pt('repository')} onClick={() => void invoke('open_external_url', { url: repository }).catch(e => setError(String(e)))}><ExternalLink size={15} /></button>}</dd></dl>
{canSelectVersion && <label className="plugin-field">{pt('version')}<input value={version} onChange={e => setVersion(e.target.value)} disabled={busy} placeholder={entry.version || 'v1.0.0'} aria-invalid={!validVersion} />{!validVersion && <small role="alert">{pt('invalidVersion')}</small>}</label>}
{error && <p className="plugin-error" role="alert">{error}</p>}
</div>
<div className="plugin-dialog-actions"><button className="secondary-button" disabled={busy} onClick={onClose}>{t('common.cancel')}</button><button className="primary-button" disabled={busy || !validVersion} onClick={() => void install()}>{busy ? t('common.loading') : action}</button></div>
</section>
</div>, document.body);
}