feat: remove legacy OpenCode runtime

Cut product flows over to Coding/Pi and retain only the migration-owned v1 boundary. Promote supported native optional packages because electron-builder omitted pnpm transitive optional closure from the packaged ASAR.
This commit is contained in:
2026-08-24 12:17:43 +08:00
parent 61fede2b4d
commit 5a275b93a7
199 changed files with 1330 additions and 64725 deletions

View File

@@ -6,31 +6,27 @@ import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import { Sheet, SheetClose, SheetContent, SheetDescription, SheetTitle } from '@/components/ui/sheet';
import { AgentCreationDialog, type AgentCreationInput } from '@/components/opencode/AgentCreationDialog';
import { AgentCreationDialog, type AgentCreationInput } from '@/components/coding/AgentCreationDialog';
import { ProjectPublishAction } from '@/components/works/ProjectPublishAction';
import { hostApiFetch } from '@/lib/host-api';
import { buildConfiguredModelOptions, getConfiguredModelCapabilityLabel, type ConfiguredModelOption } from '@/lib/model-options';
import { buildCodingModelOptions, parseCodingModelKey, type CodingModelOption } from '@/lib/coding-model-options';
import { getCodingSkills } from '@/lib/coding-product-tools';
import { abortCodingConversation } from '@/lib/coding-conversations';
import { getSkillDisplayInfo } from '@/lib/skill-display';
import { getAgentAvatarSrc } from '@/lib/agent-avatars';
import { useOpencodeStore } from '@/stores/opencode';
import { useProjectConversationStore } from '@/stores/project-conversations';
import { codingConversationStore } from '@/stores/coding-conversations';
import { useCodingWorkspaceStore } from '@/stores/coding-workspace';
import { useProjectConfigStore } from '@/stores/project-config';
import { useProviderStore } from '@/stores/providers';
import { validateAgentConfigs, type ProjectAgentConfig, type ProjectConfig } from '../../../shared/project-config';
import type { CodingProjectAgent, CodingProjectConfig } from '@/types/coding-project';
const EMPTY_FILES: string[] = [];
type DrawerMode = 'models' | 'knowledge' | 'skills' | null;
type SkillStructureEntry = { path: string; type: 'file' | 'directory' };
type RuntimeSkillInfo = { name: string; description?: string; location?: string; content?: string; entries?: SkillStructureEntry[] };
type SkillInfo = { id: string; name: string; description: string; location: string; content: string; entries: SkillStructureEntry[] };
function isMissingRuntimeSessionError(error: unknown): boolean {
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
return message.includes('404') || message.includes('not found') || message.includes('不存在');
}
function createCustomAgent(index: number, model: string | null): ProjectAgentConfig {
function createCustomAgent(index: number, modelKey: string): CodingProjectAgent {
const now = new Date().toISOString();
const model = parseCodingModelKey(modelKey);
return {
id: `custom-agent-${Date.now()}-${index}`,
avatarId: 'avatar-01',
@@ -38,7 +34,8 @@ function createCustomAgent(index: number, model: string | null): ProjectAgentCon
name: '',
builtIn: false,
enabled: true,
model,
model: model ? { ...model, thinkingLevel: 'off' } : null,
modelResolution: model ? 'resolved' : 'required',
skillIds: [],
responsibility: { mission: '', owns: [], boundaries: [], collaborators: [], principles: [] },
prompt: '',
@@ -82,26 +79,26 @@ function SkillDetail({ skill }: { skill: SkillInfo }) {
</div>;
}
function AgentCard({ agent, onOpen }: { agent: ProjectAgentConfig; onOpen: () => void }) {
function AgentCard({ agent, onOpen }: { agent: CodingProjectAgent; onOpen: () => void }) {
return <button type="button" data-testid={`project-agent-${agent.id}`} onClick={onOpen} className="config-motion-agent surface-card motion-press h-[260px] w-[210px] flex-none rounded-2xl border border-border/70 bg-background p-4 text-left shadow-none hover:shadow-soft"><img src={getAgentAvatarSrc(agent.avatarId, agent.avatarDataUrl)} alt="" className="agent-motion-avatar mx-auto h-20 w-20 rounded-xl border border-border object-cover [image-rendering:pixelated]" /><div className="mt-4 flex items-center justify-between gap-2"><p className="truncate font-semibold">{agent.name || '未命名'}</p><Badge className="shrink-0 border border-brand/15 bg-brand-soft text-brand">可用</Badge></div><p className="mt-1 truncate text-xs font-semibold text-muted-foreground">{agent.roleName}</p><p className="mt-3 line-clamp-3 min-h-12 text-xs text-muted-foreground">{agent.responsibility.mission}</p><p className="mt-2 text-[11px] text-muted-foreground">{agent.skillIds.length} 个技能</p></button>;
}
function ModelCard({ model }: { model: ConfiguredModelOption }) {
return <div data-testid={`model-card-${model.modelRef.replace(/[^a-zA-Z0-9_-]+/gu, '-')}`} className="rounded-xl border border-foreground/15 bg-white p-3">
function ModelCard({ model }: { model: CodingModelOption }) {
return <div data-testid={`model-card-${model.key.replace(/[^a-zA-Z0-9_-]+/gu, '-')}`} className="rounded-xl border border-foreground/15 bg-white p-3">
<div className="flex items-start gap-3">
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-brand-soft text-lg" aria-hidden="true">🧠</span>
<div className="min-w-0 flex-1">
<div className="flex items-start justify-between gap-2">
<p className="min-w-0 truncate font-semibold" title={model.label}>{model.label}</p>
<Badge className="shrink-0 border border-border/80 bg-surface-subtle text-foreground">{getConfiguredModelCapabilityLabel(model)}</Badge>
<Badge className="shrink-0 border border-border/80 bg-surface-subtle text-foreground">已配置</Badge>
</div>
<p className="mt-1 truncate text-xs font-medium text-muted-foreground" title={model.runtimeProviderKey}>{model.runtimeProviderKey}</p>
<p className="mt-1 truncate text-xs font-medium text-muted-foreground" title={model.accountId}>{model.accountId}</p>
</div>
</div>
</div>;
}
function ModelList({ models }: { models: ConfiguredModelOption[] }) {
function ModelList({ models }: { models: CodingModelOption[] }) {
if (models.length === 0) {
return <div data-testid="project-model-empty-state" className="rounded-xl border border-dashed border-foreground/15 bg-white p-4 text-sm font-medium text-muted-foreground">
当前没有可用模型,请先在模型管理中完成配置;模型切换请进入伙伴配置。
@@ -110,32 +107,26 @@ function ModelList({ models }: { models: ConfiguredModelOption[] }) {
return <div data-testid="project-model-list" className="space-y-3">
<p className="text-xs font-medium leading-5 text-muted-foreground">以下模型已完成配置。项目只展示模型,实际使用模型由每个伙伴单独配置。</p>
<div className="space-y-2">
{models.map((model) => <ModelCard key={model.modelRef} model={model} />)}
{models.map((model) => <ModelCard key={model.key} model={model} />)}
</div>
</div>;
}
export function ProjectConfiguration() {
const activeProject = useOpencodeStore((state) => state.activeProject);
const activeProject = useCodingWorkspaceStore((state) => state.activeProject);
const conversations = useCodingWorkspaceStore((state) => state.conversations);
const removeProject = useCodingWorkspaceStore((state) => state.removeProject);
const reloadWorkspace = useCodingWorkspaceStore((state) => state.load);
const stored = useProjectConfigStore((state) => activeProject ? state.configsByProjectId[activeProject.id] : undefined);
const knowledge = useProjectConfigStore((state) => activeProject ? state.knowledgeByProjectId[activeProject.id] ?? EMPTY_FILES : EMPTY_FILES);
const load = useProjectConfigStore((state) => state.load);
const save = useProjectConfigStore((state) => state.save);
const uploadKnowledge = useProjectConfigStore((state) => state.uploadKnowledge);
const removeProjectConfig = useProjectConfigStore((state) => state.remove);
const removeProject = useOpencodeStore((state) => state.removeProject);
const abortSession = useOpencodeStore((state) => state.abortSession);
const deleteOpencodeSession = useOpencodeStore((state) => state.deleteSession);
const sessionStatuses = useOpencodeStore((state) => state.sessionStatuses);
const sendingSessionIds = useOpencodeStore((state) => state.sendingSessionIds);
const queuedSessionPrompts = useOpencodeStore((state) => state.queuedSessionPrompts);
const loadConversationState = useProjectConversationStore((state) => state.load);
const deleteProjectSession = useProjectConversationStore((state) => state.deleteSession);
const providerAccounts = useProviderStore((state) => state.accounts);
const providerStatuses = useProviderStore((state) => state.statuses);
const providerDefaultAccountId = useProviderStore((state) => state.defaultAccountId);
const providerVendors = useProviderStore((state) => state.vendors);
const refreshProviderSnapshot = useProviderStore((state) => state.refreshProviderSnapshot);
const [draft, setDraft] = useState<ProjectConfig | null>(null);
const [draft, setDraft] = useState<CodingProjectConfig | null>(null);
const [drawerMode, setDrawerMode] = useState<DrawerMode>(null);
const [activeSkillId, setActiveSkillId] = useState<string | null>(null);
const [activeAgentId, setActiveAgentId] = useState<string | null>(null);
@@ -144,15 +135,14 @@ export function ProjectConfiguration() {
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [partnerDialogOpen, setPartnerDialogOpen] = useState(false);
const [archiveAgentId, setArchiveAgentId] = useState<string | null>(null);
const [deleteAgentId, setDeleteAgentId] = useState<string | null>(null);
const knowledgeInputRef = useRef<HTMLInputElement | null>(null);
const navigate = useNavigate();
const location = useLocation();
const loadSkills = useCallback(() => {
void hostApiFetch<{ skills?: RuntimeSkillInfo[] }>('/api/opencode/skills')
.then((result) => setSkills((result.skills ?? []).map((skill) => ({
id: skill.name,
...getSkillDisplayInfo(skill.name),
void getCodingSkills()
.then((result) => setSkills(result.skills.map((skill) => ({
id: skill.id,
...getSkillDisplayInfo(skill.id),
location: skill.location ?? '',
content: skill.content ?? '',
entries: skill.entries ?? [{ path: 'SKILL.md', type: 'file' }],
@@ -173,7 +163,10 @@ export function ProjectConfiguration() {
if (drawerMode === 'skills') loadSkills();
}, [drawerMode, loadSkills]);
useEffect(() => { void refreshProviderSnapshot(); }, [refreshProviderSnapshot]);
const modelOptions = useMemo(() => buildConfiguredModelOptions(providerAccounts, providerStatuses, providerDefaultAccountId), [providerAccounts, providerStatuses, providerDefaultAccountId]);
const modelOptions = useMemo(
() => buildCodingModelOptions(providerAccounts, providerVendors),
[providerAccounts, providerVendors],
);
const activeAgent = draft?.agents.find((agent) => agent.id === activeAgentId) ?? null;
const activeSkill = skills.find((skill) => skill.id === activeSkillId) ?? null;
const playSaveConfirmation = () => undefined;
@@ -182,18 +175,21 @@ export function ProjectConfiguration() {
if (!activeProject) return <main data-testid="project-configuration-empty-state" className="flex min-h-full items-start bg-background p-8 text-foreground"><p className="pt-4 text-xl font-semibold tracking-[-0.02em]">一念成光,万物可创。</p></main>;
if (!draft) return <div className="p-8 text-center text-sm font-medium">正在读取项目配置…</div>;
const updateAgent = (next: ProjectAgentConfig) => setDraft((current) => current ? { ...current, agents: current.agents.map((item) => item.id === next.id ? { ...next, updatedAt: new Date().toISOString() } : item) } : current);
const updateAgent = (next: CodingProjectAgent) => setDraft((current) => current ? { ...current, agents: current.agents.map((item) => item.id === next.id ? { ...next, updatedAt: new Date().toISOString() } : item) } : current);
const handlePartnerDialogOpenChange = (open: boolean) => {
setPartnerDialogOpen(open);
if (!open) setActiveAgentId(null);
};
const handleCreatePartner = async (input: AgentCreationInput) => {
const selectedModel = parseCodingModelKey(input.model);
if (!selectedModel) throw new Error('伙伴模型无效');
const agent = {
...createCustomAgent(draft.agents.length + 1, input.model),
name: input.name,
avatarId: input.avatarId,
avatarDataUrl: input.avatarDataUrl,
model: input.model,
model: { ...selectedModel, thinkingLevel: 'off' as const },
modelResolution: 'resolved' as const,
prompt: input.prompt,
skillIds: input.skillIds,
responsibility: { mission: input.responsibility, owns: [], boundaries: [], collaborators: [], principles: [] },
@@ -202,6 +198,8 @@ export function ProjectConfiguration() {
};
const handleUpdatePartner = async (input: AgentCreationInput) => {
if (!activeAgentId) return;
const selectedModel = parseCodingModelKey(input.model);
if (!selectedModel) throw new Error('伙伴模型无效');
setDraft((current) => current ? {
...current,
agents: current.agents.map((agent) => agent.id === activeAgentId ? {
@@ -209,7 +207,11 @@ export function ProjectConfiguration() {
name: input.name,
avatarId: input.avatarId,
avatarDataUrl: input.avatarDataUrl,
model: input.model,
model: {
...selectedModel,
thinkingLevel: agent.model?.thinkingLevel ?? 'off',
},
modelResolution: 'resolved',
prompt: input.prompt,
skillIds: input.skillIds,
responsibility: { ...agent.responsibility, mission: input.responsibility },
@@ -217,7 +219,30 @@ export function ProjectConfiguration() {
} : agent),
} : current);
};
const submit = async () => { const errors = validateAgentConfigs(draft.agents); if (errors.length) { toast.error('请补全伙伴名称、头像、伙伴模型和职责;名称还需要在项目内唯一。'); return; } setSaving(true); try { await save(activeProject.id, { ...draft, initialized: true }); playSaveConfirmation(); toast.success(draft.initialized ? '项目配置已更新' : '项目初始化完成'); } catch (error) { toast.error(error instanceof Error ? error.message : String(error)); } finally { setSaving(false); } };
const submit = async () => {
const names = draft.agents.map((agent) => agent.name.trim()).filter(Boolean);
const invalid = draft.agents.some((agent) => (
!agent.name.trim()
|| !agent.avatarId.trim()
|| !agent.model
|| !agent.responsibility.mission.trim()
)) || new Set(names).size !== draft.agents.length;
if (invalid) {
toast.error('请补全伙伴名称、头像、伙伴模型和职责;名称还需要在项目内唯一。');
return;
}
setSaving(true);
try {
await save(activeProject.id, { ...draft, initialized: true });
await reloadWorkspace();
playSaveConfirmation();
toast.success(draft.initialized ? '项目配置已更新' : '项目初始化完成');
} catch (error) {
toast.error(error instanceof Error ? error.message : String(error));
} finally {
setSaving(false);
}
};
const handleKnowledge = async (file?: File) => { if (!file) return; try { await uploadKnowledge(activeProject.id, file); toast.success(`已上传到 knowledge/${file.name}`); } catch (error) { toast.error(error instanceof Error ? error.message : String(error)); } };
const handleDeleteProject = async () => {
await removeProject(activeProject.id);
@@ -228,17 +253,14 @@ export function ProjectConfiguration() {
};
const handleArchiveAgent = async () => {
if (!archiveAgentId) return;
const conversationState = await loadConversationState(activeProject.id);
const sessionIds = conversationState.sessions
.filter((session) => session.agentId === archiveAgentId)
.map((session) => session.sessionId)
.filter((sessionId) => (
Boolean(sendingSessionIds[sessionId])
|| sessionStatuses[sessionId]?.type === 'busy'
|| sessionStatuses[sessionId]?.type === 'retry'
|| (queuedSessionPrompts[sessionId]?.length ?? 0) > 0
));
await Promise.all(sessionIds.map((sessionId) => abortSession(sessionId)));
const summaries = codingConversationStore.getState().summariesByConversationId;
const runningConversationIds = conversations
.filter((conversation) => (
conversation.agentId === archiveAgentId
&& summaries[conversation.id]?.runStatus !== 'idle'
))
.map((conversation) => conversation.id);
await Promise.allSettled(runningConversationIds.map(abortCodingConversation));
const archivedAt = new Date().toISOString();
setDraft((current) => current ? {
...current,
@@ -251,43 +273,6 @@ export function ProjectConfiguration() {
setActiveAgentId(null);
};
const handleDeleteAgent = async () => {
if (!deleteAgentId) return;
const target = draft.agents.find((agent) => agent.id === deleteAgentId);
if (!target || target.builtIn || !target.archivedAt) return;
const conversationState = await loadConversationState(activeProject.id);
const sessionIds = conversationState.sessions
.filter((session) => session.agentId === target.id)
.map((session) => session.sessionId);
const runningSessionIds = sessionIds.filter((sessionId) => (
Boolean(sendingSessionIds[sessionId])
|| sessionStatuses[sessionId]?.type === 'busy'
|| sessionStatuses[sessionId]?.type === 'retry'
|| (queuedSessionPrompts[sessionId]?.length ?? 0) > 0
));
await Promise.all(runningSessionIds.map((sessionId) => abortSession(sessionId)));
for (const sessionId of sessionIds) {
try {
await deleteOpencodeSession(sessionId);
} catch (error) {
if (!isMissingRuntimeSessionError(error)) throw error;
}
await deleteProjectSession(activeProject.id, sessionId);
}
const nextDraft = {
...draft,
agents: draft.agents.filter((agent) => agent.id !== target.id),
};
await save(activeProject.id, nextDraft);
setDraft(nextDraft);
setDeleteAgentId(null);
if (activeAgentId === target.id) {
setActiveAgentId(null);
setPartnerDialogOpen(false);
}
toast.success(`已永久删除伙伴“${target.name || '未命名伙伴'}”`);
};
return <main className="-m-5 flex min-h-[calc(100%+2.5rem)] min-h-0 flex-col overflow-auto bg-background p-4 text-foreground sm:-m-6 sm:min-h-[calc(100%+3rem)]" data-testid="project-configuration-page">
<div className="mx-auto flex min-h-0 w-full max-w-7xl flex-1 flex-col gap-3">
<div className="config-motion-title flex items-center gap-2">
@@ -297,7 +282,7 @@ export function ProjectConfiguration() {
<h1 className="text-xl font-semibold">配置你的项目空间</h1>
</div>
<section className="grid gap-3 md:grid-cols-3"><ResourceCard id="models" icon="🧠" title="大脑(大语言模型)" subtitle={`已配置${modelOptions.length}个`} onClick={() => setDrawerMode('models')} /><ResourceCard id="skills" icon={<Wrench className="h-5 w-5" />} title="工具箱(技能)" subtitle={`已安装${skills.length}个`} onClick={() => { setActiveSkillId(null); setDrawerMode('skills'); }} /><ResourceCard id="knowledge" icon="📓" title="笔记本(知识库)" subtitle={`已上传${knowledge.length}个`} onClick={() => setDrawerMode('knowledge')} /></section>
<section className="config-motion-partners surface-card rounded-2xl border border-border/70 bg-background p-5 shadow-soft"><div className="mb-4 flex items-center justify-between gap-3"><div><h2 className="text-xl font-semibold">我的伙伴</h2><p className="mt-1 text-sm font-medium text-muted-foreground">每个伙伴只属于当前项目;一个伙伴可以拥有多条独立会话。</p></div><Button type="button" size="icon" className="h-8 w-8 rounded-full border border-brand/20 bg-brand-soft text-brand" aria-label="创建伙伴" title="创建伙伴" onClick={() => { if (modelOptions.length === 0) { toast.error('请先在模型管理中配置至少一个可用模型。'); setDrawerMode('models'); return; } setActiveAgentId(null); setPartnerDialogOpen(true); }}><Plus className="h-4 w-4" /></Button></div><div data-testid="agent-cards-scroll-region" className="-m-2 flex gap-4 overflow-auto p-2">{draft.agents.filter((agent) => !agent.archivedAt).map((agent) => <AgentCard key={agent.id} agent={agent} onOpen={() => { setActiveAgentId(agent.id); setPartnerDialogOpen(true); }} />)}{draft.agents.every((agent) => agent.archivedAt) ? <div className="w-full rounded-xl border border-dashed border-foreground/15 p-6 text-center text-sm font-medium text-muted-foreground">还没有伙伴,点击右上角的 + 创建第一个。</div> : null}</div>{draft.agents.some((agent) => agent.archivedAt) ? <div className="mt-4 border-t border-border/70 pt-4"><p className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">已归档</p><div className="mt-2 flex flex-wrap gap-2">{draft.agents.filter((agent) => agent.archivedAt).map((agent) => <div key={agent.id} className="flex items-center gap-1"><Button type="button" variant="outline" className="border-border/70 bg-surface-subtle" onClick={() => updateAgent({ ...agent, archivedAt: null })}>{agent.name || '未命名伙伴'} · 恢复</Button>{!agent.builtIn ? <Button type="button" variant="outline" size="icon" aria-label={'永久删除伙伴 ' + (agent.name || '未命名伙伴')} title="永久删除伙伴" className="h-9 w-9 border-border/70 text-muted-foreground hover:bg-accent-soft hover:text-destructive" onClick={() => setDeleteAgentId(agent.id)}><Trash2 className="h-3.5 w-3.5" /></Button> : null}</div>)}</div></div> : null}</section>
<section className="config-motion-partners surface-card rounded-2xl border border-border/70 bg-background p-5 shadow-soft"><div className="mb-4 flex items-center justify-between gap-3"><div><h2 className="text-xl font-semibold">我的伙伴</h2><p className="mt-1 text-sm font-medium text-muted-foreground">每个伙伴只属于当前项目;一个伙伴可以拥有多条独立会话。</p></div><Button type="button" size="icon" className="h-8 w-8 rounded-full border border-brand/20 bg-brand-soft text-brand" aria-label="创建伙伴" title="创建伙伴" onClick={() => { if (modelOptions.length === 0) { toast.error('请先在模型管理中配置至少一个可用模型。'); setDrawerMode('models'); return; } setActiveAgentId(null); setPartnerDialogOpen(true); }}><Plus className="h-4 w-4" /></Button></div><div data-testid="agent-cards-scroll-region" className="-m-2 flex gap-4 overflow-auto p-2">{draft.agents.filter((agent) => !agent.archivedAt).map((agent) => <AgentCard key={agent.id} agent={agent} onOpen={() => { setActiveAgentId(agent.id); setPartnerDialogOpen(true); }} />)}{draft.agents.every((agent) => agent.archivedAt) ? <div className="w-full rounded-xl border border-dashed border-foreground/15 p-6 text-center text-sm font-medium text-muted-foreground">还没有伙伴,点击右上角的 + 创建第一个。</div> : null}</div>{draft.agents.some((agent) => agent.archivedAt) ? <div className="mt-4 border-t border-border/70 pt-4"><p className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">已归档</p><div className="mt-2 flex flex-wrap gap-2">{draft.agents.filter((agent) => agent.archivedAt).map((agent) => <Button key={agent.id} type="button" variant="outline" className="border-border/70 bg-surface-subtle" onClick={() => updateAgent({ ...agent, archivedAt: null })}>{agent.name || '未命名伙伴'} · 恢复</Button>)}</div></div> : null}</section>
<div data-testid="project-configuration-actions" className="glass-surface config-motion-actions sticky bottom-3 z-10 mt-auto flex shrink-0 flex-col gap-2 rounded-2xl border border-border/70 bg-background/85 p-2 shadow-float sm:flex-row sm:items-start sm:justify-between">
<Button type="button" variant="outline" size="sm" onClick={() => setDeleteDialogOpen(true)} className="h-9 w-full border-brand/25 bg-background px-3 font-semibold text-foreground shadow-none hover:border-brand/40 hover:bg-brand-soft sm:w-auto"><Trash2 className="mr-2 h-4 w-4" />删除项目</Button>
<div data-testid="project-configuration-primary-actions" className="flex w-full flex-col gap-2 sm:ml-auto sm:w-auto sm:flex-row sm:items-start sm:justify-end">
@@ -343,17 +328,6 @@ export function ProjectConfiguration() {
onConfirm={handleArchiveAgent}
onError={(error) => toast.error(error instanceof Error ? error.message : String(error))}
/>
<ConfirmDialog
open={Boolean(deleteAgentId)}
title={`永久删除伙伴“${draft.agents.find((agent) => agent.id === deleteAgentId)?.name || '未命名伙伴'}”?`}
message="这会永久删除伙伴及其全部会话记录,删除后无法恢复。"
confirmLabel="永久删除"
cancelLabel="取消"
variant="destructive"
onCancel={() => setDeleteAgentId(null)}
onConfirm={handleDeleteAgent}
onError={(error) => toast.error(error instanceof Error ? error.message : String(error))}
/>
<ConfirmDialog
open={deleteDialogOpen}
title={`删除项目“${activeProject.name}”?`}