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:
@@ -43,6 +43,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dompurify": "^3.3.3",
|
"dompurify": "^3.3.3",
|
||||||
"framer-motion": "^12.38.0",
|
"framer-motion": "^12.38.0",
|
||||||
|
"idb": "^8.0.3",
|
||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
|
|||||||
8
desktop/pnpm-lock.yaml
generated
8
desktop/pnpm-lock.yaml
generated
@@ -29,6 +29,9 @@ importers:
|
|||||||
framer-motion:
|
framer-motion:
|
||||||
specifier: ^12.38.0
|
specifier: ^12.38.0
|
||||||
version: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
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:
|
lucide-react:
|
||||||
specifier: ^0.577.0
|
specifier: ^0.577.0
|
||||||
version: 0.577.0(react@19.2.4)
|
version: 0.577.0(react@19.2.4)
|
||||||
@@ -2075,6 +2078,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
idb@8.0.3:
|
||||||
|
resolution: {integrity: sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==}
|
||||||
|
|
||||||
ignore@5.3.2:
|
ignore@5.3.2:
|
||||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
@@ -5323,6 +5329,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
safer-buffer: 2.1.2
|
safer-buffer: 2.1.2
|
||||||
|
|
||||||
|
idb@8.0.3: {}
|
||||||
|
|
||||||
ignore@5.3.2: {}
|
ignore@5.3.2: {}
|
||||||
|
|
||||||
ignore@7.0.5: {}
|
ignore@7.0.5: {}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { useState, useEffect, useRef, useCallback, useMemo, type MutableRefObject, type RefObject, type CSSProperties } from 'react';
|
import { useState, useEffect, useRef, useCallback, useMemo, type MutableRefObject, type RefObject, type CSSProperties } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { List, type ListImperativeAPI } from 'react-window';
|
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 { useArtifactStore } from '../store/chat/artifactStore';
|
||||||
import { useConnectionStore } from '../store/connectionStore';
|
import { useConnectionStore } from '../store/connectionStore';
|
||||||
import { useAgentStore } from '../store/agentStore';
|
import { useAgentStore } from '../store/agentStore';
|
||||||
import { useConfigStore } from '../store/configStore';
|
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 { Paperclip, SquarePen, ArrowUp, MessageSquare, Download, X, FileText, Image as ImageIcon } from 'lucide-react';
|
||||||
import { Button, EmptyState, MessageListSkeleton, LoadingDots } from './ui';
|
import { Button, EmptyState, MessageListSkeleton, LoadingDots } from './ui';
|
||||||
import { ResizableChatLayout } from './ai/ResizableChatLayout';
|
import { ResizableChatLayout } from './ai/ResizableChatLayout';
|
||||||
@@ -45,11 +47,14 @@ const VIRTUALIZATION_THRESHOLD = 100;
|
|||||||
|
|
||||||
export function ChatArea() {
|
export function ChatArea() {
|
||||||
const {
|
const {
|
||||||
messages, currentAgent, isStreaming, isLoading, currentModel,
|
messages, isStreaming, isLoading,
|
||||||
sendMessage: sendToGateway, setCurrentModel, initStreamListener,
|
sendMessage: sendToGateway, initStreamListener,
|
||||||
newConversation, chatMode, setChatMode, suggestions,
|
newConversation, chatMode, setChatMode, suggestions,
|
||||||
totalInputTokens, totalOutputTokens,
|
totalInputTokens, totalOutputTokens,
|
||||||
} = useChatStore();
|
} = useChatStore();
|
||||||
|
const currentAgent = useConversationStore((s) => s.currentAgent);
|
||||||
|
const currentModel = useConversationStore((s) => s.currentModel);
|
||||||
|
const setCurrentModel = useConversationStore((s) => s.setCurrentModel);
|
||||||
const {
|
const {
|
||||||
artifacts, selectedArtifactId, artifactPanelOpen,
|
artifacts, selectedArtifactId, artifactPanelOpen,
|
||||||
selectArtifact, setArtifactPanelOpen,
|
selectArtifact, setArtifactPanelOpen,
|
||||||
@@ -152,6 +157,29 @@ export function ChatArea() {
|
|||||||
return unsub;
|
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
|
// Auto-scroll to bottom on new messages
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (scrollRef.current && !useVirtualization) {
|
if (scrollRef.current && !useVirtualization) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useAgentStore } from '../store/agentStore';
|
|||||||
import { useConnectionStore } from '../store/connectionStore';
|
import { useConnectionStore } from '../store/connectionStore';
|
||||||
import { useConfigStore } from '../store/configStore';
|
import { useConfigStore } from '../store/configStore';
|
||||||
import { toChatAgent, useChatStore } from '../store/chatStore';
|
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 { Bot, Plus, X, Globe, Cat, Search, BarChart2, Sparkles } from 'lucide-react';
|
||||||
import { AgentOnboardingWizard } from './AgentOnboardingWizard';
|
import { AgentOnboardingWizard } from './AgentOnboardingWizard';
|
||||||
import type { Clone } from '../store/agentStore';
|
import type { Clone } from '../store/agentStore';
|
||||||
@@ -13,7 +14,8 @@ export function CloneManager() {
|
|||||||
const deleteClone = useAgentStore((s) => s.deleteClone);
|
const deleteClone = useAgentStore((s) => s.deleteClone);
|
||||||
const connectionState = useConnectionStore((s) => s.connectionState);
|
const connectionState = useConnectionStore((s) => s.connectionState);
|
||||||
const quickConfig = useConfigStore((s) => s.quickConfig);
|
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 [showWizard, setShowWizard] = useState(false);
|
||||||
|
|
||||||
const connected = connectionState === 'connected';
|
const connected = connectionState === 'connected';
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
|
import { useConversationStore } from '../store/chat/conversationStore';
|
||||||
import { useChatStore } from '../store/chatStore';
|
import { useChatStore } from '../store/chatStore';
|
||||||
import { MessageSquare, Trash2, SquarePen, Download, Check, X } from 'lucide-react';
|
import { MessageSquare, Trash2, SquarePen, Download, Check, X } from 'lucide-react';
|
||||||
import { EmptyConversations } from './ui';
|
import { EmptyConversations } from './ui';
|
||||||
@@ -171,15 +172,14 @@ function ConversationItem({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ConversationList() {
|
export function ConversationList() {
|
||||||
const {
|
const conversations = useConversationStore((s) => s.conversations);
|
||||||
conversations,
|
const currentConversationId = useConversationStore((s) => s.currentConversationId);
|
||||||
currentConversationId,
|
const { switchConversation, deleteConversation } = useChatStore();
|
||||||
switchConversation,
|
// suppress unused-var lint — these facade actions are needed
|
||||||
deleteConversation,
|
void switchConversation; void deleteConversation;
|
||||||
} = useChatStore();
|
|
||||||
|
|
||||||
const handleRename = (id: string, newTitle: string) => {
|
const handleRename = (id: string, newTitle: string) => {
|
||||||
useChatStore.setState((state) => ({
|
useConversationStore.setState((state) => ({
|
||||||
conversations: state.conversations.map((c) =>
|
conversations: state.conversations.map((c) =>
|
||||||
c.id === id ? { ...c, title: newTitle, updatedAt: new Date() } : c
|
c.id === id ? { ...c, title: newTitle, updatedAt: new Date() } : c
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import {
|
|||||||
type IdentityChangeProposal as Proposal,
|
type IdentityChangeProposal as Proposal,
|
||||||
type IdentitySnapshot,
|
type IdentitySnapshot,
|
||||||
} from '../lib/intelligence-client';
|
} from '../lib/intelligence-client';
|
||||||
import { useChatStore } from '../store/chatStore';
|
import { useConversationStore } from '../store/chat/conversationStore';
|
||||||
import { Button, Badge } from './ui';
|
import { Button, Badge } from './ui';
|
||||||
|
|
||||||
// === Error Parsing Utility ===
|
// === Error Parsing Utility ===
|
||||||
@@ -306,7 +306,7 @@ function HistoryItem({
|
|||||||
// === Main Component ===
|
// === Main Component ===
|
||||||
|
|
||||||
export function IdentityChangeProposalPanel() {
|
export function IdentityChangeProposalPanel() {
|
||||||
const { currentAgent } = useChatStore();
|
const currentAgent = useConversationStore((s) => s.currentAgent);
|
||||||
const [proposals, setProposals] = useState<Proposal[]>([]);
|
const [proposals, setProposals] = useState<Proposal[]>([]);
|
||||||
const [snapshots, setSnapshots] = useState<IdentitySnapshot[]>([]);
|
const [snapshots, setSnapshots] = useState<IdentitySnapshot[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
type GraphEdge,
|
type GraphEdge,
|
||||||
type MemoryType,
|
type MemoryType,
|
||||||
} from '../store/memoryGraphStore';
|
} from '../store/memoryGraphStore';
|
||||||
import { useChatStore } from '../store/chatStore';
|
import { useConversationStore } from '../store/chat/conversationStore';
|
||||||
import { cardHover, defaultTransition } from '../lib/animations';
|
import { cardHover, defaultTransition } from '../lib/animations';
|
||||||
|
|
||||||
// Mark as intentionally unused for future use
|
// Mark as intentionally unused for future use
|
||||||
@@ -157,7 +157,7 @@ export function MemoryGraph({ className = '' }: MemoryGraphProps) {
|
|||||||
const [showFilters, setShowFilters] = useState(false);
|
const [showFilters, setShowFilters] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
const { currentAgent } = useChatStore();
|
const currentAgent = useConversationStore((s) => s.currentAgent);
|
||||||
const agentId = currentAgent?.id || 'zclaw-main';
|
const agentId = currentAgent?.id || 'zclaw-main';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
type MemoryType,
|
type MemoryType,
|
||||||
type MemoryStats,
|
type MemoryStats,
|
||||||
} from '../lib/intelligence-client';
|
} 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 }> = {
|
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' },
|
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() {
|
export function MemoryPanel() {
|
||||||
const { currentAgent } = useChatStore();
|
const currentAgent = useConversationStore((s) => s.currentAgent);
|
||||||
const agentId = currentAgent?.id || 'zclaw-main';
|
const agentId = currentAgent?.id || 'zclaw-main';
|
||||||
|
|
||||||
const [memories, setMemories] = useState<MemoryEntry[]>([]);
|
const [memories, setMemories] = useState<MemoryEntry[]>([]);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useConnectionStore } from '../store/connectionStore';
|
|||||||
import { useAgentStore, type PluginStatus } from '../store/agentStore';
|
import { useAgentStore, type PluginStatus } from '../store/agentStore';
|
||||||
import { useConfigStore } from '../store/configStore';
|
import { useConfigStore } from '../store/configStore';
|
||||||
import { toChatAgent, useChatStore, type CodeBlock } from '../store/chatStore';
|
import { toChatAgent, useChatStore, type CodeBlock } from '../store/chatStore';
|
||||||
|
import { useConversationStore } from '../store/chat/conversationStore';
|
||||||
import {
|
import {
|
||||||
Wifi, WifiOff, Bot, BarChart3, Plug, RefreshCw,
|
Wifi, WifiOff, Bot, BarChart3, Plug, RefreshCw,
|
||||||
MessageSquare, Cpu, FileText, User, Activity, Brain,
|
MessageSquare, Cpu, FileText, User, Activity, Brain,
|
||||||
@@ -100,7 +101,9 @@ export function RightPanel() {
|
|||||||
const workspaceInfo = useConfigStore((s) => s.workspaceInfo);
|
const workspaceInfo = useConfigStore((s) => s.workspaceInfo);
|
||||||
const quickConfig = useConfigStore((s) => s.quickConfig);
|
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 [activeTab, setActiveTab] = useState<'status' | 'files' | 'agent' | 'memory' | 'reflection' | 'autonomy' | 'evolution'>('status');
|
||||||
const [memoryViewMode, setMemoryViewMode] = useState<'list' | 'graph'>('list');
|
const [memoryViewMode, setMemoryViewMode] = useState<'list' | 'graph'>('list');
|
||||||
const [isEditingAgent, setIsEditingAgent] = useState(false);
|
const [isEditingAgent, setIsEditingAgent] = useState(false);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useConnectionStore } from '../../store/connectionStore';
|
import { useConnectionStore } from '../../store/connectionStore';
|
||||||
import { useConfigStore } from '../../store/configStore';
|
import { useConfigStore } from '../../store/configStore';
|
||||||
import { useChatStore } from '../../store/chatStore';
|
import { useConversationStore } from '../../store/chat/conversationStore';
|
||||||
import { getStoredGatewayToken, setStoredGatewayToken } from '../../lib/gateway-client';
|
import { getStoredGatewayToken, setStoredGatewayToken } from '../../lib/gateway-client';
|
||||||
import { silentErrorHandler } from '../../lib/error-utils';
|
import { silentErrorHandler } from '../../lib/error-utils';
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ export function General() {
|
|||||||
const disconnect = useConnectionStore((s) => s.disconnect);
|
const disconnect = useConnectionStore((s) => s.disconnect);
|
||||||
const quickConfig = useConfigStore((s) => s.quickConfig);
|
const quickConfig = useConfigStore((s) => s.quickConfig);
|
||||||
const saveQuickConfig = useConfigStore((s) => s.saveQuickConfig);
|
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 [theme, setTheme] = useState<'light' | 'dark'>(quickConfig.theme || 'light');
|
||||||
const [autoStart, setAutoStart] = useState(quickConfig.autoStart ?? false);
|
const [autoStart, setAutoStart] = useState(quickConfig.autoStart ?? false);
|
||||||
const [showToolCalls, setShowToolCalls] = useState(quickConfig.showToolCalls ?? false);
|
const [showToolCalls, setShowToolCalls] = useState(quickConfig.showToolCalls ?? false);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { invoke } from '@tauri-apps/api/core';
|
|||||||
import { getStoredGatewayToken, getStoredGatewayUrl } from '../../lib/gateway-client';
|
import { getStoredGatewayToken, getStoredGatewayUrl } from '../../lib/gateway-client';
|
||||||
import { useConnectionStore } from '../../store/connectionStore';
|
import { useConnectionStore } from '../../store/connectionStore';
|
||||||
import { useConfigStore } from '../../store/configStore';
|
import { useConfigStore } from '../../store/configStore';
|
||||||
import { useChatStore } from '../../store/chatStore';
|
import { useConversationStore } from '../../store/chat/conversationStore';
|
||||||
import { silentErrorHandler } from '../../lib/error-utils';
|
import { silentErrorHandler } from '../../lib/error-utils';
|
||||||
import { secureStorage } from '../../lib/secure-storage';
|
import { secureStorage } from '../../lib/secure-storage';
|
||||||
import { Plus, Pencil, Trash2, Star, Eye, EyeOff, AlertCircle, X, Zap, Check } from 'lucide-react';
|
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 disconnect = useConnectionStore((s) => s.disconnect);
|
||||||
const quickConfig = useConfigStore((s) => s.quickConfig);
|
const quickConfig = useConfigStore((s) => s.quickConfig);
|
||||||
const loadModels = useConfigStore((s) => s.loadModels);
|
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 [gatewayUrl, setGatewayUrl] = useState(getStoredGatewayUrl());
|
||||||
const [gatewayToken, setGatewayToken] = useState(quickConfig.gatewayToken || getStoredGatewayToken());
|
const [gatewayToken, setGatewayToken] = useState(quickConfig.gatewayToken || getStoredGatewayToken());
|
||||||
|
|
||||||
|
|||||||
@@ -1,41 +1,4 @@
|
|||||||
import { useRef, useEffect, useState, createContext, useContext, useMemo, type ReactNode } from 'react';
|
import { useRef, useEffect, 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Conversation container with auto-stick-to-bottom scroll behavior
|
// Conversation container with auto-stick-to-bottom scroll behavior
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export { Conversation, ConversationProvider, useConversationContext } from './Conversation';
|
export { Conversation } from './Conversation';
|
||||||
export { ReasoningBlock } from './ReasoningBlock';
|
export { ReasoningBlock } from './ReasoningBlock';
|
||||||
export { StreamingText } from './StreamingText';
|
export { StreamingText } from './StreamingText';
|
||||||
export { ChatMode, type ChatModeType, type ChatModeConfig, CHAT_MODES } from './ChatMode';
|
export { ChatMode, type ChatModeType, type ChatModeConfig, CHAT_MODES } from './ChatMode';
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import type { KernelClient } from './kernel-client';
|
import type { KernelClient } from './kernel-client';
|
||||||
|
import { useConversationStore } from '../store/chat/conversationStore';
|
||||||
|
|
||||||
/** Skill shape returned by list/refresh/create/update operations. */
|
/** Skill shape returned by list/refresh/create/update operations. */
|
||||||
type SkillItem = {
|
type SkillItem = {
|
||||||
@@ -107,12 +108,16 @@ export function installSkillMethods(ClientClass: { prototype: KernelClient }): v
|
|||||||
error?: string;
|
error?: string;
|
||||||
durationMs?: number;
|
durationMs?: number;
|
||||||
}> {
|
}> {
|
||||||
|
const convStore = useConversationStore.getState();
|
||||||
|
const agent = convStore.currentAgent;
|
||||||
|
const sessionKey = convStore.sessionKey;
|
||||||
|
|
||||||
return invoke('skill_execute', {
|
return invoke('skill_execute', {
|
||||||
id,
|
id,
|
||||||
context: {
|
context: {
|
||||||
agentId: '',
|
agentId: agent?.id || 'zclaw-main',
|
||||||
sessionId: '',
|
sessionId: sessionKey || crypto.randomUUID(),
|
||||||
workingDir: '',
|
workingDir: null,
|
||||||
},
|
},
|
||||||
input: input || {},
|
input: input || {},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ import type {
|
|||||||
PromptCheckResult,
|
PromptCheckResult,
|
||||||
PromptTemplateInfo,
|
PromptTemplateInfo,
|
||||||
PromptVersionInfo,
|
PromptVersionInfo,
|
||||||
|
PaginatedResponse,
|
||||||
AgentTemplateAvailable,
|
AgentTemplateAvailable,
|
||||||
AgentTemplateFull,
|
AgentTemplateFull,
|
||||||
} from './saas-types';
|
} from './saas-types';
|
||||||
|
|||||||
@@ -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 ===
|
// === Device Keys Secure Storage ===
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useRef, useCallback } from 'react';
|
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 { intelligenceClient, type IdentityChangeProposal } from './intelligence-client';
|
||||||
import { createLogger } from './logger';
|
import { createLogger } from './logger';
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ export function useProposalNotifications(): {
|
|||||||
pendingCount: number;
|
pendingCount: number;
|
||||||
refresh: () => Promise<void>;
|
refresh: () => Promise<void>;
|
||||||
} {
|
} {
|
||||||
const { currentAgent } = useChatStore();
|
const currentAgent = useConversationStore((s) => s.currentAgent);
|
||||||
const agentId = currentAgent?.id;
|
const agentId = currentAgent?.id;
|
||||||
|
|
||||||
const pendingCountRef = useRef(0);
|
const pendingCountRef = useRef(0);
|
||||||
|
|||||||
Reference in New Issue
Block a user