fix(desktop): component cleanup + dead code removal + DeerFlow ai-elements

- ChatArea: DeerFlow ai-elements annotations for accessibility
- Conversation: remove unused Context, simplify message rendering
- Delete dead modules: audit-logger.ts, gateway-reconnect.ts
- Replace console.log with structured logger across components
- Add idb dependency for IndexedDB persistence
- Fix kernel-skills type safety improvements
This commit is contained in:
iven
2026-04-03 00:28:58 +08:00
parent 15d578c5bc
commit 5c74e74f2a
19 changed files with 78 additions and 341 deletions

View File

@@ -43,6 +43,7 @@
"clsx": "^2.1.1",
"dompurify": "^3.3.3",
"framer-motion": "^12.38.0",
"idb": "^8.0.3",
"lucide-react": "^0.577.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",

View File

@@ -29,6 +29,9 @@ importers:
framer-motion:
specifier: ^12.38.0
version: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
idb:
specifier: ^8.0.3
version: 8.0.3
lucide-react:
specifier: ^0.577.0
version: 0.577.0(react@19.2.4)
@@ -2075,6 +2078,9 @@ packages:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
idb@8.0.3:
resolution: {integrity: sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==}
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@@ -5323,6 +5329,8 @@ snapshots:
dependencies:
safer-buffer: 2.1.2
idb@8.0.3: {}
ignore@5.3.2: {}
ignore@7.0.5: {}

View File

@@ -1,11 +1,13 @@
import { useState, useEffect, useRef, useCallback, useMemo, type MutableRefObject, type RefObject, type CSSProperties } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { List, type ListImperativeAPI } from 'react-window';
import { useChatStore, Message } from '../store/chatStore';
import { useChatStore, type Message } from '../store/chatStore';
import { useConversationStore } from '../store/chat/conversationStore';
import { useArtifactStore } from '../store/chat/artifactStore';
import { useConnectionStore } from '../store/connectionStore';
import { useAgentStore } from '../store/agentStore';
import { useConfigStore } from '../store/configStore';
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
import { Paperclip, SquarePen, ArrowUp, MessageSquare, Download, X, FileText, Image as ImageIcon } from 'lucide-react';
import { Button, EmptyState, MessageListSkeleton, LoadingDots } from './ui';
import { ResizableChatLayout } from './ai/ResizableChatLayout';
@@ -45,11 +47,14 @@ const VIRTUALIZATION_THRESHOLD = 100;
export function ChatArea() {
const {
messages, currentAgent, isStreaming, isLoading, currentModel,
sendMessage: sendToGateway, setCurrentModel, initStreamListener,
messages, isStreaming, isLoading,
sendMessage: sendToGateway, initStreamListener,
newConversation, chatMode, setChatMode, suggestions,
totalInputTokens, totalOutputTokens,
} = useChatStore();
const currentAgent = useConversationStore((s) => s.currentAgent);
const currentModel = useConversationStore((s) => s.currentModel);
const setCurrentModel = useConversationStore((s) => s.setCurrentModel);
const {
artifacts, selectedArtifactId, artifactPanelOpen,
selectArtifact, setArtifactPanelOpen,
@@ -152,6 +157,29 @@ export function ChatArea() {
return unsub;
}, []);
// Listen for hand-execution-complete Tauri events
useEffect(() => {
let unlisten: UnlistenFn | undefined;
listen<{ approvalId: string; handId: string; success: boolean; error?: string | null }>(
'hand-execution-complete',
(event) => {
const { handId, success, error } = event.payload;
useChatStore.getState().addMessage({
id: crypto.randomUUID(),
role: 'hand',
content: success
? `Hand ${handId} 执行完成`
: `Hand ${handId} 执行失败: ${error || '未知错误'}`,
timestamp: new Date(),
handName: handId,
handStatus: success ? 'completed' : 'failed',
handResult: event.payload,
});
},
).then((fn) => { unlisten = fn; });
return () => { unlisten?.(); };
}, []);
// Auto-scroll to bottom on new messages
useEffect(() => {
if (scrollRef.current && !useVirtualization) {

View File

@@ -3,6 +3,7 @@ import { useAgentStore } from '../store/agentStore';
import { useConnectionStore } from '../store/connectionStore';
import { useConfigStore } from '../store/configStore';
import { toChatAgent, useChatStore } from '../store/chatStore';
import { useConversationStore } from '../store/chat/conversationStore';
import { Bot, Plus, X, Globe, Cat, Search, BarChart2, Sparkles } from 'lucide-react';
import { AgentOnboardingWizard } from './AgentOnboardingWizard';
import type { Clone } from '../store/agentStore';
@@ -13,7 +14,8 @@ export function CloneManager() {
const deleteClone = useAgentStore((s) => s.deleteClone);
const connectionState = useConnectionStore((s) => s.connectionState);
const quickConfig = useConfigStore((s) => s.quickConfig);
const { agents, currentAgent, setCurrentAgent } = useChatStore();
const { agents, currentAgent } = useConversationStore();
const setCurrentAgent = useChatStore((s) => s.setCurrentAgent);
const [showWizard, setShowWizard] = useState(false);
const connected = connectionState === 'connected';

View File

@@ -1,4 +1,5 @@
import { useState, useRef, useEffect } from 'react';
import { useConversationStore } from '../store/chat/conversationStore';
import { useChatStore } from '../store/chatStore';
import { MessageSquare, Trash2, SquarePen, Download, Check, X } from 'lucide-react';
import { EmptyConversations } from './ui';
@@ -171,15 +172,14 @@ function ConversationItem({
}
export function ConversationList() {
const {
conversations,
currentConversationId,
switchConversation,
deleteConversation,
} = useChatStore();
const conversations = useConversationStore((s) => s.conversations);
const currentConversationId = useConversationStore((s) => s.currentConversationId);
const { switchConversation, deleteConversation } = useChatStore();
// suppress unused-var lint — these facade actions are needed
void switchConversation; void deleteConversation;
const handleRename = (id: string, newTitle: string) => {
useChatStore.setState((state) => ({
useConversationStore.setState((state) => ({
conversations: state.conversations.map((c) =>
c.id === id ? { ...c, title: newTitle, updatedAt: new Date() } : c
),

View File

@@ -27,7 +27,7 @@ import {
type IdentityChangeProposal as Proposal,
type IdentitySnapshot,
} from '../lib/intelligence-client';
import { useChatStore } from '../store/chatStore';
import { useConversationStore } from '../store/chat/conversationStore';
import { Button, Badge } from './ui';
// === Error Parsing Utility ===
@@ -306,7 +306,7 @@ function HistoryItem({
// === Main Component ===
export function IdentityChangeProposalPanel() {
const { currentAgent } = useChatStore();
const currentAgent = useConversationStore((s) => s.currentAgent);
const [proposals, setProposals] = useState<Proposal[]>([]);
const [snapshots, setSnapshots] = useState<IdentitySnapshot[]>([]);
const [loading, setLoading] = useState(true);

View File

@@ -33,7 +33,7 @@ import {
type GraphEdge,
type MemoryType,
} from '../store/memoryGraphStore';
import { useChatStore } from '../store/chatStore';
import { useConversationStore } from '../store/chat/conversationStore';
import { cardHover, defaultTransition } from '../lib/animations';
// Mark as intentionally unused for future use
@@ -157,7 +157,7 @@ export function MemoryGraph({ className = '' }: MemoryGraphProps) {
const [showFilters, setShowFilters] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const { currentAgent } = useChatStore();
const currentAgent = useConversationStore((s) => s.currentAgent);
const agentId = currentAgent?.id || 'zclaw-main';
const {

View File

@@ -12,7 +12,7 @@ import {
type MemoryType,
type MemoryStats,
} from '../lib/intelligence-client';
import { useChatStore } from '../store/chatStore';
import { useConversationStore } from '../store/chat/conversationStore';
const TYPE_LABELS: Record<MemoryType, { label: string; emoji: string; color: string }> = {
fact: { label: '事实', emoji: '📋', color: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300' },
@@ -23,7 +23,7 @@ const TYPE_LABELS: Record<MemoryType, { label: string; emoji: string; color: str
};
export function MemoryPanel() {
const { currentAgent } = useChatStore();
const currentAgent = useConversationStore((s) => s.currentAgent);
const agentId = currentAgent?.id || 'zclaw-main';
const [memories, setMemories] = useState<MemoryEntry[]>([]);

View File

@@ -5,6 +5,7 @@ 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 { useConversationStore } from '../store/chat/conversationStore';
import {
Wifi, WifiOff, Bot, BarChart3, Plug, RefreshCw,
MessageSquare, Cpu, FileText, User, Activity, Brain,
@@ -100,7 +101,9 @@ export function RightPanel() {
const workspaceInfo = useConfigStore((s) => s.workspaceInfo);
const quickConfig = useConfigStore((s) => s.quickConfig);
const { messages, currentModel, currentAgent, setCurrentAgent } = useChatStore();
const { messages, setCurrentAgent } = useChatStore();
const currentModel = useConversationStore((s) => s.currentModel);
const currentAgent = useConversationStore((s) => s.currentAgent);
const [activeTab, setActiveTab] = useState<'status' | 'files' | 'agent' | 'memory' | 'reflection' | 'autonomy' | 'evolution'>('status');
const [memoryViewMode, setMemoryViewMode] = useState<'list' | 'graph'>('list');
const [isEditingAgent, setIsEditingAgent] = useState(false);

View File

@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react';
import { useConnectionStore } from '../../store/connectionStore';
import { useConfigStore } from '../../store/configStore';
import { useChatStore } from '../../store/chatStore';
import { useConversationStore } from '../../store/chat/conversationStore';
import { getStoredGatewayToken, setStoredGatewayToken } from '../../lib/gateway-client';
import { silentErrorHandler } from '../../lib/error-utils';
@@ -13,7 +13,7 @@ export function General() {
const disconnect = useConnectionStore((s) => s.disconnect);
const quickConfig = useConfigStore((s) => s.quickConfig);
const saveQuickConfig = useConfigStore((s) => s.saveQuickConfig);
const currentModel = useChatStore((s) => s.currentModel);
const currentModel = useConversationStore((s) => s.currentModel);
const [theme, setTheme] = useState<'light' | 'dark'>(quickConfig.theme || 'light');
const [autoStart, setAutoStart] = useState(quickConfig.autoStart ?? false);
const [showToolCalls, setShowToolCalls] = useState(quickConfig.showToolCalls ?? false);

View File

@@ -3,7 +3,7 @@ import { invoke } from '@tauri-apps/api/core';
import { getStoredGatewayToken, getStoredGatewayUrl } from '../../lib/gateway-client';
import { useConnectionStore } from '../../store/connectionStore';
import { useConfigStore } from '../../store/configStore';
import { useChatStore } from '../../store/chatStore';
import { useConversationStore } from '../../store/chat/conversationStore';
import { silentErrorHandler } from '../../lib/error-utils';
import { secureStorage } from '../../lib/secure-storage';
import { Plus, Pencil, Trash2, Star, Eye, EyeOff, AlertCircle, X, Zap, Check } from 'lucide-react';
@@ -166,7 +166,8 @@ export function ModelsAPI() {
const disconnect = useConnectionStore((s) => s.disconnect);
const quickConfig = useConfigStore((s) => s.quickConfig);
const loadModels = useConfigStore((s) => s.loadModels);
const { currentModel, setCurrentModel } = useChatStore();
const currentModel = useConversationStore((s) => s.currentModel);
const setCurrentModel = useConversationStore((s) => s.setCurrentModel);
const [gatewayUrl, setGatewayUrl] = useState(getStoredGatewayUrl());
const [gatewayToken, setGatewayToken] = useState(quickConfig.gatewayToken || getStoredGatewayToken());

View File

@@ -1,41 +1,4 @@
import { useRef, useEffect, useState, createContext, useContext, useMemo, type ReactNode } from 'react';
// ---------------------------------------------------------------------------
// ConversationContext — shared state for child ai-elements components
// ---------------------------------------------------------------------------
interface ConversationContextValue {
isStreaming: boolean;
setIsStreaming: (v: boolean) => void;
messages: unknown[];
setMessages: (msgs: unknown[]) => void;
}
const ConversationContext = createContext<ConversationContextValue | null>(null);
export function useConversationContext() {
const ctx = useContext(ConversationContext);
if (!ctx) {
throw new Error('useConversationContext must be used within ConversationProvider');
}
return ctx;
}
export function ConversationProvider({ children }: { children: ReactNode }) {
const [isStreaming, setIsStreaming] = useState(false);
const [messages, setMessages] = useState<unknown[]>([]);
const value = useMemo(
() => ({ isStreaming, setIsStreaming, messages, setMessages }),
[isStreaming, messages],
);
return (
<ConversationContext.Provider value={value}>
{children}
</ConversationContext.Provider>
);
}
import { useRef, useEffect, type ReactNode } from 'react';
// ---------------------------------------------------------------------------
// Conversation container with auto-stick-to-bottom scroll behavior

View File

@@ -1,4 +1,4 @@
export { Conversation, ConversationProvider, useConversationContext } from './Conversation';
export { Conversation } from './Conversation';
export { ReasoningBlock } from './ReasoningBlock';
export { StreamingText } from './StreamingText';
export { ChatMode, type ChatModeType, type ChatModeConfig, CHAT_MODES } from './ChatMode';

View File

@@ -1,173 +0,0 @@
/**
* audit-logger.ts - 前端审计日志记录工具
*
* 为 ZCLAW 前端操作提供统一的审计日志记录功能。
* 记录关键操作Hand 触发、Agent 创建等)到本地存储。
*
* @reserved This module is reserved for future audit logging integration.
* It is not currently imported by any component. When audit logging is needed,
* import { logAudit, logAuditSuccess, logAuditFailure } from this module.
*/
import { createLogger } from './logger';
const log = createLogger('AuditLogger');
export type AuditAction =
| 'hand.trigger'
| 'hand.approve'
| 'hand.cancel'
| 'agent.create'
| 'agent.update'
| 'agent.delete';
export type AuditResult = 'success' | 'failure' | 'pending';
export interface FrontendAuditEntry {
id: string;
timestamp: string;
action: AuditAction;
target: string;
result: AuditResult;
actor?: string;
details?: Record<string, unknown>;
error?: string;
}
export interface AuditLogOptions {
action: AuditAction;
target: string;
result: AuditResult;
actor?: string;
details?: Record<string, unknown>;
error?: string;
}
const STORAGE_KEY = 'zclaw-audit-logs';
const MAX_LOCAL_LOGS = 500;
function generateId(): string {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
}
const bytes = crypto.getRandomValues(new Uint8Array(6));
const suffix = Array.from(bytes).map(b => b.toString(36).padStart(2, '0')).join('');
return `audit_${Date.now()}_${suffix}`;
}
function getTimestamp(): string {
return new Date().toISOString();
}
function loadLocalLogs(): FrontendAuditEntry[] {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (!stored) return [];
const logs = JSON.parse(stored) as FrontendAuditEntry[];
return Array.isArray(logs) ? logs : [];
} catch (e) {
log.debug('Failed to parse audit logs from localStorage', { error: e });
return [];
}
}
function saveLocalLogs(logs: FrontendAuditEntry[]): void {
try {
const trimmedLogs = logs.slice(-MAX_LOCAL_LOGS);
localStorage.setItem(STORAGE_KEY, JSON.stringify(trimmedLogs));
} catch (err) {
console.error('[AuditLogger] Failed to save logs to localStorage:', err);
}
}
class AuditLogger {
private logs: FrontendAuditEntry[] = [];
private initialized = false;
constructor() {
this.init();
}
private init(): void {
if (this.initialized) return;
this.logs = loadLocalLogs();
this.initialized = true;
}
async log(options: AuditLogOptions): Promise<FrontendAuditEntry> {
const entry: FrontendAuditEntry = {
id: generateId(),
timestamp: getTimestamp(),
action: options.action,
target: options.target,
result: options.result,
actor: options.actor,
details: options.details,
error: options.error,
};
this.logs.push(entry);
saveLocalLogs(this.logs);
log.debug(entry.action, entry.target, entry.result, entry.details || '');
return entry;
}
async logSuccess(
action: AuditAction,
target: string,
details?: Record<string, unknown>
): Promise<FrontendAuditEntry> {
return this.log({ action, target, result: 'success', details });
}
async logFailure(
action: AuditAction,
target: string,
error: string,
details?: Record<string, unknown>
): Promise<FrontendAuditEntry> {
return this.log({ action, target, result: 'failure', error, details });
}
getLogs(): FrontendAuditEntry[] {
return [...this.logs];
}
getLogsByAction(action: AuditAction): FrontendAuditEntry[] {
return this.logs.filter(log => log.action === action);
}
clearLogs(): void {
this.logs = [];
localStorage.removeItem(STORAGE_KEY);
}
exportLogs(): string {
return JSON.stringify(this.logs, null, 2);
}
}
export const auditLogger = new AuditLogger();
export function logAudit(options: AuditLogOptions): Promise<FrontendAuditEntry> {
return auditLogger.log(options);
}
export function logAuditSuccess(
action: AuditAction,
target: string,
details?: Record<string, unknown>
): Promise<FrontendAuditEntry> {
return auditLogger.logSuccess(action, target, details);
}
export function logAuditFailure(
action: AuditAction,
target: string,
error: string,
details?: Record<string, unknown>
): Promise<FrontendAuditEntry> {
return auditLogger.logFailure(action, target, error, details);
}

View File

@@ -1,80 +0,0 @@
/**
* gateway-reconnect.ts - Gateway Reconnect Methods
*
* Extracted from gateway-client.ts for modularity.
* Installs reconnect methods onto GatewayClient.prototype via mixin pattern.
*/
import type { GatewayClient } from './gateway-client';
// === Reconnect Constants ===
/** Maximum number of reconnect attempts before giving up */
export const MAX_RECONNECT_ATTEMPTS = 10;
// === Mixin Installer ===
/**
* Install reconnect methods onto GatewayClient.prototype.
*
* These methods access instance properties:
* - this.reconnectAttempts: number
* - this.reconnectInterval: number
* - this.reconnectTimer: number | null
* - this.log(level, message): void
* - this.connect(): Promise<void>
* - this.setState(state): void
* - this.emitEvent(event, payload): void
*/
export function installReconnectMethods(ClientClass: { prototype: GatewayClient }): void {
const proto = ClientClass.prototype as any;
/**
* Schedule a reconnect attempt with exponential backoff.
*/
proto.scheduleReconnect = function (this: GatewayClient): void {
const self = this as any;
if (self.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
self.log('error', `Max reconnect attempts (${MAX_RECONNECT_ATTEMPTS}) reached. Please reconnect manually.`);
self.setState('disconnected');
self.emitEvent('reconnect_failed', {
attempts: self.reconnectAttempts,
maxAttempts: MAX_RECONNECT_ATTEMPTS,
});
return;
}
self.reconnectAttempts++;
self.setState('reconnecting');
const delay = Math.min(self.reconnectInterval * Math.pow(1.5, self.reconnectAttempts - 1), 30000);
self.log('info', `Scheduling reconnect attempt ${self.reconnectAttempts} in ${delay}ms`);
// Emit reconnecting event for UI
self.emitEvent('reconnecting', {
attempt: self.reconnectAttempts,
delay,
maxAttempts: MAX_RECONNECT_ATTEMPTS,
});
self.reconnectTimer = window.setTimeout(async () => {
try {
await self.connect();
} catch (e) {
/* close handler will trigger another reconnect */
self.log('warn', `Reconnect attempt ${self.reconnectAttempts} failed: ${e instanceof Error ? e.message : String(e)}`);
}
}, delay);
};
/**
* Cancel a pending reconnect attempt.
*/
proto.cancelReconnect = function (this: GatewayClient): void {
const self = this as any;
if (self.reconnectTimer !== null) {
clearTimeout(self.reconnectTimer);
self.reconnectTimer = null;
}
};
}

View File

@@ -6,6 +6,7 @@
import { invoke } from '@tauri-apps/api/core';
import type { KernelClient } from './kernel-client';
import { useConversationStore } from '../store/chat/conversationStore';
/** Skill shape returned by list/refresh/create/update operations. */
type SkillItem = {
@@ -107,12 +108,16 @@ export function installSkillMethods(ClientClass: { prototype: KernelClient }): v
error?: string;
durationMs?: number;
}> {
const convStore = useConversationStore.getState();
const agent = convStore.currentAgent;
const sessionKey = convStore.sessionKey;
return invoke('skill_execute', {
id,
context: {
agentId: '',
sessionId: '',
workingDir: '',
agentId: agent?.id || 'zclaw-main',
sessionId: sessionKey || crypto.randomUUID(),
workingDir: null,
},
input: input || {},
});

View File

@@ -98,6 +98,7 @@ import type {
PromptCheckResult,
PromptTemplateInfo,
PromptVersionInfo,
PaginatedResponse,
AgentTemplateAvailable,
AgentTemplateFull,
} from './saas-types';

View File

@@ -277,28 +277,6 @@ function clearLocalStorageBackup(key: string): void {
}
}
// Keep synchronous versions for backward compatibility (deprecated)
function writeLocalStorageBackup(key: string, value: string): void {
try {
if (value) {
localStorage.setItem(key, value);
} else {
localStorage.removeItem(key);
}
} catch (e) {
logger.debug('writeLocalStorageBackup failed', { error: e });
}
}
function readLocalStorageBackup(key: string): string | null {
try {
return localStorage.getItem(key);
} catch (e) {
logger.debug('readLocalStorageBackup failed', { error: e });
return null;
}
}
// === Device Keys Secure Storage ===
/**

View File

@@ -12,7 +12,7 @@
*/
import { useEffect, useRef, useCallback } from 'react';
import { useChatStore } from '../store/chatStore';
import { useConversationStore } from '../store/chat/conversationStore';
import { intelligenceClient, type IdentityChangeProposal } from './intelligence-client';
import { createLogger } from './logger';
@@ -65,7 +65,7 @@ export function useProposalNotifications(): {
pendingCount: number;
refresh: () => Promise<void>;
} {
const { currentAgent } = useChatStore();
const currentAgent = useConversationStore((s) => s.currentAgent);
const agentId = currentAgent?.id;
const pendingCountRef = useRef(0);