fix(acp): preserve file attachments in custom-agent history - #780
Merged
xintaofei merged 4 commits intoSep 21, 2026
Merged
Conversation
Recorded prompts contain resource/resource_link blocks, but the history projection only kept top-level text and native images. Session/load replay also discarded binary resources and user images. Use one user-content projection for recorded prompts and replay: preserve file markers, promote image resources, and keep mixed chunks in one user turn without duplicating recorded prompt echoes. No transcript/schema or client protocol changes; existing raw records can be reparsed. Validated with all 20 acp_native parser tests, including four regressions.
The attachment-preserving history projection turns a non-image resource
into a `[uri](uri)` marker block. On the `session/load` replay path the
text-coalescing rule then appended the NEXT user chunk onto it, so an
agent that stores file context before the prose ("here is the file" +
"what does this do?") read back as one run-on paragraph with the prose
welded onto the end of a markdown link. The live projection
(`user_blocks_from_prompt`) emits one block per prompt block, and this
parser exists to match it.
Coalesce only onto prose — text a previous `user_message_chunk`
streamed — so a marker or an image always closes its block.
Adds the two regression cases that the change turns on (prose after a
marker; an attachment-only chunk ending the previous assistant turn) and
a parity test that pins this projection to `user_blocks_from_prompt`
through the real `map_prompt_blocks` wire encoding, so the two
implementations of one contract cannot drift silently.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three surfaces showed a user's own prompt back and each decided for itself how an attachment looks: the live broadcast (`user_blocks_from_prompt`), the ACP-native history parser, and the grok history parser. They had already drifted — grok had no `resource_link` case at all, so a plain attached file came back from its history as an empty block, the same defect the ACP-native parser just had fixed. Collapse the rule into `acp::types::project_user_prompt_block`, with `prompt_block_from_wire` to read a recorded ACP block back into the `PromptInputBlock` it was sent as. The live path becomes a thin adapter over it (it only drops an image's `uri`, which its carrier cannot hold), and both parsers go through `parsers::user_turn_block_from_wire`. Three things fall out: - The replay path no longer serializes an already-typed content block back to JSON just to re-read it untyped — a round trip this file's own comments say it avoids, and one that copied every embedded image's base64 twice. It now converts typed, moving the payload. - Attachment markers are escaped the way the composer escapes its own `@`-file links, so `file:///a/b (1).ts` or a Windows `file:///C:\dir\` stays a well-formed link instead of closing early and rendering as raw `[…](…)` source. The frontend already parses that form (`src/lib/reference-link.ts`); a test there pins the two escapers to each other across the language boundary. - Grok's user turns keep their file attachments. Also: a prompt that is nothing but attachments is titled after what was attached. ACP has no title channel, so such a conversation was previously untitled forever. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The parsed title is authoritative: `get_folder_conversation_core` hands it to `refresh_auto_title`, which replaces any unlocked title the row holds — including one a custom ACP agent published over `session_info_update`. So returning an attachment filename from the history parse meant "Quarterly report analysis" could be overwritten with "report.pdf" on the next detail load, where before the parse returned nothing and left the agent's name alone. Put the attachment name where it is a fallback rather than a verdict: the first-prompt seed in `acp::manager`, which reaches `seed_auto_title_if_empty` and a fresh row's `title` and nothing else. A row with no title at all gets named after its file; a row that already has one keeps it. The parse goes back to prose-only. Also from the same review pass: - grok latched `first_user_text` off the projected block, so an attachment ahead of the prose would title the chat `[report.pdf](…)`. It now reads the block as sent and latches only on real prose. - `project_user_prompt_block` borrows instead of consuming. Taking it by value made `user_blocks_from_prompt` clone a whole embedded resource — body and all — only to render its uri. It now clones just the fields the projection keeps, which is what it cost before. - The raw wire reader no longer drops an empty `mimeType`, so it agrees with the typed replay reader on that input too (asserted). - `escape_markdown_text` collapses newline runs the way its TypeScript original does, so a label with a line break stays one inline token. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Owner
|
codeg work task |
xintaofei
added a commit
that referenced
this pull request
Sep 22, 2026
This one is about staying up. An agent that refuses a slash command, a grok handshake, a Cursor install on Linux, a long turn with thousands of tool calls, a crash with nothing in the log — each used to end the session you were in, and none of them does now. Alongside that: Settings gives collaboration and the built-in browser pages of their own, the file tree can copy the file itself, and thinking blocks stop opening and closing under you mid-answer. ## New - **Settings splits "Collaboration" and "Browser" out of General** — delegation and in-conversation tools on one page, the built-in browser on another, unfolded. - **The file tree's right-click menu becomes a "Copy" submenu** — relative path, absolute path, or the file itself onto the system clipboard (desktop only). - **Cursor's own extension calls are answered instead of refused**, so a Task spawn no longer shows a red "Method not found" banner. (#783 reported by @goon-13, #785, @goon-13) - **A crash now leaves a record in the log** — message, location, version and backtrace, written before the process dies. (#703, @Adam-Dalloul) ## Improved - **Updated bundled agents:** Grok 1.0.40, OpenCode 1.18.32, CodeBuddy 2.156.0, Cursor 2026.09.18, Qoder 1.1.60, Hermes 0.21.4. - **A thinking block stays folded until you open it, and stays open once you have.** - **File and folder rows line up at every depth**, with the redundant folder icon gone and the indent tightened. - **Backspace removes a reference badge in one press** instead of first eating the invisible space behind it. - **The composer is usable on mobile web again** — the editable area no longer collapses, and a tap anywhere in it opens the keyboard. (#746 reported by @evepupil, #779, @evepupil) - **File attachments survive a reload** on custom ACP agents and grok, and an attachment-only chat is named after the file. (#780, @tangsenfei) - **A reply's footer keeps filling in for agents that flush late**, so deepseek replies get their model, tokens and time — and "fork from here" stays available. - **The default-terminal picker says what your choice actually resolves to**, and badges a shell that is not installed. ## Fixed - **An agent rejecting a prompt no longer tears the session down** — Qwen Code refusing `/mcp` left the composer greyed out until a full respawn. (#797 reported by @linshaobao) - **Grok 1.0.40 connects again** — its new setup frames carry no session id yet, which read as a protocol error. (#794 reported by @qingyueyin) - **Cursor works on Linux, and an expired login now says so** instead of reading green while every session fails. - **Everything after a Claude Code `/clear` stays visible on reopen.** (#766 reported by @andrehqh, #778, @Frank-zhu0404) - **A long turn no longer freezes the session** — its tool-call history was resent whole on every attach (26.7 MB measured). (#380 reported by @ashlovepink, #712, @Adam-Dalloul) - **An agent reply no longer renders blank**, which the sub-agent dialog hit on an ordinary path. (#705, @Adam-Dalloul) - **A message sent mid-turn no longer duplicates the first half of the reply.** (#708, @Adam-Dalloul) - **Built-in browser:** a Google sign-in popup leaves no empty tab behind, a page redirecting mid-load is no longer covered by an error page, and the first blank tab stops spinning. - **A deleted or manually closed Office preview no longer reopens itself.** (#795, @Adam-Dalloul) - **Codex sessions are titled after your message**, not after the `AGENTS.md` fragment injected ahead of it. (#789 reported by @zhoujh78, #790, @dawNotPoi) - **The scientific research pack installs for Antigravity, Grok, Cursor, DeepSeek and Qoder**, whose toggles used to flip themselves back off. (#718 reported by @sunnyhmz7010, #793, @dawNotPoi) - **Launching at Windows login works within the first half-minute after boot.** (#703, @Adam-Dalloul) - **A Telegram group message can no longer crash the polling loop.** (#703, @Adam-Dalloul) Thanks to @Adam-Dalloul, @goon-13, @evepupil, @tangsenfei, @dawNotPoi and @Frank-zhu0404 for contributing to this release, and to @linshaobao, @qingyueyin, @andrehqh, @ashlovepink, @zhoujh78 and @sunnyhmz7010 for the reports. ----------------------------- # 发布版本 0.31.2 这一版的主题是「别断」。智能体拒绝一条斜杠命令、grok 的握手、Linux 上的 Cursor、一轮上千次工具调用的长任务、一次没留下日志的崩溃——它们此前都会让你正在进行的会话直接结束,现在都不会了。 同期还有:设置里的协作与内置浏览器各自独立成页,文件树可以直接复制文件本身,思考块不再在你读到一半时自己开合。 ## 新增 - **设置页把「协作」和「浏览器」从「通用」里拆了出来**——委派与会话内工具开关合为一页,内置浏览器独立一页且不再折叠。 - **文件树右键菜单从单条「复制路径」变成「复制」子菜单**——相对路径、绝对路径,或把文件本身放进系统剪贴板(仅桌面端)。 - **Cursor 自有的扩展调用不再被拒绝**,每次 Task 启动不会再糊上一条红色的「Method not found」横幅。(#783 由 @goon-13 反馈,#785,@goon-13) - **崩溃现在会在日志里留下记录**——进程退出前写入 panic 信息、代码位置、版本号和调用栈。(#703,@Adam-Dalloul) ## 改进 - **内置智能体版本更新:** Grok 1.0.40、OpenCode 1.18.32、CodeBuddy 2.156.0、Cursor 2026.09.18、Qoder 1.1.60、Hermes 0.21.4。 - **思考块在你点开之前保持折叠,点开之后就一直展开。** - **文件与目录在任意层级都对齐**,多余的文件夹图标去掉,缩进也收紧了。 - **退格键一次就能删掉引用胶囊**,不必先删掉它后面那个看不见的空格。 - **手机 Web 上的输入框恢复可用**——可编辑区域不再被压成 0 高度,点击编辑区任意位置都能唤起键盘。(#746 由 @evepupil 反馈,#779,@evepupil) - **自定义 ACP 智能体和 grok 的文件附件在刷新后不再丢失**,只带附件的对话会以附件文件名命名。(#780,@tangsenfei) - **回复页脚会为落盘较慢的智能体持续补齐信息**,deepseek 的回复能拿到模型、Token 用量和完成时间,「从此处分叉」也不再置灰。 - **默认终端选择器会显示你所选项真正解析到的程序**,未安装的终端明确标注。 ## 修复 - **智能体拒绝一条提示不再拆掉整个会话**——Qwen Code 拒绝 `/mcp` 后,输入框会一直置灰到会话重建完成。(#797 由 @linshaobao 反馈) - **Grok 1.0.40 可以正常连接**——它新增的初始化通知在会话 id 生成前为 null,被当成了协议错误。(#794 由 @qingyueyin 反馈) - **Linux 上的 Cursor 能用了,过期登录也会如实显示**,不再一边显示绿色一边每次发送都失败。 - **Claude Code `/clear` 之后的内容重新打开对话仍然可见。**(#766 由 @andrehqh 反馈,#778,@Frank-zhu0404) - **长任务不再让会话卡死**——此前每次接入都要重传整轮工具调用历史(实测 26.7 MB)。(#380 由 @ashlovepink 反馈,#712,@Adam-Dalloul) - **智能体回复不再出现整段空白**,子智能体弹窗在正常使用中就会碰到。(#705,@Adam-Dalloul) - **回合中途插话不再让回复前半段重复一遍。**(#708,@Adam-Dalloul) - **内置浏览器:** Google 登录弹窗不再留下空白标签页,加载中途跳转的页面不会被错误页盖住,首个空白标签页不再一直转圈。 - **已删除或手动关闭的 Office 预览不会再自己弹回来。**(#795,@Adam-Dalloul) - **Codex 会话标题取自你的消息**,而不是注入在前面的 `AGENTS.md` 片段。(#789 由 @zhoujh78 反馈,#790,@dawNotPoi) - **科学研究技能包对 Antigravity、Grok、Cursor、DeepSeek、Qoder 都能装上**,开关不会再自己刷回关闭。(#718 由 @sunnyhmz7010 反馈,#793,@dawNotPoi) - **开机自启在 Windows 刚启动的半分钟内不会再崩溃。**(#703,@Adam-Dalloul) - **Telegram 群消息不会再让轮询循环崩溃。**(#703,@Adam-Dalloul) 感谢 @Adam-Dalloul、@goon-13、@evepupil、@tangsenfei、@dawNotPoi、@Frank-zhu0404 为本次发布做出的贡献,也感谢 @linshaobao、@qingyueyin、@andrehqh、@ashlovepink、@zhoujh78 和 @sunnyhmz7010 的反馈。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
问题与影响
自定义 ACP 智能体的普通文件在发送时已送达,原始 ACP transcript 也保留了资源块,但刷新/重新打开会话后,历史接口返回的用户消息会遗漏这些附件。Web、桌面以及其他读取同一历史接口的客户端都会受到该解析路径的影响;这不是模型未收到文件,也不意味着所有内置智能体的专用解析器都有同一问题。
最小复现:给自定义 ACP 智能体发送文字和一个文件,等待回复,再重新读取会话。原始 prompt 例如:
[ {"type":"text","text":"Review this file"}, {"type":"resource","resource":{"uri":"attachment:///note.txt","mimeType":"text/plain","blob":"aGVsbG8="}} ]修复前
acp_native::prompt_blocks只处理原生图片或顶层text,会跳过嵌套的resource和resource_link;只有附件的消息可能变成空消息。session/load的user_message_chunk重放也只取文字,会丢掉普通二进制资源和用户图片。修复方案
[uri](uri)标记,文件链接保留[name](uri),与现有实时用户消息的轻量表示一致;不把文件正文或 base64 二进制展开到聊天文字中。不修改数据库 schema、原始 transcript、请求协议或任何客户端。已有原始记录可在重新解析时恢复标记;本 PR 不新增附件下载/持久文件管理能力,也不改变智能体会话的上下文恢复机制。
验证
新增回归覆盖混合文件/图片的历史读取、通知重放一致性、回显去重、只有附件及畸形/空资源、旧图片 MIME 字段。测试使用合成资料,不依赖真实模型或凭据。
已在 Linux 使用实际上游 crate 编译运行:
cargo test --manifest-path src-tauri/Cargo.toml --locked --no-default-features --lib parsers::acp_native::tests结果:20 passed,0 failed(包含 4 个新增回归测试);
git diff --check通过。未将该定向解析测试声明为 Web/移动端浏览器交互验收。