diff --git a/data/conversations.db b/data/conversations.db new file mode 100644 index 00000000..4ebf78cf Binary files /dev/null and b/data/conversations.db differ diff --git a/data/conversations.db-shm b/data/conversations.db-shm new file mode 100644 index 00000000..f7cdced6 Binary files /dev/null and b/data/conversations.db-shm differ diff --git a/data/conversations.db-wal b/data/conversations.db-wal new file mode 100644 index 00000000..7aec4258 Binary files /dev/null and b/data/conversations.db-wal differ diff --git a/internal/agent/agent.go b/internal/agent/agent.go index a6451212..be3ff05a 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -276,11 +276,17 @@ func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, his // 获取可用工具 tools := a.getAvailableTools() - // 发送进度更新 + // 发送迭代开始事件 if i == 0 { - sendProgress("progress", "正在分析请求并制定测试策略...", nil) + sendProgress("iteration", "开始分析请求并制定测试策略", map[string]interface{}{ + "iteration": i + 1, + "total": maxIterations, + }) } else { - sendProgress("progress", fmt.Sprintf("正在继续分析(第 %d 轮迭代)...", i+1), nil) + sendProgress("iteration", fmt.Sprintf("第 %d 轮迭代", i+1), map[string]interface{}{ + "iteration": i + 1, + "total": maxIterations, + }) } // 记录每次调用OpenAI @@ -333,6 +339,13 @@ func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, his // 检查是否有工具调用 if len(choice.Message.ToolCalls) > 0 { + // 如果有思考内容,先发送思考事件 + if choice.Message.Content != "" { + sendProgress("thinking", choice.Message.Content, map[string]interface{}{ + "iteration": i + 1, + }) + } + // 添加assistant消息(包含工具调用) messages = append(messages, ChatMessage{ Role: "assistant", @@ -341,7 +354,10 @@ func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, his }) // 发送工具调用进度 - sendProgress("progress", fmt.Sprintf("检测到 %d 个工具调用,开始执行...", len(choice.Message.ToolCalls)), nil) + sendProgress("tool_calls_detected", fmt.Sprintf("检测到 %d 个工具调用", len(choice.Message.ToolCalls)), map[string]interface{}{ + "count": len(choice.Message.ToolCalls), + "iteration": i + 1, + }) // 执行所有工具调用 for idx, toolCall := range choice.Message.ToolCalls { @@ -350,8 +366,11 @@ func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, his sendProgress("tool_call", fmt.Sprintf("正在调用工具: %s", toolCall.Function.Name), map[string]interface{}{ "toolName": toolCall.Function.Name, "arguments": string(toolArgsJSON), + "argumentsObj": toolCall.Function.Arguments, + "toolCallId": toolCall.ID, "index": idx + 1, "total": len(choice.Message.ToolCalls), + "iteration": i + 1, }) // 执行工具 @@ -369,9 +388,12 @@ func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, his sendProgress("tool_result", fmt.Sprintf("工具 %s 执行失败", toolCall.Function.Name), map[string]interface{}{ "toolName": toolCall.Function.Name, "success": false, + "isError": true, "error": err.Error(), + "toolCallId": toolCall.ID, "index": idx + 1, "total": len(choice.Message.ToolCalls), + "iteration": i + 1, }) a.logger.Warn("工具执行失败,已返回详细错误信息", @@ -399,10 +421,13 @@ func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, his "toolName": toolCall.Function.Name, "success": !execResult.IsError, "isError": execResult.IsError, - "result": resultPreview, + "result": execResult.Result, // 完整结果 + "resultPreview": resultPreview, // 预览结果 "executionId": execResult.ExecutionID, + "toolCallId": toolCall.ID, "index": idx + 1, "total": len(choice.Message.ToolCalls), + "iteration": i + 1, }) // 如果工具返回了错误,记录日志但不中断流程 @@ -423,6 +448,13 @@ func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, his Content: choice.Message.Content, }) + // 发送AI思考内容(如果没有工具调用) + if choice.Message.Content != "" { + sendProgress("thinking", choice.Message.Content, map[string]interface{}{ + "iteration": i + 1, + }) + } + // 如果完成,返回结果 if choice.FinishReason == "stop" { sendProgress("progress", "正在生成最终回复...", nil) diff --git a/internal/database/conversation.go b/internal/database/conversation.go index 95905772..e21c6c27 100644 --- a/internal/database/conversation.go +++ b/internal/database/conversation.go @@ -21,12 +21,13 @@ type Conversation struct { // Message 消息 type Message struct { - ID string `json:"id"` - ConversationID string `json:"conversationId"` - Role string `json:"role"` - Content string `json:"content"` - MCPExecutionIDs []string `json:"mcpExecutionIds,omitempty"` - CreatedAt time.Time `json:"createdAt"` + ID string `json:"id"` + ConversationID string `json:"conversationId"` + Role string `json:"role"` + Content string `json:"content"` + MCPExecutionIDs []string `json:"mcpExecutionIds,omitempty"` + ProcessDetails []map[string]interface{} `json:"processDetails,omitempty"` + CreatedAt time.Time `json:"createdAt"` } // CreateConversation 创建新对话 @@ -91,6 +92,39 @@ func (db *DB) GetConversation(id string) (*Conversation, error) { } conv.Messages = messages + // 加载过程详情(按消息ID分组) + processDetailsMap, err := db.GetProcessDetailsByConversation(id) + if err != nil { + db.logger.Warn("加载过程详情失败", zap.Error(err)) + processDetailsMap = make(map[string][]ProcessDetail) + } + + // 将过程详情附加到对应的消息上 + for i := range conv.Messages { + if details, ok := processDetailsMap[conv.Messages[i].ID]; ok { + // 将ProcessDetail转换为JSON格式,以便前端使用 + detailsJSON := make([]map[string]interface{}, len(details)) + for j, detail := range details { + var data interface{} + if detail.Data != "" { + if err := json.Unmarshal([]byte(detail.Data), &data); err != nil { + db.logger.Warn("解析过程详情数据失败", zap.Error(err)) + } + } + detailsJSON[j] = map[string]interface{}{ + "id": detail.ID, + "messageId": detail.MessageID, + "conversationId": detail.ConversationID, + "eventType": detail.EventType, + "message": detail.Message, + "data": data, + "createdAt": detail.CreatedAt, + } + } + conv.Messages[i].ProcessDetails = detailsJSON + } + } + return &conv, nil } @@ -254,3 +288,111 @@ func (db *DB) GetMessages(conversationID string) ([]Message, error) { return messages, nil } +// ProcessDetail 过程详情事件 +type ProcessDetail struct { + ID string `json:"id"` + MessageID string `json:"messageId"` + ConversationID string `json:"conversationId"` + EventType string `json:"eventType"` // iteration, thinking, tool_calls_detected, tool_call, tool_result, progress, error + Message string `json:"message"` + Data string `json:"data"` // JSON格式的数据 + CreatedAt time.Time `json:"createdAt"` +} + +// AddProcessDetail 添加过程详情事件 +func (db *DB) AddProcessDetail(messageID, conversationID, eventType, message string, data interface{}) error { + id := uuid.New().String() + + var dataJSON string + if data != nil { + jsonData, err := json.Marshal(data) + if err != nil { + db.logger.Warn("序列化过程详情数据失败", zap.Error(err)) + } else { + dataJSON = string(jsonData) + } + } + + _, err := db.Exec( + "INSERT INTO process_details (id, message_id, conversation_id, event_type, message, data, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", + id, messageID, conversationID, eventType, message, dataJSON, time.Now(), + ) + if err != nil { + return fmt.Errorf("添加过程详情失败: %w", err) + } + + return nil +} + +// GetProcessDetails 获取消息的过程详情 +func (db *DB) GetProcessDetails(messageID string) ([]ProcessDetail, error) { + rows, err := db.Query( + "SELECT id, message_id, conversation_id, event_type, message, data, created_at FROM process_details WHERE message_id = ? ORDER BY created_at ASC", + messageID, + ) + if err != nil { + return nil, fmt.Errorf("查询过程详情失败: %w", err) + } + defer rows.Close() + + var details []ProcessDetail + for rows.Next() { + var detail ProcessDetail + var createdAt string + + if err := rows.Scan(&detail.ID, &detail.MessageID, &detail.ConversationID, &detail.EventType, &detail.Message, &detail.Data, &createdAt); err != nil { + return nil, fmt.Errorf("扫描过程详情失败: %w", err) + } + + // 尝试多种时间格式解析 + var err error + detail.CreatedAt, err = time.Parse("2006-01-02 15:04:05.999999999-07:00", createdAt) + if err != nil { + detail.CreatedAt, err = time.Parse("2006-01-02 15:04:05", createdAt) + } + if err != nil { + detail.CreatedAt, err = time.Parse(time.RFC3339, createdAt) + } + + details = append(details, detail) + } + + return details, nil +} + +// GetProcessDetailsByConversation 获取对话的所有过程详情(按消息分组) +func (db *DB) GetProcessDetailsByConversation(conversationID string) (map[string][]ProcessDetail, error) { + rows, err := db.Query( + "SELECT id, message_id, conversation_id, event_type, message, data, created_at FROM process_details WHERE conversation_id = ? ORDER BY created_at ASC", + conversationID, + ) + if err != nil { + return nil, fmt.Errorf("查询过程详情失败: %w", err) + } + defer rows.Close() + + detailsMap := make(map[string][]ProcessDetail) + for rows.Next() { + var detail ProcessDetail + var createdAt string + + if err := rows.Scan(&detail.ID, &detail.MessageID, &detail.ConversationID, &detail.EventType, &detail.Message, &detail.Data, &createdAt); err != nil { + return nil, fmt.Errorf("扫描过程详情失败: %w", err) + } + + // 尝试多种时间格式解析 + var err error + detail.CreatedAt, err = time.Parse("2006-01-02 15:04:05.999999999-07:00", createdAt) + if err != nil { + detail.CreatedAt, err = time.Parse("2006-01-02 15:04:05", createdAt) + } + if err != nil { + detail.CreatedAt, err = time.Parse(time.RFC3339, createdAt) + } + + detailsMap[detail.MessageID] = append(detailsMap[detail.MessageID], detail) + } + + return detailsMap, nil +} + diff --git a/internal/database/database.go b/internal/database/database.go index 5091e5ef..0cf0e7e7 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -61,10 +61,26 @@ func (db *DB) initTables() error { FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE );` + // 创建过程详情表 + createProcessDetailsTable := ` + CREATE TABLE IF NOT EXISTS process_details ( + id TEXT PRIMARY KEY, + message_id TEXT NOT NULL, + conversation_id TEXT NOT NULL, + event_type TEXT NOT NULL, + message TEXT, + data TEXT, + created_at DATETIME NOT NULL, + FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE, + FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE + );` + // 创建索引 createIndexes := ` CREATE INDEX IF NOT EXISTS idx_messages_conversation_id ON messages(conversation_id); CREATE INDEX IF NOT EXISTS idx_conversations_updated_at ON conversations(updated_at); + CREATE INDEX IF NOT EXISTS idx_process_details_message_id ON process_details(message_id); + CREATE INDEX IF NOT EXISTS idx_process_details_conversation_id ON process_details(conversation_id); ` if _, err := db.Exec(createConversationsTable); err != nil { @@ -75,6 +91,10 @@ func (db *DB) initTables() error { return fmt.Errorf("创建messages表失败: %w", err) } + if _, err := db.Exec(createProcessDetailsTable); err != nil { + return fmt.Errorf("创建process_details表失败: %w", err) + } + if _, err := db.Exec(createIndexes); err != nil { return fmt.Errorf("创建索引失败: %w", err) } diff --git a/internal/handler/agent.go b/internal/handler/agent.go index 50974318..9d33d86d 100644 --- a/internal/handler/agent.go +++ b/internal/handler/agent.go @@ -220,9 +220,29 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) { h.logger.Error("保存用户消息失败", zap.Error(err)) } - // 创建进度回调函数 + // 预先创建助手消息,以便关联过程详情 + assistantMsg, err := h.db.AddMessage(conversationID, "assistant", "处理中...", nil) + if err != nil { + h.logger.Error("创建助手消息失败", zap.Error(err)) + // 如果创建失败,继续执行但不保存过程详情 + assistantMsg = nil + } + + // 创建进度回调函数,同时保存到数据库 + var assistantMessageID string + if assistantMsg != nil { + assistantMessageID = assistantMsg.ID + } + progressCallback := func(eventType, message string, data interface{}) { sendEvent(eventType, message, data) + + // 保存过程详情到数据库(排除response和done事件,它们会在后面单独处理) + if assistantMessageID != "" && eventType != "response" && eventType != "done" { + if err := h.db.AddProcessDetail(assistantMessageID, conversationID, eventType, message, data); err != nil { + h.logger.Warn("保存过程详情失败", zap.Error(err), zap.String("eventType", eventType)) + } + } } // 执行Agent Loop,传入进度回调 @@ -231,20 +251,44 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) { if err != nil { h.logger.Error("Agent Loop执行失败", zap.Error(err)) sendEvent("error", "执行失败: "+err.Error(), nil) + // 保存错误事件 + if assistantMessageID != "" { + h.db.AddProcessDetail(assistantMessageID, conversationID, "error", "执行失败: "+err.Error(), nil) + } sendEvent("done", "", nil) return } - // 保存助手回复 - _, err = h.db.AddMessage(conversationID, "assistant", result.Response, result.MCPExecutionIDs) - if err != nil { - h.logger.Error("保存助手消息失败", zap.Error(err)) + // 更新助手消息内容 + if assistantMsg != nil { + _, err = h.db.Exec( + "UPDATE messages SET content = ?, mcp_execution_ids = ? WHERE id = ?", + result.Response, + func() string { + if len(result.MCPExecutionIDs) > 0 { + jsonData, _ := json.Marshal(result.MCPExecutionIDs) + return string(jsonData) + } + return "" + }(), + assistantMessageID, + ) + if err != nil { + h.logger.Error("更新助手消息失败", zap.Error(err)) + } + } else { + // 如果之前创建失败,现在创建 + _, err = h.db.AddMessage(conversationID, "assistant", result.Response, result.MCPExecutionIDs) + if err != nil { + h.logger.Error("保存助手消息失败", zap.Error(err)) + } } // 发送最终响应 sendEvent("response", result.Response, map[string]interface{}{ "mcpExecutionIds": result.MCPExecutionIDs, "conversationId": conversationID, + "messageId": assistantMessageID, // 包含消息ID,以便前端关联过程详情 }) sendEvent("done", "", map[string]interface{}{ "conversationId": conversationID, diff --git a/web/static/css/style.css b/web/static/css/style.css index f462ea5e..aa543643 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -167,17 +167,32 @@ header { cursor: pointer; transition: all 0.2s; border: 1px solid transparent; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + position: relative; } .conversation-item:hover { background: var(--bg-tertiary); } +.conversation-item:hover .conversation-delete-btn { + opacity: 1; +} + .conversation-item.active { background: var(--bg-primary); border-color: var(--accent-color); } +.conversation-content { + flex: 1; + min-width: 0; + overflow: hidden; +} + .conversation-title { font-size: 0.875rem; font-weight: 500; @@ -193,6 +208,53 @@ header { color: var(--text-muted); } +.conversation-delete-btn { + width: 28px; + height: 28px; + border: none; + background: transparent; + color: var(--text-muted); + cursor: pointer; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + opacity: 0; + transition: all 0.2s ease; + flex-shrink: 0; + position: relative; +} + +.conversation-delete-btn svg { + width: 14px; + height: 14px; + transition: all 0.2s ease; +} + +.conversation-delete-btn:hover { + background: rgba(220, 53, 69, 0.1); + color: var(--error-color); + opacity: 1; +} + +.conversation-delete-btn:hover svg { + transform: scale(1.1); +} + +.conversation-delete-btn:active { + background: rgba(220, 53, 69, 0.2); + transform: scale(0.95); +} + +.conversation-item.active .conversation-delete-btn { + opacity: 0.6; +} + +.conversation-item.active:hover .conversation-delete-btn { + opacity: 1; +} + /* 对话界面样式 */ .chat-container { display: flex; @@ -503,6 +565,40 @@ header { gap: 6px; } +.process-detail-btn { + background: rgba(156, 39, 176, 0.1) !important; + border-color: rgba(156, 39, 176, 0.3) !important; + color: #9c27b0 !important; +} + +.process-detail-btn:hover { + background: rgba(156, 39, 176, 0.2) !important; + border-color: #9c27b0 !important; + color: #7b1fa2 !important; +} + +.process-details-container { + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid var(--border-color); + width: 100%; +} + +.process-details-content { + width: 100%; +} + +.process-details-content .progress-timeline { + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease; +} + +.process-details-content .progress-timeline.expanded { + max-height: 2000px; + overflow-y: auto; +} + .chat-input-container { display: flex; gap: 12px; @@ -813,3 +909,229 @@ header { margin: 10% auto; } } + +/* 进度展示样式 */ +.progress-container { + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 16px; + max-width: 100%; +} + +.progress-container.completed { + background: var(--bg-secondary); + border-color: var(--border-color); + opacity: 0.95; +} + +.progress-details { + margin-top: 8px; + margin-bottom: 24px; +} + +.progress-timeline-empty { + padding: 12px; + text-align: center; + color: var(--text-muted); + font-size: 0.875rem; +} + +.progress-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + padding-bottom: 12px; + border-bottom: 1px solid var(--border-color); +} + +.progress-title { + font-weight: 600; + color: var(--text-primary); + font-size: 0.9375rem; +} + +.progress-toggle { + padding: 4px 12px; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 4px; + font-size: 0.8125rem; + color: var(--text-secondary); + cursor: pointer; + transition: all 0.2s; +} + +.progress-toggle:hover { + background: var(--bg-secondary); + color: var(--text-primary); +} + +.progress-timeline { + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease; +} + +.progress-timeline.expanded { + max-height: 2000px; + overflow-y: auto; +} + +.timeline-item { + padding: 12px; + margin-bottom: 8px; + border-left: 3px solid var(--border-color); + padding-left: 16px; + background: var(--bg-secondary); + border-radius: 4px; + transition: all 0.2s; +} + +.timeline-item:hover { + background: var(--bg-tertiary); +} + +.timeline-item-iteration { + border-left-color: var(--accent-color); + background: rgba(0, 102, 255, 0.05); +} + +.timeline-item-thinking { + border-left-color: #9c27b0; + background: rgba(156, 39, 176, 0.05); +} + +.timeline-item-tool_call { + border-left-color: #ff9800; + background: rgba(255, 152, 0, 0.05); +} + +.timeline-item-tool_result { + border-left-color: var(--success-color); + background: rgba(40, 167, 69, 0.05); +} + +.timeline-item-tool_result.error { + border-left-color: var(--error-color); + background: rgba(220, 53, 69, 0.05); +} + +.timeline-item-error { + border-left-color: var(--error-color); + background: rgba(220, 53, 69, 0.1); +} + +.timeline-item-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 8px; +} + +.timeline-item-time { + font-size: 0.75rem; + color: var(--text-muted); + font-family: monospace; + min-width: 70px; +} + +.timeline-item-title { + font-weight: 500; + color: var(--text-primary); + font-size: 0.875rem; + flex: 1; +} + +.timeline-item-content { + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid var(--border-color); + font-size: 0.875rem; + color: var(--text-secondary); + line-height: 1.6; +} + +.tool-details { + display: flex; + flex-direction: column; + gap: 12px; +} + +.tool-arg-section { + display: flex; + flex-direction: column; + gap: 8px; +} + +.tool-arg-section strong { + color: var(--text-primary); + font-size: 0.8125rem; +} + +.tool-args { + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 4px; + padding: 12px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 0.8125rem; + line-height: 1.5; + overflow-x: auto; + margin: 0; + color: var(--text-primary); +} + +.tool-result-section { + display: flex; + flex-direction: column; + gap: 8px; +} + +.tool-result-section strong { + color: var(--text-primary); + font-size: 0.8125rem; +} + +.tool-result { + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 4px; + padding: 12px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 0.8125rem; + line-height: 1.5; + overflow-x: auto; + max-height: 400px; + overflow-y: auto; + margin: 0; + white-space: pre-wrap; + word-wrap: break-word; + color: var(--text-primary); +} + +.tool-result-section.error .tool-result { + background: rgba(220, 53, 69, 0.1); + border-color: var(--error-color); + color: var(--error-color); +} + +.tool-result-section.success .tool-result { + background: rgba(40, 167, 69, 0.1); + border-color: var(--success-color); +} + +.tool-execution-id { + margin-top: 8px; + font-size: 0.75rem; + color: var(--text-muted); +} + +.tool-execution-id code { + background: var(--bg-tertiary); + padding: 2px 6px; + border-radius: 3px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + color: var(--text-secondary); +} diff --git a/web/static/js/app.js b/web/static/js/app.js index 50bbc9ba..6b0b61fb 100644 --- a/web/static/js/app.js +++ b/web/static/js/app.js @@ -15,10 +15,9 @@ async function sendMessage() { addMessage('user', message); input.value = ''; - // 创建进度消息容器 - const progressId = addMessage('system', '正在处理中...'); + // 创建进度消息容器(使用详细的进度展示) + const progressId = addProgressMessage(); const progressElement = document.getElementById(progressId); - const progressBubble = progressElement.querySelector('.message-bubble'); let assistantMessageId = null; let mcpExecutionIds = []; @@ -54,7 +53,7 @@ async function sendMessage() { if (line.startsWith('data: ')) { try { const eventData = JSON.parse(line.slice(6)); - handleStreamEvent(eventData, progressElement, progressBubble, progressId, + handleStreamEvent(eventData, progressElement, progressId, () => assistantMessageId, (id) => { assistantMessageId = id; }, () => mcpExecutionIds, (ids) => { mcpExecutionIds = ids; }); } catch (e) { @@ -71,7 +70,7 @@ async function sendMessage() { if (line.startsWith('data: ')) { try { const eventData = JSON.parse(line.slice(6)); - handleStreamEvent(eventData, progressElement, progressBubble, progressId, + handleStreamEvent(eventData, progressElement, progressId, () => assistantMessageId, (id) => { assistantMessageId = id; }, () => mcpExecutionIds, (ids) => { mcpExecutionIds = ids; }); } catch (e) { @@ -87,13 +86,243 @@ async function sendMessage() { } } +// 创建进度消息容器 +function addProgressMessage() { + const messagesDiv = document.getElementById('chat-messages'); + const messageDiv = document.createElement('div'); + messageCounter++; + const id = 'progress-' + Date.now() + '-' + messageCounter; + messageDiv.id = id; + messageDiv.className = 'message system progress-message'; + + const contentWrapper = document.createElement('div'); + contentWrapper.className = 'message-content'; + + const bubble = document.createElement('div'); + bubble.className = 'message-bubble progress-container'; + bubble.innerHTML = ` +
${JSON.stringify(args, null, 2)}
+ ${escapeHtml(result)}
+ ${data.executionId ? `${data.executionId}