`buzz messages edit` silently deletes the message's attachments
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
## Summary
Editing a message from the CLI strips every attachment off it. `build_edit` emits only `h` and `e` tags, and the desktop's render/cache overlay reads an edit with zero `imeta` tags as "the new attachment set is empty." The CLI exits 0 and gives you no way to put them back.
Same data-loss bug PR #755 fixed for the desktop. The CLI was never brought along.
## Why it happens
`crates/buzz-sdk/src/builders.rs:377-389`:
```rust
pub fn build_edit(
channel_id: Uuid,
target_event_id: nostr::EventId,
new_content: &str,
) -> Result {
check_content(new_content, 64 * 1024)?;
let tags = vec![
tag(&["h", &channel_id.to_string()])?,
tag(&["e", &target_event_id.to_hex()])?,
];
Ok(EventBuilder::new(Kind::Custom(40003), new_content).tags(tags))
}
```
No media parameter, and no caller could supply one anyway. `buzz messages edit` takes `--event` and `--content` only (`crates/buzz-cli/src/lib.rs:413-423`) and reaches `build_edit` at `crates/buzz-cli/src/commands/messages.rs:713`.
The receiving side reads that absence as deletion. `desktop/src/features/messages/lib/applyEditTagOverlay.mjs:31-42`:
```js
export function applyEditTagOverlay(originalTags, editTags) {
if (!editTags) return originalTags;
const editEmoji = editTags.filter((t) => t[0] === "emoji");
const droppedFromOriginal =
editEmoji.length > 0
? (t) => t[0] !== "imeta" && t[0] !== "emoji"
: (t) => t[0] !== "imeta";
```
`imeta` is unconditionally dropped from the original and replaced by whatever the edit carries, which for a CLI edit is nothing. That behaviour is deliberate and pinned by name:
```js
test("edit with zero imeta tags strips all attachments; non-imeta original tags stay", ...)
```
`desktop/src/features/messages/lib/applyEditTagOverlay.test.mjs:52`
Compare the `emoji` branch two lines up: an edit that supplies no emoji tags *preserves* the original's, and the comment at `applyEditTagOverlay.mjs:16-25` explains why, that a tag-less edit "can come from an older build ... or another client that doesn't know this path." That reasoning applies to the CLI and `imeta` word for word. It just wasn't extended there.
The desktop's own builder does carry media:
```rust
pub fn build_message_edit(
channel_id: Uuid,
target_event_id: EventId,
content: &str,
media_tags: &[Vec],
custom_emoji_tags: &[Vec],
mentions: &[&str],
) -> Result {
```
`desktop/src-tauri/src/events.rs:410-417`, with `imeta_tags(media_tags, ...)` at `:425`.
## Impact
An agent fixing a typo in its own message deletes the screenshot it attached. `buzz` exits 0, nothing in the output mentions attachments, and there's no `buzz messages edit --media` to restore them. The only recovery is deleting the message and posting it again.
`VISION.md:163` says buzz-cli "mirrors and extends the MCP surface ... Agents can script the entire platform without a GUI," and `README.md:40` promises agents "the same affordances as a human teammate." A human editing in the desktop keeps their attachments. An agent editing over the CLI loses them.
## Suggested fix
Fix it at the builder, not at the overlay. The overlay's replace-on-`imeta` semantics is what makes attachment removal work in the desktop, and changing it there would break `applyEditTagOverlay.test.mjs:52` for a good reason.
1. Add an options struct, or a `media_tags: &[Vec]` parameter, to `build_edit` in `crates/buzz-sdk/src/builders.rs`, validating the `imeta` prefix the way `desktop/src-tauri/src/events.rs:96-108` does.
2. Add `--media` to `MessagesCmd::Edit` (`crates/buzz-cli/src/lib.rs:413`), matching whatever flag `buzz messages send` already uses for attachments.
3. The default is the real question. The safe default is for `buzz messages edit` with no `--media` to fetch the target event and carry its existing `imeta` forward, so today's invocations stop losing data, with an explicit `--no-media` to clear. The alternative, requiring `--media` explicitly and dropping otherwise, is easier to reason about but doesn't help anyone who isn't already paying attention. I'd go with carry-forward, but I can see the argument against putting a relay fetch in the CLI's synchronous edit path, so I'd rather agree on it before building.
The same argument applies more weakly to NIP-30 `emoji` tags, which `build_edit` also can't emit. Nothing is lost today because the overlay preserves those on empty, but it'd be worth adding in the same pass for symmetry.
`builders.rs:2171` and `:2182` already cover `build_edit` directly, so the tests have somewhere to go.
Happy to send the PR once you've picked a default.
Contributor guide
Research direction
Start with build_edit in crates/buzz-sdk/src/builders.rs:377-389, the MessagesCmd::Edit definition in crates/buzz-cli/src/lib.rs:413-423, and its caller at crates/buzz-cli/src/commands/messages.rs:713. Review the existing attachment flag and builders.rs tests at :2171 and :2182, then agree on carry-forward versus explicit clearing; done means CLI edits no longer unintentionally remove imeta tags and the behavior is covered by tests.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, rust
- Domain
- cli
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100