Compare commits
3 Commits
a40f57fb22
...
bf1549b88b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf1549b88b | ||
|
|
4742a6aed9 | ||
|
|
63714b6c6e |
@ -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` — 尚未开始
|
||||
|
||||
@ -26,6 +26,7 @@ pub(crate) fn ws_outbound_from_chat_message(message: &ChatMessage) -> Vec<WsOutb
|
||||
topic_id: None,
|
||||
timestamp: None,
|
||||
reasoning_content: message.reasoning_content.clone(),
|
||||
user_message_id: None,
|
||||
});
|
||||
}
|
||||
|
||||
@ -42,6 +43,7 @@ pub(crate) fn ws_outbound_from_chat_message(message: &ChatMessage) -> Vec<WsOutb
|
||||
topic_id: None,
|
||||
timestamp: None,
|
||||
reasoning_content: tc_reasoning.clone(),
|
||||
user_message_id: None,
|
||||
}));
|
||||
outbound
|
||||
} else {
|
||||
@ -54,6 +56,7 @@ pub(crate) fn ws_outbound_from_chat_message(message: &ChatMessage) -> Vec<WsOutb
|
||||
topic_id: None,
|
||||
timestamp: None,
|
||||
reasoning_content: message.reasoning_content.clone(),
|
||||
user_message_id: None,
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
@ -456,7 +456,7 @@ fn parse_attachments(value: &serde_json::Value) -> anyhow::Result<Vec<MediaItem>
|
||||
return Err(anyhow!("attachment file is empty: {}", raw_path));
|
||||
}
|
||||
|
||||
let content_base64 = (metadata.len() <= 50 * 1024 * 1024)
|
||||
let content_base64 = (metadata.len() <= 200 * 1024 * 1024)
|
||||
.then(|| {
|
||||
let mut file = std::fs::File::open(&resolved_path)?;
|
||||
let mut buf = Vec::with_capacity(metadata.len() as usize);
|
||||
|
||||
@ -77,9 +77,9 @@ impl Tool for TodoWriteTool {
|
||||
|
||||
fn description(&self) -> &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");
|
||||
}
|
||||
}
|
||||
|
||||
123
web/src/App.tsx
123
web/src/App.tsx
@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Zap, ArrowLeft, Bot, Clock, Sun, Moon, PanelRightOpen, X, Brain, Settings as SettingsIcon, ChevronRight } from 'lucide-react'
|
||||
import { Zap, ArrowLeft, Bot, Clock, Sun, Moon, PanelRightOpen, PanelLeftClose, PanelLeftOpen, X, Brain, Settings as SettingsIcon, ChevronRight } from 'lucide-react'
|
||||
import { ChatContainer } from './components/Chat/ChatContainer'
|
||||
import { TopicList } from './components/Sidebar/TopicList'
|
||||
import { SchedulerJobList } from './components/Sidebar/SchedulerJobList'
|
||||
@ -120,6 +120,22 @@ function App() {
|
||||
|
||||
const [rightPanelTab, setRightPanelTab] = useState<'memory' | 'skill'>('memory')
|
||||
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
|
||||
try {
|
||||
return localStorage.getItem('picobot-sidebar-collapsed') === 'true'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
const toggleSidebar = useCallback(() => {
|
||||
setSidebarCollapsed(prev => {
|
||||
const next = !prev
|
||||
localStorage.setItem('picobot-sidebar-collapsed', String(next))
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
const [theme, setTheme] = useState<'dark' | 'light'>(() => {
|
||||
const saved = localStorage.getItem('picobot-theme')
|
||||
return saved === 'light' ? 'light' : 'dark'
|
||||
@ -486,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])
|
||||
@ -562,34 +578,83 @@ function App() {
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex flex-1 overflow-hidden relative">
|
||||
{/* Left Sidebar */}
|
||||
<div className={`w-72 shrink-0 border-r border-[var(--border-color)] bg-[var(--bg-secondary)]/50 flex flex-col ${subAgentView || schedulerView ? 'opacity-50 pointer-events-none' : ''}`}>
|
||||
{/* Tab 栏 */}
|
||||
<div className="flex border-b border-[var(--border-color)]">
|
||||
<button
|
||||
onClick={() => setSidebarTab('topics')}
|
||||
className={`flex-1 py-2.5 text-sm font-medium text-center transition-colors ${
|
||||
sidebarTab === 'topics'
|
||||
? 'text-[var(--accent-cyan)] border-b-2 border-[var(--accent-cyan)]'
|
||||
: 'text-[var(--text-muted)] hover:text-[var(--text-secondary)]'
|
||||
}`}
|
||||
>
|
||||
话题
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSidebarTab('scheduler')}
|
||||
className={`flex-1 py-2.5 text-sm font-medium text-center transition-colors ${
|
||||
sidebarTab === 'scheduler'
|
||||
? 'text-[var(--accent-cyan)] border-b-2 border-[var(--accent-cyan)]'
|
||||
: 'text-[var(--text-muted)] hover:text-[var(--text-secondary)]'
|
||||
}`}
|
||||
>
|
||||
定时任务
|
||||
</button>
|
||||
{/* Left Sidebar — smooth width animation via will-change + overflow-hidden */}
|
||||
<div
|
||||
className={`shrink-0 border-r border-[var(--border-color)] bg-[var(--bg-secondary)]/50 flex flex-col overflow-hidden ${sidebarCollapsed ? 'w-11' : 'w-72'} ${subAgentView || schedulerView ? 'opacity-50 pointer-events-none' : ''}`}
|
||||
style={{ transition: 'width 200ms ease-out', willChange: 'width' }}
|
||||
>
|
||||
{/* Tab 栏 + collapse toggle */}
|
||||
<div className="flex border-b border-[var(--border-color)]" style={{ minWidth: sidebarCollapsed ? 0 : '288px' }}>
|
||||
{sidebarCollapsed ? (
|
||||
/* 收起态:紧凑竖排 tab 按钮 */
|
||||
<div className="flex flex-col w-full">
|
||||
<button
|
||||
onClick={() => { setSidebarTab('topics'); setSidebarCollapsed(false); localStorage.setItem('picobot-sidebar-collapsed', 'false'); }}
|
||||
className={`py-2 text-xs font-medium text-center transition-colors ${
|
||||
sidebarTab === 'topics'
|
||||
? 'text-[var(--accent-cyan)] bg-[var(--accent-cyan)]/10'
|
||||
: 'text-[var(--text-muted)] hover:text-[var(--text-secondary)] hover:bg-[var(--overlay-hover)]'
|
||||
}`}
|
||||
title="话题"
|
||||
>
|
||||
话<br/>题
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setSidebarTab('scheduler'); setSidebarCollapsed(false); localStorage.setItem('picobot-sidebar-collapsed', 'false'); }}
|
||||
className={`py-2 text-xs font-medium text-center transition-colors ${
|
||||
sidebarTab === 'scheduler'
|
||||
? 'text-[var(--accent-cyan)] bg-[var(--accent-cyan)]/10'
|
||||
: 'text-[var(--text-muted)] hover:text-[var(--text-secondary)] hover:bg-[var(--overlay-hover)]'
|
||||
}`}
|
||||
title="定时任务"
|
||||
>
|
||||
定<br/>时
|
||||
</button>
|
||||
{/* 展开按钮 */}
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className="flex items-center justify-center py-2 text-[var(--text-muted)] hover:text-[var(--accent-cyan)] hover:bg-[var(--overlay-hover)] transition-colors"
|
||||
title="展开侧栏"
|
||||
>
|
||||
<PanelLeftOpen className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setSidebarTab('topics')}
|
||||
className={`flex-1 py-2.5 text-sm font-medium text-center transition-colors whitespace-nowrap ${
|
||||
sidebarTab === 'topics'
|
||||
? 'text-[var(--accent-cyan)] border-b-2 border-[var(--accent-cyan)]'
|
||||
: 'text-[var(--text-muted)] hover:text-[var(--text-secondary)]'
|
||||
}`}
|
||||
>
|
||||
话题
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSidebarTab('scheduler')}
|
||||
className={`flex-1 py-2.5 text-sm font-medium text-center transition-colors whitespace-nowrap ${
|
||||
sidebarTab === 'scheduler'
|
||||
? 'text-[var(--accent-cyan)] border-b-2 border-[var(--accent-cyan)]'
|
||||
: 'text-[var(--text-muted)] hover:text-[var(--text-secondary)]'
|
||||
}`}
|
||||
>
|
||||
定时任务
|
||||
</button>
|
||||
{/* 收起按钮 */}
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className="flex items-center justify-center px-2 text-[var(--text-muted)] hover:text-[var(--accent-cyan)] hover:bg-[var(--overlay-hover)] transition-colors shrink-0"
|
||||
title="收起侧栏"
|
||||
>
|
||||
<PanelLeftClose className="h-4 w-4" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{sidebarTab === 'topics' ? (
|
||||
<div className="flex-1 overflow-hidden" style={{ minWidth: sidebarCollapsed ? 0 : '288px' }}>
|
||||
{sidebarCollapsed ? null : sidebarTab === 'topics' ? (
|
||||
<TopicList
|
||||
sessionId={sessionId}
|
||||
topics={topics}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user