diff --git a/desktop/src/components/IdentityChangeProposal.tsx b/desktop/src/components/IdentityChangeProposal.tsx
new file mode 100644
index 0000000..cc41e41
--- /dev/null
+++ b/desktop/src/components/IdentityChangeProposal.tsx
@@ -0,0 +1,476 @@
+/**
+ * Identity Change Proposal Component
+ *
+ * Displays pending personality change proposals with:
+ * - Side-by-side diff view
+ * - Accept/Reject buttons
+ * - Reason explanation
+ *
+ * Part of ZCLAW L4 Self-Evolution capability.
+ */
+
+import { useState, useEffect, useMemo } from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
+import {
+ Check,
+ X,
+ FileText,
+ Clock,
+ AlertCircle,
+ ChevronDown,
+ ChevronUp,
+ Sparkles,
+ History,
+} from 'lucide-react';
+import {
+ intelligenceClient,
+ type IdentityChangeProposal as Proposal,
+ type IdentitySnapshot,
+} from '../lib/intelligence-client';
+import { useChatStore } from '../store/chatStore';
+import { Button, Badge } from './ui';
+
+// === Diff View Component ===
+
+function DiffView({
+ current,
+ proposed,
+}: {
+ current: string;
+ proposed: string;
+}) {
+ const currentLines = useMemo(() => current.split('\n'), [current]);
+ const proposedLines = useMemo(() => proposed.split('\n'), [proposed]);
+
+ // Simple line-by-line diff
+ const maxLines = Math.max(currentLines.length, proposedLines.length);
+ const diffLines: Array<{
+ type: 'unchanged' | 'added' | 'removed' | 'modified';
+ current?: string;
+ proposed?: string;
+ lineNum: number;
+ }> = [];
+
+ // Build a simple diff - for a production system, use a proper diff algorithm
+ // Note: currentSet/proposedSet could be used for advanced diff algorithms
+ // const currentSet = new Set(currentLines);
+ // const proposedSet = new Set(proposedLines);
+
+ for (let i = 0; i < maxLines; i++) {
+ const currLine = currentLines[i];
+ const propLine = proposedLines[i];
+
+ if (currLine === propLine) {
+ diffLines.push({ type: 'unchanged', current: currLine, proposed: propLine, lineNum: i + 1 });
+ } else if (currLine === undefined) {
+ diffLines.push({ type: 'added', proposed: propLine, lineNum: i + 1 });
+ } else if (propLine === undefined) {
+ diffLines.push({ type: 'removed', current: currLine, lineNum: i + 1 });
+ } else {
+ diffLines.push({ type: 'modified', current: currLine, proposed: propLine, lineNum: i + 1 });
+ }
+ }
+
+ return (
+
+ {/* Current */}
+
+
+ 当前版本
+
+
+ {diffLines.map((line, idx) => (
+
+ {line.type === 'removed' && -}
+ {line.type === 'modified' && ~}
+ {line.current || '\u00A0'}
+
+ ))}
+
+
+
+ {/* Proposed */}
+
+
+ 建议版本
+
+
+ {diffLines.map((line, idx) => (
+
+ {line.type === 'added' && +}
+ {line.type === 'modified' && ~}
+ {line.proposed || '\u00A0'}
+
+ ))}
+
+
+
+ );
+}
+
+// === Single Proposal Card ===
+
+function ProposalCard({
+ proposal,
+ onApprove,
+ onReject,
+ isProcessing,
+}: {
+ proposal: Proposal;
+ onApprove: () => void;
+ onReject: () => void;
+ isProcessing: boolean;
+}) {
+ const [expanded, setExpanded] = useState(true);
+
+ const fileLabel = proposal.file === 'soul' ? 'SOUL.md' : 'Instructions';
+ const timeAgo = getTimeAgo(proposal.created_at);
+
+ return (
+
+ {/* Header */}
+ setExpanded(!expanded)}
+ >
+
+
+
+
+
+
+
+ 人格变更提案
+
+
+ {fileLabel}
+
+
+
+
+ {timeAgo}
+
+
+
+
+ {expanded ? (
+
+ ) : (
+
+ )}
+
+
+
+ {/* Content */}
+
+ {expanded && (
+
+
+ {/* Reason */}
+
+
+ 变更原因
+
+
{proposal.reason}
+
+
+ {/* Diff View */}
+
+
+ {/* Actions */}
+
+
+
+
+
+
+ )}
+
+
+ );
+}
+
+// === Evolution History Item ===
+
+function HistoryItem({
+ snapshot,
+ onRestore,
+ isRestoring,
+}: {
+ snapshot: IdentitySnapshot;
+ onRestore: () => void;
+ isRestoring: boolean;
+}) {
+ const timeAgo = getTimeAgo(snapshot.timestamp);
+
+ return (
+
+
+
+
+
+
+ {timeAgo}
+
+
+
+ {snapshot.reason || '自动快照'}
+
+
+
+ );
+}
+
+// === Main Component ===
+
+export function IdentityChangeProposalPanel() {
+ const { currentAgent } = useChatStore();
+ const [proposals, setProposals] = useState([]);
+ const [snapshots, setSnapshots] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [processingId, setProcessingId] = useState(null);
+ const [error, setError] = useState(null);
+
+ const agentId = currentAgent?.id;
+
+ // Load data
+ useEffect(() => {
+ if (!agentId) return;
+
+ const loadData = async () => {
+ setLoading(true);
+ setError(null);
+ try {
+ const [pendingProposals, agentSnapshots] = await Promise.all([
+ intelligenceClient.identity.getPendingProposals(agentId),
+ intelligenceClient.identity.getSnapshots(agentId, 10),
+ ]);
+ setProposals(pendingProposals);
+ setSnapshots(agentSnapshots);
+ } catch (err) {
+ console.error('[IdentityChangeProposal] Failed to load data:', err);
+ setError('加载失败');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ loadData();
+ }, [agentId]);
+
+ const handleApprove = async (proposalId: string) => {
+ if (!agentId) return;
+ setProcessingId(proposalId);
+ setError(null);
+ try {
+ await intelligenceClient.identity.approveProposal(proposalId);
+ // Refresh data
+ const [pendingProposals, agentSnapshots] = await Promise.all([
+ intelligenceClient.identity.getPendingProposals(agentId),
+ intelligenceClient.identity.getSnapshots(agentId, 10),
+ ]);
+ setProposals(pendingProposals);
+ setSnapshots(agentSnapshots);
+ } catch (err) {
+ console.error('[IdentityChangeProposal] Failed to approve:', err);
+ setError('审批失败');
+ } finally {
+ setProcessingId(null);
+ }
+ };
+
+ const handleReject = async (proposalId: string) => {
+ if (!agentId) return;
+ setProcessingId(proposalId);
+ setError(null);
+ try {
+ await intelligenceClient.identity.rejectProposal(proposalId);
+ // Refresh proposals
+ const pendingProposals = await intelligenceClient.identity.getPendingProposals(agentId);
+ setProposals(pendingProposals);
+ } catch (err) {
+ console.error('[IdentityChangeProposal] Failed to reject:', err);
+ setError('拒绝失败');
+ } finally {
+ setProcessingId(null);
+ }
+ };
+
+ const handleRestore = async (snapshotId: string) => {
+ if (!agentId) return;
+ setProcessingId(snapshotId);
+ setError(null);
+ try {
+ await intelligenceClient.identity.restoreSnapshot(agentId, snapshotId);
+ // Refresh snapshots
+ const agentSnapshots = await intelligenceClient.identity.getSnapshots(agentId, 10);
+ setSnapshots(agentSnapshots);
+ } catch (err) {
+ console.error('[IdentityChangeProposal] Failed to restore:', err);
+ setError('恢复失败');
+ } finally {
+ setProcessingId(null);
+ }
+ };
+
+ if (!agentId) {
+ return (
+
+ );
+ }
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {/* Error */}
+ {error && (
+
+ )}
+
+ {/* Pending Proposals */}
+
+
+
+ 待审批提案
+ {proposals.length > 0 && (
+
+ {proposals.length}
+
+ )}
+
+
+ {proposals.length === 0 ? (
+
+ 暂无待审批的人格变更提案
+
+ ) : (
+
+
+ {proposals.map((proposal) => (
+ handleApprove(proposal.id)}
+ onReject={() => handleReject(proposal.id)}
+ isProcessing={processingId === proposal.id}
+ />
+ ))}
+
+
+ )}
+
+
+ {/* Evolution History */}
+
+
+
+ 演化历史
+
+
+ {snapshots.length === 0 ? (
+
+ 暂无演化历史
+
+ ) : (
+
+ {snapshots.map((snapshot) => (
+ handleRestore(snapshot.id)}
+ isRestoring={processingId === snapshot.id}
+ />
+ ))}
+
+ )}
+
+
+ );
+}
+
+// === Helper ===
+
+function getTimeAgo(timestamp: string): string {
+ const now = Date.now();
+ const then = new Date(timestamp).getTime();
+ const diff = now - then;
+
+ if (diff < 60000) return '刚刚';
+ if (diff < 3600000) return `${Math.floor(diff / 60000)} 分钟前`;
+ if (diff < 86400000) return `${Math.floor(diff / 3600000)} 小时前`;
+ if (diff < 604800000) return `${Math.floor(diff / 86400000)} 天前`;
+ return new Date(timestamp).toLocaleDateString('zh-CN');
+}
+
+export default IdentityChangeProposalPanel;
diff --git a/desktop/src/components/ReflectionLog.tsx b/desktop/src/components/ReflectionLog.tsx
index f6a5f36..55df0dc 100644
--- a/desktop/src/components/ReflectionLog.tsx
+++ b/desktop/src/components/ReflectionLog.tsx
@@ -413,9 +413,34 @@ export function ReflectionLog({
const result = await intelligenceClient.reflection.reflect(agentId, []);
setHistory((prev) => [result, ...prev]);
- // Update pending proposals
- if (result.identity_proposals.length > 0) {
- setPendingProposals((prev) => [...prev, ...result.identity_proposals]);
+ // Convert reflection identity_proposals to actual identity proposals
+ // The reflection result contains proposals that need to be persisted
+ if (result.identity_proposals && result.identity_proposals.length > 0) {
+ for (const proposal of result.identity_proposals) {
+ try {
+ // Determine which file to modify based on the field
+ const file: 'soul' | 'instructions' =
+ proposal.field === 'soul' || proposal.field === 'instructions'
+ ? (proposal.field as 'soul' | 'instructions')
+ : proposal.field.toLowerCase().includes('soul')
+ ? 'soul'
+ : 'instructions';
+
+ // Persist the proposal to the identity system
+ await intelligenceClient.identity.proposeChange(
+ agentId,
+ file,
+ proposal.proposed_value,
+ proposal.reason
+ );
+ } catch (err) {
+ console.warn('[ReflectionLog] Failed to create identity proposal:', err);
+ }
+ }
+
+ // Refresh pending proposals from the identity system
+ const proposals = await intelligenceClient.identity.getPendingProposals(agentId);
+ setPendingProposals(proposals);
}
} catch (error) {
console.error('[ReflectionLog] Reflection failed:', error);
diff --git a/desktop/src/components/RightPanel.tsx b/desktop/src/components/RightPanel.tsx
index 56a5dc6..482c6a3 100644
--- a/desktop/src/components/RightPanel.tsx
+++ b/desktop/src/components/RightPanel.tsx
@@ -8,7 +8,7 @@ import { toChatAgent, useChatStore, type CodeBlock } from '../store/chatStore';
import {
Wifi, WifiOff, Bot, BarChart3, Plug, RefreshCw,
MessageSquare, Cpu, FileText, User, Activity, Brain,
- Shield, Sparkles, GraduationCap, List, Network
+ Shield, Sparkles, GraduationCap, List, Network, Dna
} from 'lucide-react';
// === Helper to extract code blocks from markdown content ===
@@ -74,6 +74,7 @@ import { MemoryGraph } from './MemoryGraph';
import { ReflectionLog } from './ReflectionLog';
import { AutonomyConfig } from './AutonomyConfig';
import { ActiveLearningPanel } from './ActiveLearningPanel';
+import { IdentityChangeProposalPanel } from './IdentityChangeProposal';
import { CodeSnippetPanel, type CodeSnippet } from './CodeSnippetPanel';
import { cardHover, defaultTransition } from '../lib/animations';
import { Button, Badge } from './ui';
@@ -101,7 +102,7 @@ export function RightPanel() {
const quickConfig = useConfigStore((s) => s.quickConfig);
const { messages, currentModel, currentAgent, setCurrentAgent } = useChatStore();
- const [activeTab, setActiveTab] = useState<'status' | 'files' | 'agent' | 'memory' | 'reflection' | 'autonomy' | 'learning'>('status');
+ const [activeTab, setActiveTab] = useState<'status' | 'files' | 'agent' | 'memory' | 'reflection' | 'autonomy' | 'learning' | 'evolution'>('status');
const [memoryViewMode, setMemoryViewMode] = useState<'list' | 'graph'>('list');
const [isEditingAgent, setIsEditingAgent] = useState(false);
const [agentDraft, setAgentDraft] = useState(null);
@@ -263,6 +264,12 @@ export function RightPanel() {
icon={}
label="学习"
/>
+ setActiveTab('evolution')}
+ icon={}
+ label="演化"
+ />
@@ -324,6 +331,8 @@ export function RightPanel() {
) : activeTab === 'learning' ? (
+ ) : activeTab === 'evolution' ? (
+
) : activeTab === 'agent' ? (