diff --git a/crates/zclaw-runtime/src/loop_runner.rs b/crates/zclaw-runtime/src/loop_runner.rs index e7df5ad..ef31b7f 100644 --- a/crates/zclaw-runtime/src/loop_runner.rs +++ b/crates/zclaw-runtime/src/loop_runner.rs @@ -1074,6 +1074,7 @@ pub enum LoopEvent { ToolEnd { name: String, output: serde_json::Value }, /// Sub-agent task status update (started/running/completed/failed) SubtaskStatus { + task_id: String, description: String, status: String, detail: Option, diff --git a/crates/zclaw-runtime/src/tool/builtin/task.rs b/crates/zclaw-runtime/src/tool/builtin/task.rs index eec7a30..e4eb274 100644 --- a/crates/zclaw-runtime/src/tool/builtin/task.rs +++ b/crates/zclaw-runtime/src/tool/builtin/task.rs @@ -107,17 +107,19 @@ impl Tool for TaskTool { ); // Emit subtask_started event + // Create a sub-agent with its own ID + let sub_agent_id = AgentId::new(); + let task_id = sub_agent_id.to_string(); + if let Some(ref tx) = context.event_sender { let _ = tx.send(LoopEvent::SubtaskStatus { + task_id: task_id.clone(), description: description.to_string(), status: "started".to_string(), detail: None, }).await; } - // Create a sub-agent with its own ID - let sub_agent_id = AgentId::new(); - // Create a fresh session for the sub-agent let session_id = self.memory.create_session(&sub_agent_id).await?; @@ -160,6 +162,7 @@ impl Tool for TaskTool { // Emit subtask_running event if let Some(ref tx) = context.event_sender { let _ = tx.send(LoopEvent::SubtaskStatus { + task_id: task_id.clone(), description: description.to_string(), status: "running".to_string(), detail: Some("子Agent正在执行中...".to_string()), @@ -177,6 +180,7 @@ impl Tool for TaskTool { // Emit subtask_completed event if let Some(ref tx) = context.event_sender { let _ = tx.send(LoopEvent::SubtaskStatus { + task_id: task_id.clone(), description: description.to_string(), status: "completed".to_string(), detail: Some(format!( @@ -201,6 +205,7 @@ impl Tool for TaskTool { // Emit subtask_failed event if let Some(ref tx) = context.event_sender { let _ = tx.send(LoopEvent::SubtaskStatus { + task_id: task_id.clone(), description: description.to_string(), status: "failed".to_string(), detail: Some(e.to_string()), diff --git a/crates/zclaw-types/src/agent.rs b/crates/zclaw-types/src/agent.rs index 0df1b11..ed84fc8 100644 --- a/crates/zclaw-types/src/agent.rs +++ b/crates/zclaw-types/src/agent.rs @@ -128,6 +128,7 @@ impl AgentConfig { /// Agent runtime state #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] pub enum AgentState { /// Agent is running and can receive messages Running, diff --git a/desktop/src-tauri/src/kernel_commands/chat.rs b/desktop/src-tauri/src/kernel_commands/chat.rs index 166de51..5de9f0a 100644 --- a/desktop/src-tauri/src/kernel_commands/chat.rs +++ b/desktop/src-tauri/src/kernel_commands/chat.rs @@ -49,7 +49,7 @@ pub enum StreamChatEvent { ThinkingDelta { delta: String }, ToolStart { name: String, input: serde_json::Value }, ToolEnd { name: String, output: serde_json::Value }, - SubtaskStatus { description: String, status: String, detail: Option }, + SubtaskStatus { task_id: String, description: String, status: String, detail: Option }, IterationStart { iteration: usize, max_iterations: usize }, HandStart { name: String, params: serde_json::Value }, HandEnd { name: String, result: serde_json::Value }, @@ -341,9 +341,10 @@ pub async fn agent_chat_stream( StreamChatEvent::ToolEnd { name: name.clone(), output: output.clone() } } } - LoopEvent::SubtaskStatus { description, status, detail } => { - tracing::debug!("[agent_chat_stream] SubtaskStatus: {} - {}", description, status); + LoopEvent::SubtaskStatus { task_id, description, status, detail } => { + tracing::debug!("[agent_chat_stream] SubtaskStatus: {} - {} (id={})", description, status, task_id); StreamChatEvent::SubtaskStatus { + task_id: task_id.clone(), description: description.clone(), status: status.clone(), detail: detail.clone(), diff --git a/desktop/src/components/ChatArea.tsx b/desktop/src/components/ChatArea.tsx index e62cd91..33cf9ed 100644 --- a/desktop/src/components/ChatArea.tsx +++ b/desktop/src/components/ChatArea.tsx @@ -1,4 +1,4 @@ -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, createElement } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { List, type ListImperativeAPI } from 'react-window'; import { useChatStore, type Message } from '../store/chatStore'; @@ -262,7 +262,6 @@ export function ChatArea() { artifacts={artifacts} selectedId={selectedArtifactId} onSelect={selectArtifact} - onClose={() => setArtifactPanelOpen(false)} /> ); @@ -609,18 +608,21 @@ function MessageBubble({ message, setInput }: { message: Message; setInput: (tex const isUser = message.role === 'user'; const isThinking = message.streaming && !message.content; - // Extract typed arrays for JSX rendering (avoids TS2322 from && chain producing unknown) + // Extract typed arrays for JSX rendering const toolCallSteps: ToolCallStep[] | undefined = message.toolSteps; const subtaskList: Subtask[] | undefined = message.subtasks; - const renderToolSteps = (): React.ReactNode => { + // framer-motion 12 + React 19 type compat: motion.span/div return types resolve to `unknown`. + // Use createElement to bypass JSX child-context type inference. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const renderToolChain = (): any => { if (isUser || !toolCallSteps || toolCallSteps.length === 0) return null; - return ; + return createElement(ToolCallChain, { steps: toolCallSteps, isStreaming: !!message.streaming }); }; - - const renderSubtasks = (): React.ReactNode => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const renderSubtasks = (): any => { if (isUser || !subtaskList || subtaskList.length === 0) return null; - return ; + return createElement(TaskProgress, { tasks: subtaskList, className: 'mb-3' }); }; // Download message as Markdown file @@ -669,7 +671,7 @@ function MessageBubble({ message, setInput }: { message: Message; setInput: (tex /> )} {/* Tool call steps chain (DeerFlow-inspired) */} - {renderToolSteps()} + {renderToolChain()} {/* Subtask tracking (DeerFlow-inspired) */} {renderSubtasks()} {/* Message content with streaming support */} diff --git a/desktop/src/components/ai/ArtifactPanel.tsx b/desktop/src/components/ai/ArtifactPanel.tsx index d1d0749..bd730fb 100644 --- a/desktop/src/components/ai/ArtifactPanel.tsx +++ b/desktop/src/components/ai/ArtifactPanel.tsx @@ -28,7 +28,6 @@ interface ArtifactPanelProps { artifacts: ArtifactFile[]; selectedId?: string | null; onSelect: (id: string) => void; - onClose?: () => void; className?: string; } @@ -74,7 +73,6 @@ export function ArtifactPanel({ artifacts, selectedId, onSelect, - onClose: _onClose, className = '', }: ArtifactPanelProps) { const [viewMode, setViewMode] = useState<'preview' | 'code'>('preview'); diff --git a/desktop/src/hooks/index.ts b/desktop/src/hooks/index.ts index 75c9593..70d2823 100644 --- a/desktop/src/hooks/index.ts +++ b/desktop/src/hooks/index.ts @@ -4,16 +4,4 @@ * @module hooks */ -export { - useAutomationEvents, - useHandEvents, - useWorkflowEvents, -} from './useAutomationEvents'; - -// Re-export types from useAutomationEvents -export type { - UseAutomationEventsOptions, -} from './useAutomationEvents'; - export { useOptimisticMessages } from './useOptimisticMessages'; - diff --git a/desktop/src/lib/gateway-client.ts b/desktop/src/lib/gateway-client.ts index 98a758f..2fefc02 100644 --- a/desktop/src/lib/gateway-client.ts +++ b/desktop/src/lib/gateway-client.ts @@ -178,7 +178,7 @@ export class GatewayClient { onThinkingDelta?: (delta: string) => void; onTool?: (tool: string, input: string, output: string) => void; onHand?: (name: string, status: string, result?: unknown) => void; - onSubtaskStatus?: (description: string, status: string, detail?: string) => void; + onSubtaskStatus?: (taskId: string, description: string, status: string, detail?: string) => void; onComplete: (inputTokens?: number, outputTokens?: number) => void; onError: (error: string) => void; }>(); @@ -404,7 +404,7 @@ export class GatewayClient { const agents = await this.restGet>('/api/agents'); if (agents && agents.length > 0) { // Prefer agent with state "Running", otherwise use first agent - const runningAgent = agents.find((a: { id: string; name?: string; state?: string }) => a.state === 'Running'); + const runningAgent = agents.find((a: { id: string; name?: string; state?: string }) => a.state === 'running'); const defaultAgent = runningAgent || agents[0]; this.defaultAgentId = defaultAgent.id; this.log('info', `Fetched default agent from /api/agents: ${this.defaultAgentId} (${defaultAgent.name || 'unnamed'})`); @@ -470,7 +470,7 @@ export class GatewayClient { onThinkingDelta?: (delta: string) => void; onTool?: (tool: string, input: string, output: string) => void; onHand?: (name: string, status: string, result?: unknown) => void; - onSubtaskStatus?: (description: string, status: string, detail?: string) => void; + onSubtaskStatus?: (taskId: string, description: string, status: string, detail?: string) => void; onComplete: (inputTokens?: number, outputTokens?: number) => void; onError: (error: string) => void; }, @@ -652,7 +652,7 @@ export class GatewayClient { case 'subtask_status': // Sub-agent task status update if (callbacks.onSubtaskStatus && data.description) { - callbacks.onSubtaskStatus(data.description, data.status || '', data.detail); + callbacks.onSubtaskStatus(data.task_id || data.description, data.description, data.status || '', data.detail); } break; diff --git a/desktop/src/lib/gateway-types.ts b/desktop/src/lib/gateway-types.ts index cfb4c97..1e08ba6 100644 --- a/desktop/src/lib/gateway-types.ts +++ b/desktop/src/lib/gateway-types.ts @@ -88,6 +88,7 @@ export interface ZclawStreamEvent { agent_id?: string; agents?: Array<{ id: string; name: string; status: string }>; // Subtask status fields + task_id?: string; description?: string; status?: string; detail?: string; diff --git a/desktop/src/lib/kernel-chat.ts b/desktop/src/lib/kernel-chat.ts index 7a855f9..5f43f94 100644 --- a/desktop/src/lib/kernel-chat.ts +++ b/desktop/src/lib/kernel-chat.ts @@ -147,9 +147,10 @@ export function installChatMethods(ClientClass: { prototype: KernelClient }): vo break; case 'subtaskStatus': - log.debug('Subtask status:', streamEvent.description, streamEvent.status, streamEvent.detail); + log.debug('Subtask status:', streamEvent.taskId, streamEvent.description, streamEvent.status, streamEvent.detail); if (callbacks.onSubtaskStatus) { callbacks.onSubtaskStatus( + streamEvent.taskId, streamEvent.description, streamEvent.status, streamEvent.detail ?? undefined diff --git a/desktop/src/lib/kernel-types.ts b/desktop/src/lib/kernel-types.ts index cf991c9..58fef8b 100644 --- a/desktop/src/lib/kernel-types.ts +++ b/desktop/src/lib/kernel-types.ts @@ -69,7 +69,7 @@ export interface StreamCallbacks { onThinkingDelta?: (delta: string) => void; onTool?: (tool: string, input: string, output: string) => void; onHand?: (name: string, status: string, result?: unknown) => void; - onSubtaskStatus?: (description: string, status: string, detail?: string) => void; + onSubtaskStatus?: (taskId: string, description: string, status: string, detail?: string) => void; onComplete: (inputTokens?: number, outputTokens?: number) => void; onError: (error: string) => void; } @@ -129,6 +129,7 @@ export interface StreamEventHandEnd { export interface StreamEventSubtaskStatus { type: 'subtaskStatus'; + taskId: string; description: string; status: string; detail?: string; diff --git a/desktop/src/store/chat/streamStore.ts b/desktop/src/store/chat/streamStore.ts index 8f297fd..e7d9454 100644 --- a/desktop/src/store/chat/streamStore.ts +++ b/desktop/src/store/chat/streamStore.ts @@ -382,7 +382,7 @@ export const useStreamStore = create()( } } }, - onSubtaskStatus: (description: string, status: string, detail?: string) => { + onSubtaskStatus: (taskId: string, description: string, status: string, detail?: string) => { // Map backend status to frontend Subtask status const statusMap: Record = { started: 'pending', @@ -396,12 +396,12 @@ export const useStreamStore = create()( msgs.map(m => { if (m.id !== assistantId) return m; const subtasks = [...(m.subtasks || [])]; - const existingIdx = subtasks.findIndex(st => st.description === description); + const existingIdx = subtasks.findIndex(st => st.id === taskId); if (existingIdx >= 0) { subtasks[existingIdx] = { ...subtasks[existingIdx], status: mappedStatus, result: detail }; } else { subtasks.push({ - id: `subtask_${Date.now()}_${generateRandomString(4)}`, + id: taskId, description, status: mappedStatus, result: detail, diff --git a/desktop/src/types/index.ts b/desktop/src/types/index.ts index a3c0e86..476536c 100644 --- a/desktop/src/types/index.ts +++ b/desktop/src/types/index.ts @@ -129,34 +129,6 @@ export { createPaginatedResponse, } from './api-responses'; -// Automation Types -export type { - CategoryType, - CategoryConfig, - CategoryStats, - AutomationStatus, - AutomationType, - RunInfo, - ScheduleInfo, - AutomationItem, -} from './automation'; - -// Automation Constants and Functions -export { - HAND_CATEGORY_MAP, - CATEGORY_CONFIGS, - handStatusToAutomationStatus, - workflowStatusToAutomationStatus, - handToAutomationItem, - workflowToAutomationItem, - adaptToAutomationItems, - calculateCategoryStats, - filterByCategory, - filterByType, - filterByStatus, - searchAutomationItems, -} from './automation'; - // Classroom Types export type { AgentProfile,