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
52 changes: 52 additions & 0 deletions rust/lithe-agent-host/src/claude_earlier_history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ const MAX_TRANSCRIPT_BYTES: u64 = 128 * 1024 * 1024;
const MAX_UPDATES: usize = 30_000;
/// Per-field text limit, in characters, for tool output and tool input values.
const TEXT_LIMIT: usize = 4_000;
/// Longest base64 image replayed, in bytes: the client's 10 MB decoded limit
/// (`AgentImage.maximumBytes`) inflated by a third. A larger screenshot would only
/// be dropped on arrival, so it is not sent.
const IMAGE_DATA_LIMIT: usize = 10_000_000 / 3 * 4;

/// Projects directory of the account the adapter runs under: the explicit
/// account directory, then `CLAUDE_CONFIG_DIR`, then `~/.claude`.
Expand Down Expand Up @@ -182,10 +186,37 @@ impl Converter {
}
(Chunk::Agent, Some("tool_use")) => self.tool_call(block),
(Chunk::User, Some("tool_result")) => self.tool_result(block),
(Chunk::User, Some("image")) => self.user_image(block),
_ => {}
}
}

/// A pasted screenshot stays attached to the user message that references
/// it. Only inline base64 sources are replayed; the client bounds the size.
fn user_image(&mut self, block: &Value) {
let source = &block["source"];
let (Some("base64"), Some(data)) = (source["type"].as_str(), source["data"].as_str())
else {
return;
};
if data.is_empty() || data.len() > IMAGE_DATA_LIMIT {
return;
}
self.last = Some(Chunk::User);
let mut update = json!({
"sessionUpdate": "user_message_chunk",
"content": {
"type": "image",
"data": data,
"mimeType": source["media_type"].as_str().unwrap_or("image/png"),
},
});
if let Some(id) = &self.row_id {
update["messageId"] = json!(id);
}
self.updates.push(update);
}

fn text(&mut self, kind: Chunk, text: &str) {
if text.trim().is_empty() {
return;
Expand Down Expand Up @@ -424,6 +455,27 @@ mod tests {
assert_eq!(updates[1]["messageId"], "q1");
}

#[test]
fn pasted_screenshots_stay_on_the_user_message_that_references_them() {
let (dir, cwd) = transcript(&[
json!({"type": "user", "uuid": "u1", "message": {"content": [
{"type": "text", "text": "[Image #1] look"},
{"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": "/9j/4A=="}},
{"type": "image", "source": {"type": "url", "url": "https://example.test/a.png"}},
{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": ""}},
{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": "A".repeat(IMAGE_DATA_LIMIT + 4)}},
]}}),
boundary(),
]);
let updates = earlier_updates(dir.path(), &cwd, "abc-1");
assert_eq!(updates.len(), 2, "only the inline base64 image within the limit is replayed");
assert_eq!(updates[1]["sessionUpdate"], "user_message_chunk");
assert_eq!(updates[1]["content"]["type"], "image");
assert_eq!(updates[1]["content"]["data"], "/9j/4A==");
assert_eq!(updates[1]["content"]["mimeType"], "image/jpeg");
assert_eq!(updates[1]["messageId"], updates[0]["messageId"]);
}

#[test]
fn long_text_is_cut_on_a_character_boundary() {
let long = "é".repeat(TEXT_LIMIT + 10);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"macos/Tests/LitheTests/AgentThoughtTranscriptTests.swift",
"macos/Sources/LitheAgentConversationModule/Application/AgentImage.swift",
"macos/Sources/Lithe/Views/Agent/AgentImageViews.swift",
"macos/Tests/LitheTests/AgentImageTests.swift"
"macos/Tests/LitheTests/AgentImageTests.swift",
"rust/lithe-agent-host/src/claude_earlier_history.rs"
],
"implementationStatus": "implemented",
"verificationStatus": "pending"
Expand All @@ -33,6 +34,6 @@
"verificationStatus": "pending"
},
"owner": "Agent",
"verification": "macOS 与 Windows:构造思考→过程说明→读文件→思考→执行命令→最终回复,确认每轮只有一个过程入口且默认收起,流式也不自动展开;收起时过程文字与工具行不占高度,最终回复始终可读。展开按原顺序呈现,只有相邻工具合并,状态与证据可继续查看。纯空白思考、过程说明不产生空行,没有思考和工具的直接回答不产生入口;新用户消息、已记录的轮次结束分别切断分组,工具或思考结束而没有最终回复时不伪造回复。搜索过程文字及工具输入、输出、文件路径,匹配组临时展开且不能在搜索期间收起,清空后恢复手动选择;切换和关闭会话验证偏好隔离与释放。深浅主题、窄宽面板、历史恢复和实时流均验证;历史导出、文件审阅、权限卡片、轮次统计和回复复制继续可用。单元测试与组件回放不替代真实 Agent 和 Windows Tauri 运行验收,未执行部分保持 pending。 macOS 图片:让 Agent 读取多张图片,确认缩略图最多三张并显示 +N,悬停出现带标题和序号的预览,点击进入放大查看并可用方向键切换、Esc 关闭;用户粘贴图片后气泡显示缩略图且可放大;触发后台任务后不再出现原始 task-notification 标记。Windows 对应能力尚未实现。 macOS 后台任务与压缩历史:让 Agent 连续启动多个后台命令,确认成功的通知不再单独成行,而是在对应一轮的“思考过程”展开后以中文状态行出现;失败的通知仍在对话中可见。恢复一个经历过多次压缩的 Claude 会话,确认压缩分界线之前的用户消息、回复、思考和工具调用都能滚动查看,且每次压缩处有“对话已从摘要继续”标记。 点击缩略图后的大图支持双指缩放、拖动、百分比与 ⌘+/⌘−/⌘0;用户消息里的 file:// 链接与 [Image: source:] 占位符显示为缩略图;Claude Code 的斜杠命令、本地命令输出、Shell 转义、压缩续接摘要、技能展开和宿主上下文显示为状态行或折叠块,只给模型看的提醒不显示。",
"notes": "2026-10-24:改为每轮一个默认折叠的过程区,包含思考、工具和中间说明;只改变呈现,不改写历史或 ACP 协议。ACP 无最终回复角色,按最后一条思考或工具的位置保留末尾正文;后续活动到达时才将先前正文移入过程。 2026-10-24:macOS 增加工具图片缩略图、悬停预览、放大查看器和后台任务通知状态行(#1462),Windows 待对齐。 2026-10-24(续):折叠的思考过程标题行也显示图片缩略图;放大查看器增加缩放;识别更多 Claude Code 特殊消息格式。 2026-10-24(再续):成功的后台任务通知并入后续过程区;Rust 宿主在 session/load 时只读取最后一个压缩点之前的会话记录,补全 Claude 适配器不回放的历史。"
"verification": "macOS 与 Windows:构造思考→过程说明→读文件→思考→执行命令→最终回复,确认每轮只有一个过程入口且默认收起,流式也不自动展开;收起时过程文字与工具行不占高度,最终回复始终可读。展开按原顺序呈现,只有相邻工具合并,状态与证据可继续查看。纯空白思考、过程说明不产生空行,没有思考和工具的直接回答不产生入口;新用户消息、已记录的轮次结束分别切断分组,工具或思考结束而没有最终回复时不伪造回复。搜索过程文字及工具输入、输出、文件路径,匹配组临时展开且不能在搜索期间收起,清空后恢复手动选择;切换和关闭会话验证偏好隔离与释放。深浅主题、窄宽面板、历史恢复和实时流均验证;历史导出、文件审阅、权限卡片、轮次统计和回复复制继续可用。单元测试与组件回放不替代真实 Agent 和 Windows Tauri 运行验收,未执行部分保持 pending。 macOS 图片:让 Agent 读取多张图片,确认缩略图最多三张并显示 +N,悬停出现带标题和序号的预览,点击进入放大查看并可用方向键切换、Esc 关闭;用户粘贴图片后气泡显示缩略图且可放大;触发后台任务后不再出现原始 task-notification 标记。Windows 对应能力尚未实现。 macOS 后台任务与压缩历史:让 Agent 连续启动多个后台命令,确认成功的通知不再单独成行,而是在对应一轮的“思考过程”展开后以中文状态行出现;失败的通知仍在对话中可见。恢复一个经历过多次压缩的 Claude 会话,确认压缩分界线之前的用户消息、回复、思考和工具调用都能滚动查看,且每次压缩处有“对话已从摘要继续”标记。 点击缩略图后的大图支持双指缩放、拖动、百分比与 ⌘+/⌘−/⌘0;用户消息里的 file:// 链接与 [Image: source:] 占位符显示为缩略图;Claude Code 的斜杠命令、本地命令输出、Shell 转义、压缩续接摘要、技能展开和宿主上下文显示为状态行或折叠块,只给模型看的提醒不显示。 压缩前的用户消息若带粘贴截图,恢复会话后截图仍以缩略图显示在原消息气泡里(不是只剩 [Image #N] 文字),可点击放大。",
"notes": "2026-10-24:改为每轮一个默认折叠的过程区,包含思考、工具和中间说明;只改变呈现,不改写历史或 ACP 协议。ACP 无最终回复角色,按最后一条思考或工具的位置保留末尾正文;后续活动到达时才将先前正文移入过程。 2026-10-24:macOS 增加工具图片缩略图、悬停预览、放大查看器和后台任务通知状态行(#1462),Windows 待对齐。 2026-10-24(续):折叠的思考过程标题行也显示图片缩略图;放大查看器增加缩放;识别更多 Claude Code 特殊消息格式。 2026-10-24(再续):成功的后台任务通知并入后续过程区;Rust 宿主在 session/load 时只读取最后一个压缩点之前的会话记录,补全 Claude 适配器不回放的历史。 2026-10-25:压缩前历史由 Rust 宿主读取会话记录补全,之前丢弃用户消息里的内联截图,现在作为同一条用户消息的 image 块回放(仅 base64 来源);Windows 待对齐。"
}
Loading