refactor: 核心插件与插件页合并——插件页按 内置/全局/项目 三作用域统一管理
This commit is contained in:
@@ -30,7 +30,7 @@ import type {
|
|||||||
import { THEME_MODES, THEME_PRESETS, applyThemeColor } from '../lib/theme'
|
import { THEME_MODES, THEME_PRESETS, applyThemeColor } from '../lib/theme'
|
||||||
import { usePluginList } from '../plugins/store'
|
import { usePluginList } from '../plugins/store'
|
||||||
|
|
||||||
type Tab = 'general' | 'corePlugins' | 'model' | 'prompt' | 'plugins' | 'skills' | 'commands'
|
type Tab = 'general' | 'model' | 'prompt' | 'plugins' | 'skills' | 'commands'
|
||||||
|
|
||||||
/** 左侧分组导航(参考 ZCode 设置页:基础设置 / Agent 能力) */
|
/** 左侧分组导航(参考 ZCode 设置页:基础设置 / Agent 能力) */
|
||||||
const NAV: Array<{ group: string; items: Array<{ id: Tab; label: string; icon: IconType }> }> = [
|
const NAV: Array<{ group: string; items: Array<{ id: Tab; label: string; icon: IconType }> }> = [
|
||||||
@@ -39,8 +39,7 @@ const NAV: Array<{ group: string; items: Array<{ id: Tab; label: string; icon: I
|
|||||||
items: [
|
items: [
|
||||||
{ id: 'general', label: '常规', icon: LuSettings2 },
|
{ id: 'general', label: '常规', icon: LuSettings2 },
|
||||||
{ id: 'model', label: '模型设置', icon: LuCpu },
|
{ id: 'model', label: '模型设置', icon: LuCpu },
|
||||||
{ id: 'prompt', label: '系统提示词', icon: LuTextQuote },
|
{ id: 'prompt', label: '系统提示词', icon: LuTextQuote }
|
||||||
{ id: 'corePlugins', label: '核心插件', icon: LuPuzzle }
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -55,10 +54,9 @@ const NAV: Array<{ group: string; items: Array<{ id: Tab; label: string; icon: I
|
|||||||
|
|
||||||
const TAB_META: Record<Tab, { title: string; desc: string }> = {
|
const TAB_META: Record<Tab, { title: string; desc: string }> = {
|
||||||
general: { title: '常规', desc: '主题色等外观偏好。' },
|
general: { title: '常规', desc: '主题色等外观偏好。' },
|
||||||
corePlugins: { title: '核心插件', desc: '内置的第一方功能插件,官方维护' },
|
|
||||||
model: { title: '模型设置', desc: '' },
|
model: { title: '模型设置', desc: '' },
|
||||||
prompt: { title: '系统提示词', desc: '' },
|
prompt: { title: '系统提示词', desc: '' },
|
||||||
plugins: { title: '插件', desc: '第三方扩展插件(全局 / 项目目录),按需启停。' },
|
plugins: { title: '插件', desc: '扩展插件(内置 / 全局 / 项目),按需启停。' },
|
||||||
skills: { title: '技能', desc: 'SKILL.md 技能包,Agent 自动按需加载。' },
|
skills: { title: '技能', desc: 'SKILL.md 技能包,Agent 自动按需加载。' },
|
||||||
commands: { title: '命令', desc: '聊天中 / 触发的自定义命令模板。' }
|
commands: { title: '命令', desc: '聊天中 / 触发的自定义命令模板。' }
|
||||||
}
|
}
|
||||||
@@ -628,74 +626,22 @@ function ModelSettings({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 核心插件页:内置插件列表 + 启停开关(单一事实源在主进程,listChanged 事件驱动刷新) */
|
/** 插件页:内置 / 全局 / 项目三个作用域统一管理(交互对齐技能页),行内启停开关;单一事实源在主进程,listChanged 驱动刷新 */
|
||||||
function CorePluginPanel({
|
function PluginsPanel({
|
||||||
togglingId,
|
|
||||||
onToggle
|
|
||||||
}: {
|
|
||||||
/** 正在切换中的插件 id(切换期间全部开关禁用,避免并发重建运行时) */
|
|
||||||
togglingId: string | null
|
|
||||||
onToggle: (plugin: PluginInfo, next: boolean) => void
|
|
||||||
}): ReactNode {
|
|
||||||
// 本页只管核心(内置)插件;外置插件留给「Agent 能力 → 插件」
|
|
||||||
const core = usePluginList().filter((p) => p.source === 'builtin')
|
|
||||||
if (core.length === 0) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center justify-center gap-1 rounded-card border border-dashed border-line py-12 text-center">
|
|
||||||
<div className="text-[13px] font-medium text-ink-2">暂无核心插件</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
{core
|
|
||||||
.slice()
|
|
||||||
.sort((a, b) => (a.manifest.order ?? 100) - (b.manifest.order ?? 100) || a.manifest.id.localeCompare(b.manifest.id))
|
|
||||||
.map((p) => (
|
|
||||||
<div key={p.manifest.id} className="rounded-card border border-line px-3 py-2.5">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-[12.5px] font-medium text-ink">{p.manifest.name}</span>
|
|
||||||
<span className="rounded-control bg-hover px-1.5 py-0.5 text-[10px] text-ink-3">内置 v{p.manifest.version}</span>
|
|
||||||
<span className="ml-auto text-[11px] text-ink-3">{p.enabled ? '已启用' : '已停用'}</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
role="switch"
|
|
||||||
aria-checked={p.enabled}
|
|
||||||
aria-label={`启用${p.manifest.name}`}
|
|
||||||
disabled={togglingId !== null}
|
|
||||||
onClick={() => onToggle(p, !p.enabled)}
|
|
||||||
className={`relative h-6 w-10 shrink-0 rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-50 ${
|
|
||||||
p.enabled ? 'bg-accent' : 'bg-line-strong'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`absolute top-0.5 size-5 rounded-full bg-white shadow transition-[left] ${p.enabled ? 'left-[18px]' : 'left-0.5'}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{p.manifest.description && <div className="mt-1 text-[12px] leading-5 text-ink-3">{p.manifest.description}</div>}
|
|
||||||
{p.error && <div className="mt-1 text-[12px] text-red">装载失败:{p.error}</div>}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 第三方插件页:全局/项目两个作用域(交互对齐技能页),行内启停开关与核心插件相同 */
|
|
||||||
function ExternalPluginPanel({
|
|
||||||
togglingId,
|
togglingId,
|
||||||
onToggle,
|
onToggle,
|
||||||
devBuild,
|
devBuild,
|
||||||
onOpenDir
|
onOpenDir
|
||||||
}: {
|
}: {
|
||||||
|
/** 正在切换中的插件 id(切换期间全部开关禁用,避免并发重建运行时) */
|
||||||
togglingId: string | null
|
togglingId: string | null
|
||||||
onToggle: (plugin: PluginInfo, next: boolean) => void
|
onToggle: (plugin: PluginInfo, next: boolean) => void
|
||||||
/** 开发版构建(第三方装载开关开启);用户版展示关闭提示 */
|
/** 开发版构建(第三方装载开关开启);用户版展示关闭提示 */
|
||||||
devBuild: boolean
|
devBuild: boolean
|
||||||
onOpenDir: (path: string) => void
|
onOpenDir: (path: string) => void
|
||||||
}): ReactNode {
|
}): ReactNode {
|
||||||
const external = usePluginList().filter((p) => p.source === 'external')
|
const plugins = usePluginList()
|
||||||
const [scope, setScope] = useState<'user' | 'project'>('user')
|
const [scope, setScope] = useState<'user' | 'project' | 'builtin'>('user')
|
||||||
const [dirs, setDirs] = useState<{ user: string[]; project: string[]; global: string[] }>({
|
const [dirs, setDirs] = useState<{ user: string[]; project: string[]; global: string[] }>({
|
||||||
user: [],
|
user: [],
|
||||||
project: [],
|
project: [],
|
||||||
@@ -713,16 +659,19 @@ function ExternalPluginPanel({
|
|||||||
alive = false
|
alive = false
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
// 全局作用域 = 全局扩展目录(含应用外置目录,统一按全局展示);项目作用域 = 当前工作区 .pi/extensions
|
|
||||||
const scoped = external.filter((p) => (scope === 'project' ? p.origin === 'project' : p.origin !== 'project'))
|
const scoped =
|
||||||
const scopedDirs = (scope === 'user' ? dirs.global : dirs.project).filter(Boolean)
|
scope === 'builtin'
|
||||||
|
? plugins.filter((p) => p.source === 'builtin')
|
||||||
|
: plugins.filter((p) => p.source === 'external' && (scope === 'project' ? p.origin === 'project' : p.origin !== 'project'))
|
||||||
|
const scopedDirs = scope === 'user' ? dirs.global : scope === 'project' ? dirs.project : []
|
||||||
const scopedHints = scope === 'user' ? ['全局'] : ['项目']
|
const scopedHints = scope === 'user' ? ['全局'] : ['项目']
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-0 flex-col">
|
<div className="flex min-h-0 flex-col">
|
||||||
<div className="flex items-center gap-2 pb-3">
|
<div className="flex items-center gap-2 pb-3">
|
||||||
<div className="flex items-center rounded-control bg-zinc-100 p-0.5">
|
<div className="flex items-center rounded-control bg-zinc-100 p-0.5">
|
||||||
{(['user', 'project'] as const).map((s) => (
|
{(['user', 'project', 'builtin'] as const).map((s) => (
|
||||||
<button
|
<button
|
||||||
key={s}
|
key={s}
|
||||||
type="button"
|
type="button"
|
||||||
@@ -731,7 +680,7 @@ function ExternalPluginPanel({
|
|||||||
scope === s ? 'bg-white font-medium text-zinc-900 shadow-sm' : 'text-zinc-500 hover:text-zinc-700'
|
scope === s ? 'bg-white font-medium text-zinc-900 shadow-sm' : 'text-zinc-500 hover:text-zinc-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{s === 'user' ? '全局' : '项目'}
|
{s === 'user' ? '全局' : s === 'project' ? '项目' : '内置'}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -753,11 +702,12 @@ function ExternalPluginPanel({
|
|||||||
{scoped.length === 0 ? (
|
{scoped.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center gap-1 rounded-card border border-dashed border-line py-12 text-center">
|
<div className="flex flex-col items-center justify-center gap-1 rounded-card border border-dashed border-line py-12 text-center">
|
||||||
<div className="text-[13px] font-medium text-ink-2">
|
<div className="text-[13px] font-medium text-ink-2">
|
||||||
{devBuild ? '暂无第三方插件' : '第三方插件装载已关闭(用户版构建)'}
|
{scope === 'builtin' ? '暂无内置插件' : devBuild ? '暂无第三方插件' : '第三方插件装载已关闭(用户版构建)'}
|
||||||
</div>
|
</div>
|
||||||
{devBuild && (
|
{scope !== 'builtin' && devBuild && (
|
||||||
<div className="text-[11.5px] text-ink-3">
|
<div className="text-[11.5px] text-ink-3">
|
||||||
把含 manifest.json 的插件目录放入全局(~/.pi/agent/extensions)即可自动识别
|
把含 manifest.json 的插件目录放入
|
||||||
|
{scope === 'user' ? '全局(~/.pi/agent/extensions)' : '项目 .pi/extensions/'}即可自动识别
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -765,13 +715,16 @@ function ExternalPluginPanel({
|
|||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
{scoped
|
{scoped
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => a.manifest.id.localeCompare(b.manifest.id))
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
(a.manifest.order ?? 100) - (b.manifest.order ?? 100) || a.manifest.id.localeCompare(b.manifest.id)
|
||||||
|
)
|
||||||
.map((p) => (
|
.map((p) => (
|
||||||
<div key={p.manifest.id} className="rounded-card border border-line px-3 py-2.5">
|
<div key={p.manifest.id} className="rounded-card border border-line px-3 py-2.5">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-[12.5px] font-medium text-ink">{p.manifest.name}</span>
|
<span className="text-[12.5px] font-medium text-ink">{p.manifest.name}</span>
|
||||||
<span className="rounded-control bg-hover px-1.5 py-0.5 text-[10px] text-ink-3">
|
<span className="rounded-control bg-hover px-1.5 py-0.5 text-[10px] text-ink-3">
|
||||||
{scope === 'user' ? '全局' : '项目'} v{p.manifest.version}
|
{scope === 'builtin' ? '内置' : scope === 'user' ? '全局' : '项目'} v{p.manifest.version}
|
||||||
</span>
|
</span>
|
||||||
<span className="ml-auto text-[11px] text-ink-3">{p.enabled ? '已启用' : '已停用'}</span>
|
<span className="ml-auto text-[11px] text-ink-3">{p.enabled ? '已启用' : '已停用'}</span>
|
||||||
<button
|
<button
|
||||||
@@ -799,6 +752,7 @@ function ExternalPluginPanel({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 系统提示词预览:整段 pi 提示词是大文本,memo 化避免弹窗内任意状态变化触发重渲染;挂载耗时靠过渡提交兜住 */
|
/** 系统提示词预览:整段 pi 提示词是大文本,memo 化避免弹窗内任意状态变化触发重渲染;挂载耗时靠过渡提交兜住 */
|
||||||
const PromptPreview = memo(function PromptPreview({ loading, text }: { loading: boolean; text: string }): ReactNode {
|
const PromptPreview = memo(function PromptPreview({ loading, text }: { loading: boolean; text: string }): ReactNode {
|
||||||
return (
|
return (
|
||||||
@@ -1098,10 +1052,8 @@ export function SettingsModal({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tab === 'corePlugins' && <CorePluginPanel togglingId={togglingId} onToggle={togglePlugin} />}
|
|
||||||
|
|
||||||
{tab === 'plugins' && (
|
{tab === 'plugins' && (
|
||||||
<ExternalPluginPanel
|
<PluginsPanel
|
||||||
togglingId={togglingId}
|
togglingId={togglingId}
|
||||||
onToggle={togglePlugin}
|
onToggle={togglePlugin}
|
||||||
devBuild={import.meta.env.DEV}
|
devBuild={import.meta.env.DEV}
|
||||||
@@ -1216,7 +1168,7 @@ export function SettingsModal({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tab !== 'general' && tab !== 'corePlugins' && tab !== 'plugins' && tab !== 'model' && tab !== 'prompt' && tab !== 'skills' && tab !== 'commands' && (
|
{tab !== 'general' && tab !== 'plugins' && tab !== 'model' && tab !== 'prompt' && tab !== 'skills' && tab !== 'commands' && (
|
||||||
<ComingSoon icon={NAV[1].items.find((i) => i.id === tab)?.icon ?? LuPuzzle} label={meta.title} />
|
<ComingSoon icon={NAV[1].items.find((i) => i.id === tab)?.icon ?? LuPuzzle} label={meta.title} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user