refactor(panel): 移除 Agent tab — 跨会话身份由 soul.md 接管
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Agent tab 展示的信息对用户无实际作用,身份记忆已通过 soul.md → pre_conversation_hook 实现跨会话。移除 Agent tab (简洁+专业模式),清理 ~280 行 dead code。
This commit is contained in:
@@ -1,21 +1,17 @@
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { motion } from 'framer-motion';
|
||||
import { getStoredGatewayUrl } from '../lib/gateway-client';
|
||||
import { useConnectionStore } from '../store/connectionStore';
|
||||
import { useAgentStore, type PluginStatus } from '../store/agentStore';
|
||||
import { useConfigStore } from '../store/configStore';
|
||||
import { toChatAgent, useChatStore, type CodeBlock } from '../store/chatStore';
|
||||
import { useChatStore, type CodeBlock } from '../store/chatStore';
|
||||
import { useConversationStore } from '../store/chat/conversationStore';
|
||||
import { intelligenceClient, type IdentitySnapshot } from '../lib/intelligence-client';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
|
||||
import type { AgentInfo } from '../lib/kernel-types';
|
||||
import {
|
||||
Wifi, WifiOff, Bot, BarChart3, Plug, RefreshCw,
|
||||
MessageSquare, Cpu, FileText, User, Activity, Brain,
|
||||
Shield, Sparkles, List, Network, Dna, History,
|
||||
ChevronDown, ChevronUp, RotateCcw, AlertCircle, Loader2,
|
||||
MessageSquare, Cpu, FileText, Activity, Brain,
|
||||
Shield, Sparkles, List, Network, Dna,
|
||||
ConciergeBell,
|
||||
} from 'lucide-react';
|
||||
import { ButlerPanel } from './ButlerPanel';
|
||||
@@ -86,7 +82,7 @@ import { IdentityChangeProposalPanel } from './IdentityChangeProposal';
|
||||
import { CodeSnippetPanel, type CodeSnippet } from './CodeSnippetPanel';
|
||||
import { cardHover, defaultTransition } from '../lib/animations';
|
||||
import { Button, Badge } from './ui';
|
||||
import { getPersonalityById } from '../lib/personality-presets';
|
||||
|
||||
import { silentErrorHandler } from '../lib/error-utils';
|
||||
|
||||
interface RightPanelProps {
|
||||
@@ -110,12 +106,10 @@ export function RightPanel({ simpleMode = false }: RightPanelProps) {
|
||||
const updateClone = useAgentStore((s) => s.updateClone);
|
||||
|
||||
// Config store
|
||||
const workspaceInfo = useConfigStore((s) => s.workspaceInfo);
|
||||
const quickConfig = useConfigStore((s) => s.quickConfig);
|
||||
|
||||
// Use shallow selector for message stats to avoid re-rendering during streaming.
|
||||
// Counts only change when messages are added/removed, not when content is appended.
|
||||
const setCurrentAgent = useChatStore((s) => s.setCurrentAgent);
|
||||
const { messageCount, userMsgCount, assistantMsgCount, toolCallCount } = useChatStore(
|
||||
useShallow((s) => ({
|
||||
messageCount: s.messages.length,
|
||||
@@ -133,36 +127,12 @@ export function RightPanel({ simpleMode = false }: RightPanelProps) {
|
||||
const messages = stableMessagesRef.current;
|
||||
const currentModel = useConversationStore((s) => s.currentModel);
|
||||
const currentAgent = useConversationStore((s) => s.currentAgent);
|
||||
const [activeTab, setActiveTab] = useState<'status' | 'files' | 'agent' | 'memory' | 'reflection' | 'autonomy' | 'evolution' | 'butler'>('status');
|
||||
const [activeTab, setActiveTab] = useState<'status' | 'files' | 'memory' | 'reflection' | 'autonomy' | 'evolution' | 'butler'>('status');
|
||||
const [memoryViewMode, setMemoryViewMode] = useState<'list' | 'graph'>('list');
|
||||
const [isEditingAgent, setIsEditingAgent] = useState(false);
|
||||
const [agentDraft, setAgentDraft] = useState<AgentDraft | null>(null);
|
||||
|
||||
// Identity snapshot state
|
||||
const [snapshots, setSnapshots] = useState<IdentitySnapshot[]>([]);
|
||||
const [snapshotsExpanded, setSnapshotsExpanded] = useState(false);
|
||||
const [snapshotsLoading, setSnapshotsLoading] = useState(false);
|
||||
const [snapshotsError, setSnapshotsError] = useState<string | null>(null);
|
||||
const [restoringSnapshotId, setRestoringSnapshotId] = useState<string | null>(null);
|
||||
const [confirmRestoreId, setConfirmRestoreId] = useState<string | null>(null);
|
||||
|
||||
// UserProfile from memory store (dynamic, learned from conversations)
|
||||
const [userProfile, setUserProfile] = useState<Record<string, unknown> | null>(null);
|
||||
|
||||
const connected = connectionState === 'connected';
|
||||
const selectedClone = useMemo(
|
||||
() => clones.find((clone) => clone.id === currentAgent?.id),
|
||||
[clones, currentAgent?.id]
|
||||
);
|
||||
const focusAreas = selectedClone?.scenarios?.length ? selectedClone.scenarios : ['coding', 'writing', 'research', 'product', 'data'];
|
||||
const bootstrapFiles = selectedClone?.bootstrapFiles || [];
|
||||
const gatewayUrl = quickConfig.gatewayUrl || getStoredGatewayUrl();
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedClone || isEditingAgent) return;
|
||||
setAgentDraft(createAgentDraft(selectedClone, currentModel));
|
||||
}, [selectedClone, currentModel, isEditingAgent]);
|
||||
|
||||
// Load data when connected
|
||||
useEffect(() => {
|
||||
if (connected) {
|
||||
@@ -172,31 +142,6 @@ export function RightPanel({ simpleMode = false }: RightPanelProps) {
|
||||
}
|
||||
}, [connected]);
|
||||
|
||||
// Fetch UserProfile from agent data (includes memory-learned profile)
|
||||
useEffect(() => {
|
||||
if (!currentAgent?.id) return;
|
||||
invoke<AgentInfo | null>('agent_get', { agentId: currentAgent.id })
|
||||
.then(data => setUserProfile(data?.userProfile ?? null))
|
||||
.catch(() => setUserProfile(null));
|
||||
}, [currentAgent?.id]);
|
||||
|
||||
// Listen for profile updates after conversations (fired after memory extraction completes)
|
||||
// This single handler handles both userProfile refresh and clone name refresh
|
||||
useEffect(() => {
|
||||
const handler = async (e: Event) => {
|
||||
const detail = (e as CustomEvent).detail;
|
||||
if (detail?.agentId === currentAgent?.id && currentAgent?.id) {
|
||||
invoke<AgentInfo | null>('agent_get', { agentId: currentAgent.id })
|
||||
.then(data => setUserProfile(data?.userProfile ?? null))
|
||||
.catch(() => {});
|
||||
// Refresh clones data so selectedClone (name, role, nickname, etc.) stays current
|
||||
await loadClones();
|
||||
}
|
||||
};
|
||||
window.addEventListener('zclaw:agent-profile-updated', handler);
|
||||
return () => window.removeEventListener('zclaw:agent-profile-updated', handler);
|
||||
}, [currentAgent?.id, clones]);
|
||||
|
||||
// Listen for Tauri identity update events (from Rust post_conversation_hook)
|
||||
// When agent name changes in soul.md, update AgentConfig.name and refresh panel
|
||||
useEffect(() => {
|
||||
@@ -218,84 +163,7 @@ export function RightPanel({ simpleMode = false }: RightPanelProps) {
|
||||
connect().catch(silentErrorHandler('RightPanel'));
|
||||
};
|
||||
|
||||
const handleStartEdit = () => {
|
||||
if (!selectedClone) return;
|
||||
setAgentDraft(createAgentDraft(selectedClone, currentModel));
|
||||
setIsEditingAgent(true);
|
||||
};
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
if (selectedClone) {
|
||||
setAgentDraft(createAgentDraft(selectedClone, currentModel));
|
||||
}
|
||||
setIsEditingAgent(false);
|
||||
};
|
||||
|
||||
const handleSaveAgent = async () => {
|
||||
if (!selectedClone || !agentDraft || !agentDraft.name.trim()) return;
|
||||
const updatedClone = await updateClone(selectedClone.id, {
|
||||
name: agentDraft.name.trim(),
|
||||
role: agentDraft.role.trim() || undefined,
|
||||
nickname: agentDraft.nickname.trim() || undefined,
|
||||
model: agentDraft.model.trim() || undefined,
|
||||
scenarios: agentDraft.scenarios.split(',').map((item) => item.trim()).filter(Boolean),
|
||||
workspaceDir: agentDraft.workspaceDir.trim() || undefined,
|
||||
userName: agentDraft.userName.trim() || undefined,
|
||||
userRole: agentDraft.userRole.trim() || undefined,
|
||||
restrictFiles: agentDraft.restrictFiles,
|
||||
privacyOptIn: agentDraft.privacyOptIn,
|
||||
});
|
||||
if (updatedClone) {
|
||||
setCurrentAgent(toChatAgent(updatedClone));
|
||||
setAgentDraft(createAgentDraft(updatedClone, updatedClone.model || currentModel));
|
||||
setIsEditingAgent(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadSnapshots = useCallback(async () => {
|
||||
const agentId = currentAgent?.id;
|
||||
if (!agentId) return;
|
||||
setSnapshotsLoading(true);
|
||||
setSnapshotsError(null);
|
||||
try {
|
||||
const result = await intelligenceClient.identity.getSnapshots(agentId, 20);
|
||||
setSnapshots(result);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
setSnapshotsError(`加载快照失败: ${msg}`);
|
||||
} finally {
|
||||
setSnapshotsLoading(false);
|
||||
}
|
||||
}, [currentAgent?.id]);
|
||||
|
||||
const handleRestoreSnapshot = useCallback(async (snapshotId: string) => {
|
||||
const agentId = currentAgent?.id;
|
||||
if (!agentId) return;
|
||||
setRestoringSnapshotId(snapshotId);
|
||||
setSnapshotsError(null);
|
||||
setConfirmRestoreId(null);
|
||||
try {
|
||||
await intelligenceClient.identity.restoreSnapshot(agentId, snapshotId);
|
||||
await loadSnapshots();
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
setSnapshotsError(`回滚失败: ${msg}`);
|
||||
} finally {
|
||||
setRestoringSnapshotId(null);
|
||||
}
|
||||
}, [currentAgent?.id, loadSnapshots]);
|
||||
|
||||
// Load snapshots when agent tab is active and agent changes
|
||||
useEffect(() => {
|
||||
if (activeTab === 'agent' && currentAgent?.id) {
|
||||
loadSnapshots();
|
||||
}
|
||||
}, [activeTab, currentAgent?.id, loadSnapshots]);
|
||||
|
||||
const runtimeSummary = connected ? '已连接' : connectionState === 'connecting' ? '连接中...' : connectionState === 'reconnecting' ? '重连中...' : '未连接';
|
||||
const userNameDisplay = selectedClone?.userName || quickConfig.userName || 'User';
|
||||
const userAddressing = selectedClone?.userName || quickConfig.userName || 'User';
|
||||
const localTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone || '系统时区';
|
||||
|
||||
// Extract code blocks from all messages (both from codeBlocks property and content parsing)
|
||||
const codeSnippets = useMemo((): CodeSnippet[] => {
|
||||
@@ -339,7 +207,7 @@ export function RightPanel({ simpleMode = false }: RightPanelProps) {
|
||||
{/* 顶部工具栏 - Tab 栏 */}
|
||||
<div className="border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
|
||||
{simpleMode ? (
|
||||
/* 简洁模式: 仅 状态 / Agent / 管家 */
|
||||
/* 简洁模式: 仅 状态 / 管家 */
|
||||
<div className="flex items-center px-2 py-2 gap-1">
|
||||
<TabButton
|
||||
active={activeTab === 'status'}
|
||||
@@ -347,12 +215,6 @@ export function RightPanel({ simpleMode = false }: RightPanelProps) {
|
||||
icon={<Activity className="w-4 h-4" />}
|
||||
label="状态"
|
||||
/>
|
||||
<TabButton
|
||||
active={activeTab === 'agent'}
|
||||
onClick={() => setActiveTab('agent')}
|
||||
icon={<User className="w-4 h-4" />}
|
||||
label="Agent"
|
||||
/>
|
||||
<TabButton
|
||||
active={activeTab === 'butler'}
|
||||
onClick={() => setActiveTab('butler')}
|
||||
@@ -370,12 +232,6 @@ export function RightPanel({ simpleMode = false }: RightPanelProps) {
|
||||
icon={<Activity className="w-4 h-4" />}
|
||||
label="状态"
|
||||
/>
|
||||
<TabButton
|
||||
active={activeTab === 'agent'}
|
||||
onClick={() => setActiveTab('agent')}
|
||||
icon={<User className="w-4 h-4" />}
|
||||
label="Agent"
|
||||
/>
|
||||
<TabButton
|
||||
active={activeTab === 'files'}
|
||||
onClick={() => setActiveTab('files')}
|
||||
@@ -491,289 +347,6 @@ export function RightPanel({ simpleMode = false }: RightPanelProps) {
|
||||
<IdentityChangeProposalPanel />
|
||||
) : activeTab === 'butler' ? (
|
||||
<ButlerPanel agentId={currentAgent?.id} />
|
||||
) : activeTab === 'agent'? (
|
||||
<div className="space-y-4">
|
||||
<motion.div
|
||||
whileHover={cardHover}
|
||||
transition={defaultTransition}
|
||||
className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 shadow-sm"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-orange-400 to-red-500 flex items-center justify-center text-white text-lg font-semibold">
|
||||
{selectedClone?.emoji ? (
|
||||
<span className="text-2xl">{selectedClone.emoji}</span>
|
||||
) : (
|
||||
<span>🦞</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-base font-semibold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
||||
{selectedClone?.name || currentAgent?.name || '全能助手'}
|
||||
{selectedClone?.personality ? (
|
||||
<Badge variant="default" className="text-xs ml-1">
|
||||
{getPersonalityById(selectedClone.personality)?.label || selectedClone.personality}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="default" className="text-xs ml-1">
|
||||
友好亲切
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">{selectedClone?.role || '全能型 AI 助手'}</div>
|
||||
</div>
|
||||
</div>
|
||||
{selectedClone ? (
|
||||
isEditingAgent ? (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCancelEdit}
|
||||
aria-label="Cancel edit"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => { handleSaveAgent().catch(silentErrorHandler('RightPanel')); }}
|
||||
aria-label="Save edit"
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleStartEdit}
|
||||
aria-label="Edit Agent"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
whileHover={cardHover}
|
||||
transition={defaultTransition}
|
||||
className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 shadow-sm"
|
||||
>
|
||||
<div className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-3">关于我</div>
|
||||
{isEditingAgent && agentDraft ? (
|
||||
<div className="space-y-2">
|
||||
<AgentInput label="名称" value={agentDraft.name} onChange={(value) => setAgentDraft({ ...agentDraft, name: value })} />
|
||||
<AgentInput label="角色" value={agentDraft.role} onChange={(value) => setAgentDraft({ ...agentDraft, role: value })} />
|
||||
<AgentInput label="昵称" value={agentDraft.nickname} onChange={(value) => setAgentDraft({ ...agentDraft, nickname: value })} />
|
||||
<AgentInput label="模型" value={agentDraft.model} onChange={(value) => setAgentDraft({ ...agentDraft, model: value })} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 text-sm">
|
||||
<AgentRow label="角色" value={selectedClone?.role || '全能型 AI 助手'} />
|
||||
<AgentRow label="昵称" value={selectedClone?.nickname || '小龙'} />
|
||||
<AgentRow label="模型" value={selectedClone?.model || currentModel} />
|
||||
<AgentRow label="表情" value={selectedClone?.emoji || '🦞'} />
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
whileHover={cardHover}
|
||||
transition={defaultTransition}
|
||||
className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 shadow-sm"
|
||||
>
|
||||
<div className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-3">我眼中的你</div>
|
||||
{isEditingAgent && agentDraft ? (
|
||||
<div className="space-y-2">
|
||||
<AgentInput label="你的名称" value={agentDraft.userName} onChange={(value) => setAgentDraft({ ...agentDraft, userName: value })} />
|
||||
<AgentInput label="你的角色" value={agentDraft.userRole} onChange={(value) => setAgentDraft({ ...agentDraft, userRole: value })} />
|
||||
<AgentInput label="场景" value={agentDraft.scenarios} onChange={(value) => setAgentDraft({ ...agentDraft, scenarios: value })} placeholder="编程, 研究" />
|
||||
<AgentInput label="工作区" value={agentDraft.workspaceDir} onChange={(value) => setAgentDraft({ ...agentDraft, workspaceDir: value })} />
|
||||
<AgentToggle label="文件限制" checked={agentDraft.restrictFiles} onChange={(value) => setAgentDraft({ ...agentDraft, restrictFiles: value })} />
|
||||
<AgentToggle label="隐私计划" checked={agentDraft.privacyOptIn} onChange={(value) => setAgentDraft({ ...agentDraft, privacyOptIn: value })} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 text-sm">
|
||||
<AgentRow label="你的名称" value={userNameDisplay} />
|
||||
<AgentRow label="称呼方式" value={userAddressing} />
|
||||
<AgentRow label="时区" value={localTimezone} />
|
||||
<div className="flex gap-4">
|
||||
<div className="w-16 text-gray-500 dark:text-gray-400">专注</div>
|
||||
<div className="flex-1 flex flex-wrap gap-2">
|
||||
{focusAreas.map((item) => (
|
||||
<Badge key={item} variant="default">{item}</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<AgentRow label="工作区" value={selectedClone?.workspaceDir || workspaceInfo?.path || '~/.zclaw/zclaw-workspace'} />
|
||||
<AgentRow label="已解析" value={selectedClone?.workspaceResolvedPath || workspaceInfo?.resolvedPath || '-'} />
|
||||
<AgentRow label="文件限制" value={selectedClone?.restrictFiles ? '已开启' : '已关闭'} />
|
||||
<AgentRow label="隐私计划" value={selectedClone?.privacyOptIn ? '已加入' : '未加入'} />
|
||||
{/* Dynamic: UserProfile data (from conversation learning) */}
|
||||
{userProfile && (
|
||||
<div className="mt-3 pt-3 border-t border-gray-100 dark:border-gray-800">
|
||||
<div className="text-xs text-gray-400 mb-2">对话中了解到的</div>
|
||||
{userProfile.industry ? (
|
||||
<AgentRow label="行业" value={String(userProfile.industry)} />
|
||||
) : null}
|
||||
{userProfile.role ? (
|
||||
<AgentRow label="角色" value={String(userProfile.role)} />
|
||||
) : null}
|
||||
{userProfile.communicationStyle ? (
|
||||
<AgentRow label="沟通偏好" value={String(userProfile.communicationStyle)} />
|
||||
) : null}
|
||||
{Array.isArray(userProfile.recentTopics) && (userProfile.recentTopics as string[]).length > 0 ? (
|
||||
<AgentRow label="近期话题" value={(userProfile.recentTopics as string[]).slice(0, 5).join(', ')} />
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
whileHover={cardHover}
|
||||
transition={defaultTransition}
|
||||
className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 shadow-sm"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="text-sm font-semibold text-gray-900 dark:text-gray-100">引导文件</div>
|
||||
<Badge variant={selectedClone?.bootstrapReady ? 'success' : 'default'}>
|
||||
{selectedClone?.bootstrapReady ? '已生成' : '未生成'}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm">
|
||||
{bootstrapFiles.length > 0 ? bootstrapFiles.map((file) => (
|
||||
<div key={file.name} className="rounded-lg border border-gray-100 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50 px-3 py-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="font-medium text-gray-800 dark:text-gray-200">{file.name}</span>
|
||||
<Badge variant={file.exists ? 'success' : 'error'}>
|
||||
{file.exists ? '已存在' : '缺失'}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400 break-all">{file.path}</div>
|
||||
</div>
|
||||
)) : (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">该 Agent 尚未生成引导文件。</p>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 历史快照 */}
|
||||
<motion.div
|
||||
whileHover={cardHover}
|
||||
transition={defaultTransition}
|
||||
className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 shadow-sm"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="w-full flex items-center justify-between mb-0"
|
||||
onClick={() => setSnapshotsExpanded(!snapshotsExpanded)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<History className="w-4 h-4 text-gray-500 dark:text-gray-400" />
|
||||
<span className="text-sm font-semibold text-gray-900 dark:text-gray-100">历史快照</span>
|
||||
{snapshots.length > 0 && (
|
||||
<Badge variant="default" className="text-xs">{snapshots.length}</Badge>
|
||||
)}
|
||||
</div>
|
||||
{snapshotsExpanded ? (
|
||||
<ChevronUp className="w-4 h-4 text-gray-400" />
|
||||
) : (
|
||||
<ChevronDown className="w-4 h-4 text-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{snapshotsExpanded && (
|
||||
<div className="mt-3 space-y-2">
|
||||
{snapshotsError && (
|
||||
<div className="flex items-center gap-2 p-2 rounded-lg bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 text-xs">
|
||||
<AlertCircle className="w-3.5 h-3.5 flex-shrink-0" />
|
||||
<span>{snapshotsError}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{snapshotsLoading ? (
|
||||
<div className="flex items-center justify-center py-4 text-gray-500 dark:text-gray-400 text-xs">
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
加载中...
|
||||
</div>
|
||||
) : snapshots.length === 0 ? (
|
||||
<div className="text-center py-4 text-gray-500 dark:text-gray-400 text-xs bg-gray-50 dark:bg-gray-800/50 rounded-lg border border-gray-100 dark:border-gray-700">
|
||||
暂无快照记录
|
||||
</div>
|
||||
) : (
|
||||
snapshots.map((snap) => {
|
||||
const isRestoring = restoringSnapshotId === snap.id;
|
||||
const isConfirming = confirmRestoreId === snap.id;
|
||||
const timeLabel = formatSnapshotTime(snap.timestamp);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={snap.id}
|
||||
className="flex items-start gap-3 p-3 rounded-lg bg-gray-50 dark:bg-gray-800/50 border border-gray-100 dark:border-gray-700"
|
||||
>
|
||||
<div className="w-7 h-7 rounded-md bg-gray-200 dark:bg-gray-700 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<History className="w-3.5 h-3.5 text-gray-500 dark:text-gray-400" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">{timeLabel}</span>
|
||||
{isConfirming ? (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setConfirmRestoreId(null)}
|
||||
disabled={isRestoring}
|
||||
className="text-xs px-2 py-0.5 h-auto"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => handleRestoreSnapshot(snap.id)}
|
||||
disabled={isRestoring}
|
||||
className="text-xs px-2 py-0.5 h-auto bg-orange-500 hover:bg-orange-600"
|
||||
>
|
||||
{isRestoring ? (
|
||||
<Loader2 className="w-3 h-3 mr-1 animate-spin" />
|
||||
) : (
|
||||
<RotateCcw className="w-3 h-3 mr-1" />
|
||||
)}
|
||||
确认回滚
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setConfirmRestoreId(snap.id)}
|
||||
disabled={restoringSnapshotId !== null}
|
||||
className="text-xs text-gray-500 hover:text-orange-600 px-2 py-0.5 h-auto"
|
||||
title="回滚到此版本"
|
||||
>
|
||||
<RotateCcw className="w-3 h-3 mr-1" />
|
||||
回滚
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300 mt-1 truncate" title={snap.reason}>
|
||||
{snap.reason || '自动快照'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
) : activeTab === 'files' ? (
|
||||
<div className="p-4">
|
||||
<CodeSnippetPanel snippets={codeSnippets} />
|
||||
@@ -997,107 +570,3 @@ export function RightPanel({ simpleMode = false }: RightPanelProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function AgentRow({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
<div className="w-16 text-gray-500">{label}</div>
|
||||
<div className="flex-1 text-gray-700 break-all">{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type AgentDraft = {
|
||||
name: string;
|
||||
role: string;
|
||||
nickname: string;
|
||||
model: string;
|
||||
scenarios: string;
|
||||
workspaceDir: string;
|
||||
userName: string;
|
||||
userRole: string;
|
||||
restrictFiles: boolean;
|
||||
privacyOptIn: boolean;
|
||||
};
|
||||
|
||||
function createAgentDraft(
|
||||
clone: {
|
||||
name: string;
|
||||
role?: string;
|
||||
nickname?: string;
|
||||
model?: string;
|
||||
scenarios?: string[];
|
||||
workspaceDir?: string;
|
||||
userName?: string;
|
||||
userRole?: string;
|
||||
restrictFiles?: boolean;
|
||||
privacyOptIn?: boolean;
|
||||
},
|
||||
currentModel: string
|
||||
): AgentDraft {
|
||||
return {
|
||||
name: clone.name || '',
|
||||
role: clone.role || '',
|
||||
nickname: clone.nickname || '',
|
||||
model: clone.model || currentModel,
|
||||
scenarios: clone.scenarios?.join(', ') || '',
|
||||
workspaceDir: clone.workspaceDir || '~/.zclaw/zclaw-workspace',
|
||||
userName: clone.userName || '',
|
||||
userRole: clone.userRole || '',
|
||||
restrictFiles: clone.restrictFiles ?? true,
|
||||
privacyOptIn: clone.privacyOptIn ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
function AgentInput({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
}) {
|
||||
return (
|
||||
<label className="block">
|
||||
<div className="text-xs text-gray-500 mb-1">{label}</div>
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="w-full text-sm border border-gray-200 rounded-lg px-3 py-2 focus:outline-none"
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentToggle({
|
||||
label,
|
||||
checked,
|
||||
onChange,
|
||||
}: {
|
||||
label: string;
|
||||
checked: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<label className="flex items-center justify-between text-sm text-gray-700 border border-gray-100 rounded-lg px-3 py-2">
|
||||
<span>{label}</span>
|
||||
<input type="checkbox" checked={checked} onChange={(e) => onChange(e.target.checked)} />
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function formatSnapshotTime(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');
|
||||
}
|
||||
|
||||
@@ -9,6 +9,13 @@ tags: [log, history]
|
||||
|
||||
> Append-only 操作记录。格式: `## [日期] 类型 | 描述`
|
||||
|
||||
## [2026-04-23] fix | Agent 命名检测重构+跨会话记忆修复+Agent tab 移除
|
||||
- **fix(desktop)**: `detectAgentNameSuggestion` 从 6 个固定正则改为 trigger+extract 两步法 (10 个 trigger)
|
||||
- **fix(desktop)**: 名字检测从 memory extraction 解耦 — 502 不再阻断面板刷新
|
||||
- **fix(src-tauri)**: `agent_update` 同步写入 soul.md — config.name → system prompt 断链修复
|
||||
- **refactor(desktop)**: 移除 Agent tab (简洁模式/专业模式),清理 dead code (~280 行)
|
||||
- **验证**: cargo check 0 error, tsc --noEmit 0 error
|
||||
|
||||
## [2026-04-23] fix | 身份信号提取与持久化 — 对话中起名跨会话记忆+面板刷新
|
||||
- **fix(zclaw-growth)**: ProfileSignals 增加 agent_name/user_name 字段 + 提取提示词扩展 + 解析器+回退逻辑
|
||||
- **fix(zclaw-runtime)**: 身份信号存入 VikingStorage (importance=8)
|
||||
|
||||
@@ -133,6 +133,7 @@ tags: [module, memory, fts5, growth]
|
||||
|
||||
| 日期 | 变更 | 关联 |
|
||||
|------|------|------|
|
||||
| 2026-04-23 | agent_update 同步写 soul.md + 命名检测解耦 memory extraction + Agent tab 移除 | commit 394cb66+0bb5265+1c00290 |
|
||||
| 2026-04-23 | 身份信号提取: ProfileSignals+agent_name/user_name + VikingStorage identity 存储 + soul.md 写回 | commit 08812e5+e64a3ea |
|
||||
| 2026-04-22 | 跨会话记忆断裂修复: profile_store 连接 + 双数据库统一 + 诊断日志 | commit adf0251 |
|
||||
| 2026-04-22 | Wiki 5-section 重构: 363→~190 行,详细逻辑归档 | wiki/ |
|
||||
|
||||
Reference in New Issue
Block a user