Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,24 @@

## [Unreleased]

## [0.4.20] - 2026-09-21

### 新增
- 对话:长对话左侧出现聊天大纲,点刻度可跳到自己发出的消息;悬停预览,当前轮高亮
- 对话:顶栏可切换会话,Alt+↑ / Alt+↓ 切上一条 / 下一条
- 对话:本轮改过的文件会列出来,点「查看修改」可预览

### 改进
- 对话:历史按工作目录分组,侧栏入口改成「工作区」;组标题显示文件夹名,悬停看路径,加号开新会话
- 对话:思考过程和工具分开显示;思考是可折叠段,完成后写时长
- 对话:计划条标出待做 / 进行中 / 已完成,可收起;Claude 新对话的任务清单接到计划条
- 对话:顶栏不再写这次怎么接;从历史继续时,对上对方会话就复用
- 用量:刷新嵌入的模型单价

### 修复
- 对话:宽面板下聊天大纲会挂上可点刻度,不再只留下看不见的测量层
- 对话:点「新建对话」不再因循环引用卡住

## [0.4.19] - 2026-09-17

### 新增
Expand Down
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ members = [
]

[workspace.package]
version = "0.4.19"
version = "0.4.20"
edition = "2021"
license = "MIT"
authors = ["AgentHub"]
Expand Down
169 changes: 169 additions & 0 deletions crates/agenthub-core/src/services/chat_runtime/actor_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2651,6 +2651,175 @@ fn claude_stream_result_completes_turn_and_keeps_session() {
);
}

#[test]
fn claude_todo_write_fills_snapshot_not_timeline() {
let db = Database::open_in_memory().unwrap();
conversation_with(&db, "claude-plan", AgentId::Claude, &std::env::temp_dir());
let mut worker = worker(&db, "claude-plan");
worker.agent = AgentId::Claude;
worker.store.enable_if_new("claude-plan").unwrap();
start_placeholder(&mut worker);

worker
.notification(
"claude/stream",
&json!({
"type": "assistant",
"session_id": "claude-sess-plan",
"message": {
"role": "assistant",
"content": [{
"type": "tool_use",
"id": "toolu_todo",
"name": "TodoWrite",
"input": {
"todos": [
{ "content": "read", "status": "completed" },
{ "content": "edit", "status": "in_progress" }
]
}
}]
}
}),
)
.unwrap();

let stored = worker.store.snapshot("claude-plan", None).unwrap();
assert!(!stored.events.iter().any(|event| matches!(
&event.event,
ChatEvent::AgentProcess { .. }
)));
let snapshot = worker.with_catalog_epoch(stored);
assert_eq!(snapshot.plan.len(), 2);
assert_eq!(snapshot.plan[0].content, "read");
assert_eq!(snapshot.plan[0].status.as_deref(), Some("completed"));
assert_eq!(snapshot.plan[1].content, "edit");
assert_eq!(snapshot.plan[1].status.as_deref(), Some("in_progress"));

worker
.notification(
"claude/stream",
&json!({
"type": "assistant",
"message": {
"content": [{
"type": "tool_use",
"id": "toolu_create",
"name": "TaskCreate",
"input": { "subject": "write tests" }
}]
}
}),
)
.unwrap();
worker
.notification(
"claude/stream",
&json!({
"type": "user",
"tool_use_result": { "task": { "id": "task-2", "subject": "write tests" } },
"message": {
"content": [{
"type": "tool_result",
"tool_use_id": "toolu_create",
"content": "created"
}]
}
}),
)
.unwrap();
worker
.notification(
"claude/stream",
&json!({
"type": "assistant",
"message": {
"content": [{
"type": "tool_use",
"name": "TaskUpdate",
"input": { "taskId": "task-2", "status": "in_progress" }
}]
}
}),
)
.unwrap();

worker
.notification(
"claude/stream",
&json!({
"type": "user",
"tool_use_result": {
"oldTodos": [{ "content": "read", "status": "pending" }],
"newTodos": [{ "content": "read", "status": "completed" }]
},
"message": {
"content": [{
"type": "tool_result",
"tool_use_id": "toolu_todo",
"content": "Todos have been modified successfully."
}]
}
}),
)
.unwrap();
worker
.notification(
"claude/stream",
&json!({
"type": "assistant",
"message": {
"content": [{
"type": "tool_use",
"id": "toolu_bash",
"name": "Bash",
"input": { "command": "ls" }
}]
}
}),
)
.unwrap();
worker
.notification(
"claude/stream",
&json!({
"type": "assistant",
"message": {
"content": [{
"type": "tool_use",
"name": "TodoWrite",
"input": { "todos": [] }
}]
}
}),
)
.unwrap();

let patched = worker.with_catalog_epoch(worker.store.snapshot("claude-plan", None).unwrap());
assert_eq!(patched.plan.len(), 3);
assert_eq!(patched.plan[2].content, "write tests");
assert_eq!(patched.plan[2].id.as_deref(), Some("task-2"));
assert_eq!(patched.plan[2].status.as_deref(), Some("in_progress"));
assert!(patched.events.iter().any(|event| matches!(
&event.event,
ChatEvent::AgentProcess {
step: crate::models::ProcessStep::Tool { name, .. },
..
} if name == "Bash"
)));
assert!(!patched.events.iter().any(|event| matches!(
&event.event,
ChatEvent::AgentProcess {
step: crate::models::ProcessStep::Tool { name, .. },
..
} if name == "TodoWrite" || name == "TaskCreate" || name == "TaskUpdate" || name == "tool"
)));

worker.clear_turn_plan();
let cleared = worker.with_catalog_epoch(worker.store.snapshot("claude-plan", None).unwrap());
assert!(cleared.plan.is_empty());
}

#[cfg(unix)]
#[test]
fn grok_fs_write_outside_cwd_emits_card_then_writes_on_allow() {
Expand Down
Loading
Loading