diff --git a/src/gateway/todo_prompt_provider.rs b/src/gateway/todo_prompt_provider.rs index 2d9ade2..f5e2b6f 100644 --- a/src/gateway/todo_prompt_provider.rs +++ b/src/gateway/todo_prompt_provider.rs @@ -27,8 +27,8 @@ const TODO_WRITE_INSTRUCTIONS: &str = r#" - 不需要为简单的单步操作(如回答一个问题、读取一个文件)创建 todo ### merge 参数 -- `merge: false`(默认):全量替换 — 只传入需要追踪的 todo,不在列表中的项将被移除 -- `merge: true`(推荐):增量更新 — 只传入需要添加或更新的项,未提及的项保持不变。**绝大多数情况应该使用 merge=true** +- `merge: true`(默认,推荐):增量更新 — 只传入需要添加或更新的项,未提及的项保持不变。**绝大多数情况使用默认即可** +- `merge: false`:全量替换 — 只传入需要追踪的 todo,不在列表中的项将被移除 ### 状态语义 - `pending` — 尚未开始 diff --git a/src/protocol/ws_adapter.rs b/src/protocol/ws_adapter.rs index 14e36a9..a85111b 100644 --- a/src/protocol/ws_adapter.rs +++ b/src/protocol/ws_adapter.rs @@ -26,6 +26,7 @@ pub(crate) fn ws_outbound_from_chat_message(message: &ChatMessage) -> Vec Vec Vec &str { "Manage a structured task list for tracking work within the current conversation. \ - Two modes: merge=false (default, full replacement — omitted items are removed); \ - merge=true (incremental — only send the items you want to add/update, \ - previously existing items are preserved). \ + Two modes: merge=true (default, incremental — only send the items you want to add/update, \ + previously existing items are preserved); \ + merge=false (full replacement — omitted items are removed). \ Use when you have 3+ distinct steps to track. \ Rules: only ONE in_progress at a time, complete work before marking completed, \ every item requires an id (generate a short random string for new items)." @@ -91,7 +91,7 @@ impl Tool for TodoWriteTool { "properties": { "merge": { "type": "boolean", - "description": "false (default): full replacement — todos not in the list are removed. true: incremental — only send items you want to add or update, existing items not mentioned are preserved." + "description": "true (default): incremental — only send items you want to add or update, existing items not mentioned are preserved. false: full replacement — todos not in the list are removed." }, "todos": { "type": "array", @@ -156,7 +156,7 @@ impl Tool for TodoWriteTool { let merge_mode = args .get("merge") .and_then(|v| v.as_bool()) - .unwrap_or(false); + .unwrap_or(true); // 3. 读锁获取旧状态 let old_items = { @@ -754,11 +754,12 @@ mod tests { .await .unwrap(); - // 只传入一个任务(任务B 被移除) + // 全量替换:只传入一个任务(任务B 被移除) let result = tool .execute_with_context( &context, json!({ + "merge": false, "todos": [ {"id": "i1", "content": "任务A", "status": "in_progress"} ] @@ -1054,11 +1055,12 @@ mod tests { .await .unwrap(); - // merge=false(默认)— 只传一个,另一个被删 + // 显式指定 merge=false — 只传一个,另一个被删 let result = tool .execute_with_context( &context, json!({ + "merge": false, "todos": [ {"id": "p1", "content": "任务A", "status": "in_progress"} ] @@ -1140,4 +1142,45 @@ mod tests { assert!(!result.success); assert!(result.error.unwrap().contains("missing or empty 'id'")); } + + #[tokio::test] + async fn test_default_is_merge_mode() { + let state = test_state(); + let tool = TodoWriteTool::new(state.clone()); + let context = test_context(); + + // 先创建 2 个 todo + let _ = tool + .execute_with_context( + &context, + json!({ + "todos": [ + {"id": "x1", "content": "任务A", "status": "pending"}, + {"id": "x2", "content": "任务B", "status": "pending"} + ] + }), + ) + .await + .unwrap(); + + // 不传 merge 参数,只更新一项 — 默认应为 merge=true,旧项保留 + let result = tool + .execute_with_context( + &context, + json!({ + "todos": [ + {"id": "x1", "content": "任务A", "status": "in_progress"} + ] + }), + ) + .await + .unwrap(); + + assert!(result.success); + let output: serde_json::Value = serde_json::from_str(&result.output).unwrap(); + let todos = output["current_todos"].as_array().unwrap(); + assert_eq!(todos.len(), 2); // 默认 merge,旧项保留 + let task_a = todos.iter().find(|t| t["id"] == "x1").unwrap(); + assert_eq!(task_a["status"], "in_progress"); + } } diff --git a/web/src/App.tsx b/web/src/App.tsx index 5606df4..bc91dbe 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -502,10 +502,10 @@ function App() { } } - // 过滤无实质内容的 merged_tool(无结果且非等待中) + // 过滤无实质内容的 merged_tool:result 到达后才显示保留;calling/pending 有 callContent 也保留 return result.filter(msg => { if (msg.type !== 'merged_tool') return true - if (msg.status === 'pending') return true + if (msg.status === 'calling' || msg.status === 'pending') return true return !!(msg.resultContent && msg.resultContent.trim()) }) }, [messages])