fix: Phase 0 阻碍项修复 — 流式事件错误处理 + CI 排除 + UI 中文化
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

BLK-2: loop_runner.rs 22 处 let _ = tx.send() 全部替换为
if let Err(e) { tracing::warn!(...) },修复流式事件静默丢失问题

BLK-5: 50+ 英文字符串翻译为中文
- HandApprovalModal.tsx (~40处): 风险标签/按钮/状态/表单标签
- ChatArea.tsx: Thinking.../Sending...
- AuditLogsPanel.tsx: 空状态文案
- HandParamsForm.tsx: 空列表提示
- CreateTriggerModal.tsx: 成功提示
- MessageSearch.tsx: 时间筛选/搜索历史

BLK-6: CI/Release workflow 添加 --exclude zclaw-saas
- ci.yml: clippy/test/build 三个步骤
- release.yml: test 步骤

验证: cargo check ✓ | tsc --noEmit ✓
This commit is contained in:
iven
2026-04-17 18:12:42 +08:00
parent 93df380ca8
commit 2cae822775
9 changed files with 126 additions and 78 deletions

View File

@@ -50,7 +50,7 @@ jobs:
- name: Rust Clippy
working-directory: .
run: cargo clippy --workspace -- -D warnings
run: cargo clippy --workspace --exclude zclaw-saas -- -D warnings
- name: Install frontend dependencies
working-directory: desktop
@@ -94,7 +94,7 @@ jobs:
- name: Run Rust tests
working-directory: .
run: cargo test --workspace
run: cargo test --workspace --exclude zclaw-saas
- name: Install frontend dependencies
working-directory: desktop
@@ -138,7 +138,7 @@ jobs:
- name: Rust release build
working-directory: .
run: cargo build --release --workspace
run: cargo build --release --workspace --exclude zclaw-saas
- name: Install frontend dependencies
working-directory: desktop

View File

@@ -45,7 +45,7 @@ jobs:
- name: Run Rust tests
working-directory: .
run: cargo test --workspace
run: cargo test --workspace --exclude zclaw-saas
- name: Install frontend dependencies
working-directory: desktop

View File

@@ -652,12 +652,14 @@ impl AgentLoop {
enhanced_prompt = mw_ctx.system_prompt;
}
middleware::MiddlewareDecision::Stop(reason) => {
let _ = tx.send(LoopEvent::Complete(AgentLoopResult {
if let Err(e) = tx.send(LoopEvent::Complete(AgentLoopResult {
response: reason,
input_tokens: 0,
output_tokens: 0,
iterations: 1,
})).await;
})).await {
tracing::warn!("[AgentLoop] Failed to send Complete event: {}", e);
}
return Ok(rx);
}
}
@@ -691,15 +693,19 @@ impl AgentLoop {
'outer: loop {
iteration += 1;
if iteration > max_iterations {
let _ = tx.send(LoopEvent::Error("达到最大迭代次数".to_string())).await;
if let Err(e) = tx.send(LoopEvent::Error("达到最大迭代次数".to_string())).await {
tracing::warn!("[AgentLoop] Failed to send Error event: {}", e);
}
break;
}
// Notify iteration start
let _ = tx.send(LoopEvent::IterationStart {
if let Err(e) = tx.send(LoopEvent::IterationStart {
iteration,
max_iterations,
}).await;
}).await {
tracing::warn!("[AgentLoop] Failed to send IterationStart event: {}", e);
}
// Build completion request
let request = CompletionRequest {
@@ -742,13 +748,17 @@ impl AgentLoop {
text_delta_count += 1;
tracing::debug!("[AgentLoop] TextDelta #{}: {} chars", text_delta_count, delta.len());
iteration_text.push_str(delta);
let _ = tx.send(LoopEvent::Delta(delta.clone())).await;
if let Err(e) = tx.send(LoopEvent::Delta(delta.clone())).await {
tracing::warn!("[AgentLoop] Failed to send Delta event: {}", e);
}
}
StreamChunk::ThinkingDelta { delta } => {
thinking_delta_count += 1;
tracing::debug!("[AgentLoop] ThinkingDelta #{}: {} chars", thinking_delta_count, delta.len());
reasoning_text.push_str(delta);
let _ = tx.send(LoopEvent::ThinkingDelta(delta.clone())).await;
if let Err(e) = tx.send(LoopEvent::ThinkingDelta(delta.clone())).await {
tracing::warn!("[AgentLoop] Failed to send ThinkingDelta event: {}", e);
}
}
StreamChunk::ToolUseStart { id, name } => {
tracing::debug!("[AgentLoop] ToolUseStart: id={}, name={}", id, name);
@@ -770,7 +780,9 @@ impl AgentLoop {
// Update with final parsed input and emit ToolStart event
if let Some(tool) = pending_tool_calls.iter_mut().find(|(tid, _, _)| tid == id) {
tool.2 = input.clone();
let _ = tx.send(LoopEvent::ToolStart { name: tool.1.clone(), input: input.clone() }).await;
if let Err(e) = tx.send(LoopEvent::ToolStart { name: tool.1.clone(), input: input.clone() }).await {
tracing::warn!("[AgentLoop] Failed to send ToolStart event: {}", e);
}
}
}
StreamChunk::Complete { input_tokens: it, output_tokens: ot, .. } => {
@@ -787,20 +799,26 @@ impl AgentLoop {
}
StreamChunk::Error { message } => {
tracing::error!("[AgentLoop] Stream error: {}", message);
let _ = tx.send(LoopEvent::Error(message.clone())).await;
if let Err(e) = tx.send(LoopEvent::Error(message.clone())).await {
tracing::warn!("[AgentLoop] Failed to send Error event: {}", e);
}
stream_errored = true;
}
}
}
Ok(Some(Err(e))) => {
tracing::error!("[AgentLoop] Chunk error: {}", e);
let _ = tx.send(LoopEvent::Error(format!("LLM 响应错误: {}", e.to_string()))).await;
if let Err(e) = tx.send(LoopEvent::Error(format!("LLM 响应错误: {}", e.to_string()))).await {
tracing::warn!("[AgentLoop] Failed to send Error event: {}", e);
}
stream_errored = true;
}
Ok(None) => break, // Stream ended normally
Err(_) => {
tracing::error!("[AgentLoop] Stream chunk timeout ({}s)", chunk_timeout.as_secs());
let _ = tx.send(LoopEvent::Error("LLM 响应超时,请重试".to_string())).await;
if let Err(e) = tx.send(LoopEvent::Error("LLM 响应超时,请重试".to_string())).await {
tracing::warn!("[AgentLoop] Failed to send Error event: {}", e);
}
stream_errored = true;
}
}
@@ -820,7 +838,9 @@ impl AgentLoop {
if iteration_text.is_empty() && !reasoning_text.is_empty() {
tracing::info!("[AgentLoop] Model generated {} chars of reasoning but no text — using reasoning as response",
reasoning_text.len());
let _ = tx.send(LoopEvent::Delta(reasoning_text.clone())).await;
if let Err(e) = tx.send(LoopEvent::Delta(reasoning_text.clone())).await {
tracing::warn!("[AgentLoop] Failed to send Delta event: {}", e);
}
iteration_text = reasoning_text.clone();
} else if iteration_text.is_empty() {
tracing::warn!("[AgentLoop] No text content after {} chunks (thinking_delta={})",
@@ -838,12 +858,14 @@ impl AgentLoop {
tracing::warn!("[AgentLoop] Failed to save final assistant message: {}", e);
}
let _ = tx.send(LoopEvent::Complete(AgentLoopResult {
if let Err(e) = tx.send(LoopEvent::Complete(AgentLoopResult {
response: iteration_text.clone(),
input_tokens: total_input_tokens,
output_tokens: total_output_tokens,
iterations: iteration,
})).await;
})).await {
tracing::warn!("[AgentLoop] Failed to send Complete event: {}", e);
}
// Post-completion: middleware after_completion (memory extraction, etc.)
if let Some(ref chain) = middleware_chain {
@@ -906,13 +928,17 @@ impl AgentLoop {
Ok(middleware::ToolCallDecision::Block(msg)) => {
tracing::warn!("[AgentLoop] Tool '{}' blocked by middleware: {}", name, msg);
let error_output = serde_json::json!({ "error": msg });
let _ = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output.clone() }).await;
if let Err(e) = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output.clone() }).await {
tracing::warn!("[AgentLoop] Failed to send ToolEnd event: {}", e);
}
messages.push(Message::tool_result(id, zclaw_types::ToolId::new(&name), error_output, true));
continue;
}
Ok(middleware::ToolCallDecision::AbortLoop(reason)) => {
tracing::warn!("[AgentLoop] Loop aborted by middleware: {}", reason);
let _ = tx.send(LoopEvent::Error(reason)).await;
if let Err(e) = tx.send(LoopEvent::Error(reason)).await {
tracing::warn!("[AgentLoop] Failed to send Error event: {}", e);
}
break 'outer;
}
Ok(middleware::ToolCallDecision::ReplaceInput(new_input)) => {
@@ -936,18 +962,24 @@ impl AgentLoop {
let (result, is_error) = if let Some(tool) = tools.get(&name) {
match tool.execute(new_input, &tool_context).await {
Ok(output) => {
let _ = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: output.clone() }).await;
if let Err(e) = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: output.clone() }).await {
tracing::warn!("[AgentLoop] Failed to send ToolEnd event: {}", e);
}
(output, false)
}
Err(e) => {
let error_output = serde_json::json!({ "error": e.to_string() });
let _ = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output.clone() }).await;
if let Err(e) = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output.clone() }).await {
tracing::warn!("[AgentLoop] Failed to send ToolEnd event: {}", e);
}
(error_output, true)
}
}
} else {
let error_output = serde_json::json!({ "error": format!("Unknown tool: {}", name) });
let _ = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output.clone() }).await;
if let Err(e) = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output.clone() }).await {
tracing::warn!("[AgentLoop] Failed to send ToolEnd event: {}", e);
}
(error_output, true)
};
messages.push(Message::tool_result(id, zclaw_types::ToolId::new(&name), result, is_error));
@@ -956,7 +988,9 @@ impl AgentLoop {
Err(e) => {
tracing::error!("[AgentLoop] Middleware error for tool '{}': {}", name, e);
let error_output = serde_json::json!({ "error": e.to_string() });
let _ = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output.clone() }).await;
if let Err(e) = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output.clone() }).await {
tracing::warn!("[AgentLoop] Failed to send ToolEnd event: {}", e);
}
messages.push(Message::tool_result(id, zclaw_types::ToolId::new(&name), error_output, true));
continue;
}
@@ -966,13 +1000,17 @@ impl AgentLoop {
let guard_result = loop_guard_clone.lock().unwrap_or_else(|e| e.into_inner()).check(&name, &input);
match guard_result {
LoopGuardResult::CircuitBreaker => {
let _ = tx.send(LoopEvent::Error("检测到工具调用循环,已自动终止".to_string())).await;
if let Err(e) = tx.send(LoopEvent::Error("检测到工具调用循环,已自动终止".to_string())).await {
tracing::warn!("[AgentLoop] Failed to send Error event: {}", e);
}
break 'outer;
}
LoopGuardResult::Blocked => {
tracing::warn!("[AgentLoop] Tool '{}' blocked by loop guard", name);
let error_output = serde_json::json!({ "error": "工具调用被循环防护拦截" });
let _ = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output.clone() }).await;
if let Err(e) = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output.clone() }).await {
tracing::warn!("[AgentLoop] Failed to send ToolEnd event: {}", e);
}
messages.push(Message::tool_result(id, zclaw_types::ToolId::new(&name), error_output, true));
continue;
}
@@ -1005,20 +1043,26 @@ impl AgentLoop {
match tool.execute(input.clone(), &tool_context).await {
Ok(output) => {
tracing::debug!("[AgentLoop] Tool '{}' executed successfully: {:?}", name, output);
let _ = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: output.clone() }).await;
if let Err(e) = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: output.clone() }).await {
tracing::warn!("[AgentLoop] Failed to send ToolEnd event: {}", e);
}
(output, false)
}
Err(e) => {
tracing::error!("[AgentLoop] Tool '{}' execution failed: {}", name, e);
let error_output = serde_json::json!({ "error": e.to_string() });
let _ = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output.clone() }).await;
if let Err(e) = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output.clone() }).await {
tracing::warn!("[AgentLoop] Failed to send ToolEnd event: {}", e);
}
(error_output, true)
}
}
} else {
tracing::error!("[AgentLoop] Tool '{}' not found in registry", name);
let error_output = serde_json::json!({ "error": format!("Unknown tool: {}", name) });
let _ = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output.clone() }).await;
if let Err(e) = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output.clone() }).await {
tracing::warn!("[AgentLoop] Failed to send ToolEnd event: {}", e);
}
(error_output, true)
};
@@ -1038,13 +1082,17 @@ impl AgentLoop {
is_error,
));
// Send the question as final delta so the user sees it
let _ = tx.send(LoopEvent::Delta(question.clone())).await;
let _ = tx.send(LoopEvent::Complete(AgentLoopResult {
if let Err(e) = tx.send(LoopEvent::Delta(question.clone())).await {
tracing::warn!("[AgentLoop] Failed to send Delta event: {}", e);
}
if let Err(e) = tx.send(LoopEvent::Complete(AgentLoopResult {
response: question.clone(),
input_tokens: total_input_tokens,
output_tokens: total_output_tokens,
iterations: iteration,
})).await;
})).await {
tracing::warn!("[AgentLoop] Failed to send Complete event: {}", e);
}
if let Err(e) = memory.append_message(&session_id_clone, &Message::assistant(&question)).await {
tracing::warn!("[AgentLoop] Failed to save clarification message: {}", e);
}

View File

@@ -862,7 +862,7 @@ export function AuditLogsPanel() {
{filteredLogs.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-gray-500 dark:text-gray-400">
<AlertCircle className="w-8 h-8 mb-2" />
<p>No audit logs found</p>
<p></p>
{(searchTerm || Object.keys(filter).length > 0) && (
<button
onClick={handleResetFilters}

View File

@@ -683,14 +683,14 @@ function MessageBubble({ message, onRetry }: { message: Message; setInput?: (tex
// Thinking indicator
<div className="flex items-center gap-2 px-4 py-3 text-gray-500 dark:text-gray-400">
<LoadingDots />
<span className="text-sm">Thinking...</span>
<span className="text-sm">...</span>
</div>
) : (
<div className={`p-4 shadow-sm ${isUser ? 'chat-bubble-user shadow-md' : 'chat-bubble-assistant'} relative group`}>
{/* Optimistic sending indicator */}
{isUser && message.optimistic && (
<span className="text-xs text-blue-200 dark:text-blue-300 mb-1 block animate-pulse">
Sending...
...
</span>
)}
{/* Reasoning block for thinking content (DeerFlow-inspired) */}

View File

@@ -543,7 +543,7 @@ export function CreateTriggerModal({ isOpen, onClose, onSuccess }: CreateTrigger
{submitStatus === 'success' && (
<div className="flex items-center gap-2 p-3 bg-green-50 dark:bg-green-900/20 rounded-lg text-green-700 dark:text-green-400">
<CheckCircle className="w-5 h-5 flex-shrink-0" />
<span className="text-sm">Trigger created successfully!</span>
<span className="text-sm"></span>
</div>
)}
{submitStatus === 'error' && (

View File

@@ -57,21 +57,21 @@ const RISK_CONFIG: Record<
{ label: string; color: string; bgColor: string; borderColor: string; icon: typeof AlertTriangle }
> = {
low: {
label: 'Low Risk',
label: '低风险',
color: 'text-green-600 dark:text-green-400',
bgColor: 'bg-green-100 dark:bg-green-900/30',
borderColor: 'border-green-300 dark:border-green-700',
icon: CheckCircle,
},
medium: {
label: 'Medium Risk',
label: '中风险',
color: 'text-yellow-600 dark:text-yellow-400',
bgColor: 'bg-yellow-100 dark:bg-yellow-900/30',
borderColor: 'border-yellow-300 dark:border-yellow-700',
icon: AlertTriangle,
},
high: {
label: 'High Risk',
label: '高风险',
color: 'text-red-600 dark:text-red-400',
bgColor: 'bg-red-100 dark:bg-red-900/30',
borderColor: 'border-red-300 dark:border-red-700',
@@ -135,32 +135,32 @@ function calculateRiskLevel(handId: HandId, params: Record<string, unknown>): Ri
function getExpectedImpact(handId: HandId, params: Record<string, unknown>): string {
switch (handId) {
case 'browser':
return `Will perform browser automation on ${params.url || 'specified URL'}`;
return `将在 ${params.url || '指定网址'} 执行浏览器自动化`;
case 'twitter':
if (params.action === 'post') {
return 'Will post content to Twitter/X publicly';
return '将公开发布内容到 Twitter/X';
}
if (params.action === 'engage') {
return 'Will like/reply to tweets';
return '将点赞/回复推文';
}
return 'Will perform Twitter/X operations';
return '将执行 Twitter/X 操作';
case 'collector':
return `Will collect data from ${params.targetUrl || 'specified source'}`;
return `将从 ${params.targetUrl || '指定来源'} 收集数据`;
case 'lead':
return `Will search for leads from ${params.source || 'specified source'}`;
return `将从 ${params.source || '指定来源'} 搜索线索`;
case 'clip':
return `Will process video: ${params.inputPath || 'specified input'}`;
return `将处理视频: ${params.inputPath || '指定输入'}`;
case 'predictor':
return `Will run prediction on ${params.dataSource || 'specified data'}`;
return `将对 ${params.dataSource || '指定数据'} 运行预测`;
case 'researcher':
return `Will conduct research on: ${params.topic || 'specified topic'}`;
return `将研究: ${params.topic || '指定主题'}`;
default:
return 'Will execute Hand operation';
return '将执行 Hand 操作';
}
}
function formatTimeRemaining(seconds: number): string {
if (seconds <= 0) return 'Expired';
if (seconds <= 0) return '已过期';
if (seconds < 60) return `${seconds}s`;
const minutes = Math.floor(seconds / 60);
const secs = seconds % 60;
@@ -218,7 +218,7 @@ function TimeoutProgress({ timeRemaining, totalSeconds }: { timeRemaining: numbe
<div className="flex items-center justify-between text-xs">
<span className="text-gray-500 dark:text-gray-400 flex items-center gap-1">
<Clock className="w-3 h-3" />
Time Remaining
</span>
<span
className={`font-medium ${isUrgent ? 'text-red-600 dark:text-red-400' : 'text-gray-700 dark:text-gray-300'}`}
@@ -241,7 +241,7 @@ function TimeoutProgress({ timeRemaining, totalSeconds }: { timeRemaining: numbe
function ParamsDisplay({ params }: { params: Record<string, unknown> }) {
if (!params || Object.keys(params).length === 0) {
return (
<p className="text-sm text-gray-500 dark:text-gray-400 italic">No parameters provided</p>
<p className="text-sm text-gray-500 dark:text-gray-400 italic"></p>
);
}
@@ -282,7 +282,7 @@ export function HandApprovalModal({
runId: handRun.runId,
handId,
handName: handDef?.name || handId,
description: handDef?.description || 'Hand execution request',
description: handDef?.description || 'Hand 执行请求',
params,
riskLevel: calculateRiskLevel(handId, params),
expectedImpact: getExpectedImpact(handId, params),
@@ -329,7 +329,7 @@ export function HandApprovalModal({
await onApprove(approvalData.runId);
onClose();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to approve');
setError(err instanceof Error ? err.message : '批准失败');
} finally {
setIsProcessing(false);
}
@@ -344,7 +344,7 @@ export function HandApprovalModal({
}
if (!rejectReason.trim()) {
setError('Please provide a reason for rejection');
setError('请提供拒绝原因');
return;
}
@@ -355,7 +355,7 @@ export function HandApprovalModal({
await onReject(approvalData.runId, rejectReason.trim());
onClose();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to reject');
setError(err instanceof Error ? err.message : '拒绝失败');
} finally {
setIsProcessing(false);
}
@@ -387,10 +387,10 @@ export function HandApprovalModal({
</div>
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
Hand Approval Request
Hand
</h2>
<p className="text-xs text-gray-500 dark:text-gray-400">
Review and approve Hand execution
Hand
</p>
</div>
</div>
@@ -408,7 +408,7 @@ export function HandApprovalModal({
{isExpired && (
<div className="flex items-center gap-2 p-3 bg-gray-100 dark:bg-gray-900 rounded-lg text-gray-600 dark:text-gray-400">
<Clock className="w-5 h-5 flex-shrink-0" />
<span className="text-sm">This approval request has expired</span>
<span className="text-sm"></span>
</div>
)}
@@ -439,7 +439,7 @@ export function HandApprovalModal({
{/* Parameters */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Execution Parameters
</label>
<ParamsDisplay params={approvalData.params} />
</div>
@@ -449,7 +449,7 @@ export function HandApprovalModal({
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 flex items-center gap-1">
<Info className="w-3.5 h-3.5" />
Expected Impact
</label>
<p className="text-sm text-gray-600 dark:text-gray-400 bg-blue-50 dark:bg-blue-900/20 p-3 rounded-lg">
{approvalData.expectedImpact}
@@ -459,9 +459,9 @@ export function HandApprovalModal({
{/* Request Info */}
<div className="text-xs text-gray-500 dark:text-gray-400 space-y-1 pt-2 border-t border-gray-200 dark:border-gray-700">
<p>Run ID: {approvalData.runId}</p>
<p> ID: {approvalData.runId}</p>
<p>
Requested: {new Date(approvalData.requestedAt).toLocaleString()}
: {new Date(approvalData.requestedAt).toLocaleString()}
</p>
</div>
@@ -469,12 +469,12 @@ export function HandApprovalModal({
{showRejectInput && (
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Rejection Reason <span className="text-red-500">*</span>
<span className="text-red-500">*</span>
</label>
<textarea
value={rejectReason}
onChange={(e) => setRejectReason(e.target.value)}
placeholder="Please provide a reason for rejecting this request..."
placeholder="请提供拒绝此请求的原因..."
className="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-md bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-red-500"
rows={3}
autoFocus
@@ -502,7 +502,7 @@ export function HandApprovalModal({
disabled={isProcessing}
className="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors disabled:opacity-50"
>
Cancel
</button>
<button
type="button"
@@ -513,12 +513,12 @@ export function HandApprovalModal({
{isProcessing ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Rejecting...
...
</>
) : (
<>
<XCircle className="w-4 h-4" />
Confirm Rejection
</>
)}
</button>
@@ -531,7 +531,7 @@ export function HandApprovalModal({
disabled={isProcessing}
className="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors disabled:opacity-50"
>
Close
</button>
<button
type="button"
@@ -540,7 +540,7 @@ export function HandApprovalModal({
className="px-4 py-2 text-sm border border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors disabled:opacity-50 flex items-center gap-2"
>
<XCircle className="w-4 h-4" />
Reject
</button>
<button
type="button"
@@ -551,12 +551,12 @@ export function HandApprovalModal({
{isProcessing ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Approving...
...
</>
) : (
<>
<CheckCircle className="w-4 h-4" />
Approve
</>
)}
</button>

View File

@@ -368,7 +368,7 @@ function ArrayParamInput({ param, value, onChange, disabled, error }: ParamInput
</div>
{items.length === 0 && !newItem && (
<p className="text-xs text-gray-400 text-center">No items added yet</p>
<p className="text-xs text-gray-400 text-center"></p>
)}
</div>
);

View File

@@ -428,10 +428,10 @@ export function MessageSearch({ onNavigateToMessage }: MessageSearchProps) {
onChange={(e) => setFilters((prev) => ({ ...prev, timeRange: e.target.value as SearchFilters['timeRange'] }))}
className="text-xs bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded px-2 py-1 focus:outline-none focus:ring-1 focus:ring-orange-500"
>
<option value="all">All time</option>
<option value="today">Today</option>
<option value="week">This week</option>
<option value="month">This month</option>
<option value="all"></option>
<option value="today"></option>
<option value="week"></option>
<option value="month"></option>
</select>
</div>
</div>
@@ -442,7 +442,7 @@ export function MessageSearch({ onNavigateToMessage }: MessageSearchProps) {
{/* Search history */}
{!query && searchHistory.length > 0 && (
<div className="mt-2">
<div className="text-xs text-gray-400 dark:text-gray-500 mb-1">Recent searches:</div>
<div className="text-xs text-gray-400 dark:text-gray-500 mb-1">:</div>
<div className="flex flex-wrap gap-1">
{searchHistory.slice(0, 5).map((item, index) => (
<button