fix: plugins related wip

This commit is contained in:
dev 2025-11-05 10:56:40 +08:00
parent bd4a979f62
commit 468aebd632
2 changed files with 249 additions and 246 deletions

View File

@ -1,6 +1,7 @@
import { Button, Chip, Skeleton, Table, TableBody, TableCell, TableColumn, TableHeader, TableRow } from '@heroui/react'
import type { InstalledPlugin } from '@renderer/types/plugin' import type { InstalledPlugin } from '@renderer/types/plugin'
import { Trash2 } from 'lucide-react' import type { TableProps } from 'antd'
import { Button, Skeleton, Table as AntTable, Tag } from 'antd'
import { Dot, Trash2 } from 'lucide-react'
import type { FC } from 'react' import type { FC } from 'react'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -33,10 +34,10 @@ export const InstalledPluginsList: FC<InstalledPluginsListProps> = ({ plugins, o
if (loading) { if (loading) {
return ( return (
<div className="space-y-2"> <div className="flex flex-col space-y-2">
<Skeleton className="h-12 w-full rounded-lg" /> <Skeleton.Input active className="w-full" size={'large'} style={{ width: '100%' }} />
<Skeleton className="h-12 w-full rounded-lg" /> <Skeleton.Input active className="w-full" size={'large'} style={{ width: '100%' }} />
<Skeleton className="h-12 w-full rounded-lg" /> <Skeleton.Input active className="w-full" size={'large'} style={{ width: '100%' }} />
</div> </div>
) )
} }
@ -50,50 +51,59 @@ export const InstalledPluginsList: FC<InstalledPluginsListProps> = ({ plugins, o
) )
} }
return ( const columns: TableProps<InstalledPlugin>['columns'] = [
<Table aria-label="Installed plugins table" removeWrapper> {
<TableHeader> title: t('plugins.name'),
<TableColumn>{t('plugins.name')}</TableColumn> dataIndex: 'name',
<TableColumn>{t('plugins.type')}</TableColumn> key: 'name',
<TableColumn>{t('plugins.category')}</TableColumn> render: (_: any, plugin: InstalledPlugin) => (
<TableColumn align="end">{t('plugins.actions')}</TableColumn> <div className="flex flex-col">
</TableHeader> <span className="font-semibold text-small">{plugin.metadata.name}</span>
<TableBody> {plugin.metadata.description && (
{plugins.map((plugin) => ( <span className="line-clamp-1 text-default-400 text-tiny">{plugin.metadata.description}</span>
<TableRow key={plugin.filename}> )}
<TableCell> </div>
<div className="flex flex-col"> )
<span className="font-semibold text-small">{plugin.metadata.name}</span> },
{plugin.metadata.description && ( {
<span className="line-clamp-1 text-default-400 text-tiny">{plugin.metadata.description}</span> title: t('plugins.type'),
)} dataIndex: 'type',
</div> key: 'type',
</TableCell> render: (type: string) => <Tag color={type === 'agent' ? 'magenta' : 'purple'}>{type}</Tag>
<TableCell> },
<Chip size="sm" variant="flat" color={plugin.type === 'agent' ? 'primary' : 'secondary'}> {
{plugin.type} title: t('plugins.category'),
</Chip> dataIndex: 'category',
</TableCell> key: 'category',
<TableCell> render: (_: any, plugin: InstalledPlugin) => (
<Chip size="sm" variant="dot"> <Tag
{plugin.metadata.category} icon={<Dot size={14} strokeWidth={8} />}
</Chip> style={{
</TableCell> display: 'flex',
<TableCell> flexDirection: 'row',
<Button alignItems: 'center',
size="sm" gap: '2px'
color="danger" }}>
variant="light" {plugin.metadata.category}
isIconOnly </Tag>
onPress={() => handleUninstall(plugin)} )
isLoading={uninstallingPlugin === plugin.filename} },
isDisabled={loading}> {
<Trash2 className="h-4 w-4" /> title: t('plugins.actions'),
</Button> key: 'actions',
</TableCell> align: 'right' as const,
</TableRow> render: (_: any, plugin: InstalledPlugin) => (
))} <Button
</TableBody> danger
</Table> type="text"
) onClick={() => handleUninstall(plugin)}
loading={uninstallingPlugin === plugin.filename}
disabled={loading}
icon={<Trash2 className="h-4 w-4" />}
/>
)
}
]
return <AntTable columns={columns} dataSource={plugins} />
} }

View File

@ -1,16 +1,6 @@
import {
Button,
Chip,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
Spinner,
Textarea
} from '@heroui/react'
import type { PluginMetadata } from '@renderer/types/plugin' import type { PluginMetadata } from '@renderer/types/plugin'
import { Download, Edit, Save, Trash2, X } from 'lucide-react' import { Button as AntButton, Input, Modal as AntdModal, Spin, Tag } from 'antd'
import { Dot, Download, Edit, Save, Trash2, X } from 'lucide-react'
import type { FC } from 'react' import type { FC } from 'react'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { createPortal } from 'react-dom' import { createPortal } from 'react-dom'
@ -121,200 +111,203 @@ export const PluginDetailModal: FC<PluginDetailModalProps> = ({
if (!plugin) return null if (!plugin) return null
const modalContent = ( const modalContent = (
<Modal <AntdModal
isOpen={isOpen} centered
onClose={onClose} open={isOpen}
size="2xl" onCancel={onClose}
scrollBehavior="inside" styles={{
classNames={{ body: {
wrapper: 'z-[9999]' maxHeight: '60vh',
}}> overflowY: 'auto'
<ModalContent> }
<ModalHeader className="flex flex-col gap-1"> }}
style={{
width: '70%'
}}
title={
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h2 className="font-bold text-xl">{plugin.name}</h2> <h2 className="font-bold text-xl">{plugin.name}</h2>
<Chip size="sm" variant="solid" color={plugin.type === 'agent' ? 'primary' : 'secondary'}> <Tag color={plugin.type === 'agent' ? 'magenta' : 'purple'}>{plugin.type}</Tag>
{plugin.type}
</Chip>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Chip size="sm" variant="dot" color="default"> <Tag
icon={<Dot size={14} strokeWidth={8} />}
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
gap: '2px'
}}>
{plugin.category} {plugin.category}
</Chip> </Tag>
{plugin.version && ( {plugin.version && <Tag>v{plugin.version}</Tag>}
<Chip size="sm" variant="bordered">
v{plugin.version}
</Chip>
)}
</div> </div>
</ModalHeader> </div>
}
<ModalBody> footer={
{/* Description */} <div className="flex flex-row justify-end gap-4">
{plugin.description && ( <AntButton type="text" onClick={onClose}>
<div className="mb-4"> {t('common.close')}
<h3 className="mb-2 font-semibold text-small">Description</h3> </AntButton>
<p className="text-default-600 text-small">{plugin.description}</p>
</div>
)}
{/* Author */}
{plugin.author && (
<div className="mb-4">
<h3 className="mb-2 font-semibold text-small">Author</h3>
<p className="text-default-600 text-small">{plugin.author}</p>
</div>
)}
{/* Tools (for agents) */}
{plugin.tools && plugin.tools.length > 0 && (
<div className="mb-4">
<h3 className="mb-2 font-semibold text-small">Tools</h3>
<div className="flex flex-wrap gap-1">
{plugin.tools.map((tool) => (
<Chip key={tool} size="sm" variant="flat">
{tool}
</Chip>
))}
</div>
</div>
)}
{/* Allowed Tools (for commands) */}
{plugin.allowed_tools && plugin.allowed_tools.length > 0 && (
<div className="mb-4">
<h3 className="mb-2 font-semibold text-small">Allowed Tools</h3>
<div className="flex flex-wrap gap-1">
{plugin.allowed_tools.map((tool) => (
<Chip key={tool} size="sm" variant="flat">
{tool}
</Chip>
))}
</div>
</div>
)}
{/* Tags */}
{plugin.tags && plugin.tags.length > 0 && (
<div className="mb-4">
<h3 className="mb-2 font-semibold text-small">Tags</h3>
<div className="flex flex-wrap gap-1">
{plugin.tags.map((tag) => (
<Chip key={tag} size="sm" variant="bordered">
{tag}
</Chip>
))}
</div>
</div>
)}
{/* Metadata */}
<div className="mb-4">
<h3 className="mb-2 font-semibold text-small">Metadata</h3>
<div className="space-y-1 text-small">
<div className="flex justify-between">
<span className="text-default-500">File:</span>
<span className="font-mono text-default-600 text-tiny">{plugin.filename}</span>
</div>
<div className="flex justify-between">
<span className="text-default-500">Size:</span>
<span className="text-default-600">{(plugin.size / 1024).toFixed(2)} KB</span>
</div>
<div className="flex justify-between">
<span className="text-default-500">Source:</span>
<span className="font-mono text-default-600 text-tiny">{plugin.sourcePath}</span>
</div>
{plugin.installedAt && (
<div className="flex justify-between">
<span className="text-default-500">Installed:</span>
<span className="text-default-600">{new Date(plugin.installedAt).toLocaleString()}</span>
</div>
)}
</div>
</div>
{/* Content */}
<div className="mb-4">
<div className="mb-2 flex items-center justify-between">
<h3 className="font-semibold text-small">Content</h3>
{installed && !contentLoading && !contentError && (
<div className="flex gap-2">
{isEditing ? (
<>
<Button
size="sm"
variant="flat"
color="danger"
startContent={<X className="h-3 w-3" />}
onPress={handleCancelEdit}
isDisabled={saving}>
Cancel
</Button>
<Button
size="sm"
color="primary"
startContent={saving ? <Spinner size="sm" color="current" /> : <Save className="h-3 w-3" />}
onPress={handleSave}
isDisabled={saving}>
{saving ? 'Saving...' : 'Save'}
</Button>
</>
) : (
<Button size="sm" variant="flat" startContent={<Edit className="h-3 w-3" />} onPress={handleEdit}>
Edit
</Button>
)}
</div>
)}
</div>
{contentLoading ? (
<div className="flex items-center justify-center py-4">
<Spinner size="sm" />
</div>
) : contentError ? (
<div className="rounded-md bg-danger-50 p-3 text-danger text-small">{contentError}</div>
) : isEditing ? (
<Textarea
value={editedContent}
onValueChange={setEditedContent}
minRows={20}
classNames={{
input: 'font-mono text-tiny'
}}
/>
) : (
<pre className="max-h-96 overflow-auto whitespace-pre-wrap rounded-md bg-default-100 p-3 font-mono text-tiny">
{content}
</pre>
)}
</div>
</ModalBody>
<ModalFooter>
<Button variant="light" onPress={onClose}>
Close
</Button>
{installed ? ( {installed ? (
<Button <AntButton
color="danger" danger
variant="flat" variant="filled"
startContent={loading ? <Spinner size="sm" color="current" /> : <Trash2 className="h-4 w-4" />} icon={loading ? <Spin size="small" /> : <Trash2 className="h-4 w-4" />}
onPress={onUninstall} iconPosition={'start'}
isDisabled={loading}> onClick={onUninstall}
disabled={loading}>
{loading ? t('plugins.uninstalling') : t('plugins.uninstall')} {loading ? t('plugins.uninstalling') : t('plugins.uninstall')}
</Button> </AntButton>
) : ( ) : (
<Button <AntButton
color="primary" color="primary"
startContent={loading ? <Spinner size="sm" color="current" /> : <Download className="h-4 w-4" />} variant="solid"
onPress={onInstall} icon={loading ? <Spin size="small" /> : <Download className="h-4 w-4" />}
isDisabled={loading}> iconPosition={'start'}
onClick={onInstall}
disabled={loading}>
{loading ? t('plugins.installing') : t('plugins.install')} {loading ? t('plugins.installing') : t('plugins.install')}
</Button> </AntButton>
)} )}
</ModalFooter> </div>
</ModalContent> }>
</Modal> <div>
{/* Description */}
{plugin.description && (
<div className="mb-4">
<h3 className="mb-2 font-semibold text-small">Description</h3>
<p className="text-default-600 text-small">{plugin.description}</p>
</div>
)}
{/* Author */}
{plugin.author && (
<div className="mb-4">
<h3 className="mb-2 font-semibold text-small">Author</h3>
<p className="text-default-600 text-small">{plugin.author}</p>
</div>
)}
{/* Tools (for agents) */}
{plugin.tools && plugin.tools.length > 0 && (
<div className="mb-4">
<h3 className="mb-2 font-semibold text-small">Tools</h3>
<div className="flex flex-wrap gap-1">
{plugin.tools.map((tool) => (
<Tag key={tool}>{tool}</Tag>
))}
</div>
</div>
)}
{/* Allowed Tools (for commands) */}
{plugin.allowed_tools && plugin.allowed_tools.length > 0 && (
<div className="mb-4">
<h3 className="mb-2 font-semibold text-small">Allowed Tools</h3>
<div className="flex flex-wrap gap-1">
{plugin.allowed_tools.map((tool) => (
<Tag key={tool}>{tool}</Tag>
))}
</div>
</div>
)}
{/* Tags */}
{plugin.tags && plugin.tags.length > 0 && (
<div className="mb-4">
<h3 className="mb-2 font-semibold text-small">Tags</h3>
<div className="flex flex-wrap gap-1">
{plugin.tags.map((tag) => (
<Tag key={tag}>{tag}</Tag>
))}
</div>
</div>
)}
{/* Metadata */}
<div className="mb-4">
<h3 className="mb-2 font-semibold text-small">Metadata</h3>
<div className="space-y-1 text-small">
<div className="flex justify-between">
<span className="text-default-500">File:</span>
<span className="font-mono text-default-600 text-tiny">{plugin.filename}</span>
</div>
<div className="flex justify-between">
<span className="text-default-500">Size:</span>
<span className="text-default-600">{(plugin.size / 1024).toFixed(2)} KB</span>
</div>
<div className="flex justify-between">
<span className="text-default-500">Source:</span>
<span className="font-mono text-default-600 text-tiny">{plugin.sourcePath}</span>
</div>
{plugin.installedAt && (
<div className="flex justify-between">
<span className="text-default-500">Installed:</span>
<span className="text-default-600">{new Date(plugin.installedAt).toLocaleString()}</span>
</div>
)}
</div>
</div>
{/* Content */}
<div className="mb-4">
<div className="mb-2 flex items-center justify-between">
<h3 className="font-semibold text-small">Content</h3>
{installed && !contentLoading && !contentError && (
<div className="flex gap-2">
{isEditing ? (
<>
<AntButton
danger
variant="filled"
icon={<X className="h-3 w-3" />}
iconPosition="start"
onClick={handleCancelEdit}
disabled={saving}>
{t('common.cancel')}
</AntButton>
<AntButton
color="primary"
variant="filled"
icon={saving ? <Spin size="small" /> : <Save className="h-3 w-3" />}
onClick={handleSave}
disabled={saving}>
{t('common.save')}
</AntButton>
</>
) : (
<AntButton variant="filled" icon={<Edit className="h-3 w-3" />} onClick={handleEdit}>
{t('common.edit')}
</AntButton>
)}
</div>
)}
</div>
{contentLoading ? (
<div className="flex items-center justify-center py-4">
<Spin size="small" />
</div>
) : contentError ? (
<div className="rounded-md bg-danger-50 p-3 text-danger text-small">{contentError}</div>
) : isEditing ? (
<Input.TextArea
value={editedContent}
onChange={(e) => setEditedContent(e.target.value)}
autoSize={{ minRows: 20 }}
classNames={{
textarea: 'font-mono text-tiny'
}}
/>
) : (
<pre className="max-h-96 overflow-auto whitespace-pre-wrap rounded-md bg-default-100 p-3 font-mono text-tiny">
{content}
</pre>
)}
</div>
</div>
</AntdModal>
) )
return createPortal(modalContent, document.body) return createPortal(modalContent, document.body)