From bf1549b88baa80c434e58a0c874fcdb71e43b827 Mon Sep 17 00:00:00 2001 From: oudecheng <13802883547@139.com> Date: Mon, 29 Jun 2026 09:24:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=20todo=5Fwrite=20?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E7=9A=84=20merge=20=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E6=8F=8F=E8=BF=B0=EF=BC=8C=E8=B0=83=E6=95=B4=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=80=BC=E9=80=BB=E8=BE=91=E5=B9=B6=E6=B7=BB=E5=8A=A0=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gateway/todo_prompt_provider.rs | 4 +- src/protocol/ws_adapter.rs | 3 ++ src/tools/todo_write.rs | 57 +++++++++++++++++++++++++---- web/src/App.tsx | 4 +- 4 files changed, 57 insertions(+), 11 deletions(-) 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])