Agent activity: posted message text is dropped when content contains a backtick or `$`
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
## Summary
`extractBuzzCliInlineContent` in the agent activity classifier throws away the text of a `buzz messages send` whenever the `--content` value contains a backtick or `$`. Agents post markdown by default (inline code spans are routine), so this is the common case rather than an edge case.
The dropped text is recoverable in the one surface that renders it today, but only by an event fetch, and only for sends that already completed successfully. Where the fetch cannot run, the posted text is simply not shown.
Found while capturing agent-activity screenshots: a seeded post with a markdown body produced a screenshot with none of the message in it, and the test still passed.
## Where
`desktop/src/features/agents/ui/agentSessionToolClassifier.ts`, line numbers at `origin/main` = `7a1b7d8e09f96c10b6617a66bb8589e9984ffb3c`:
```ts
// :453-461
function extractBuzzCliInlineContent(
tokens: string[],
range: BuzzCommandRange,
): string | null {
const content = getFlagValue(tokens, range.verbIndex + 1, "--content");
if (!content || content === "-") return null;
if (content.includes("$") || content.includes("`")) return null; // <-- here
return content;
}
```
That `null` becomes the descriptor's `preview` at `:367-378`.
## Measured
`parseBuzzCliCommand` driven directly via vite-node against the file above, command otherwise identical each time:
```
PLAIN PROSE preview="Confirmed the mismatch is on the emit side." object=
WITH BACKTICKS (double-quoted) preview=null object="messages"
WITH BACKTICKS (single-quoted) preview=null object="messages"
WITH DOLLAR preview=null object="messages"
REAL SUBSTITUTION $(cat ...) preview=null object="messages"
STDIN FORM --content - preview=null object="messages"
```
## The guard is right in intent, but quoting context is already gone
The `$`/backtick rejection came in with #2201 (`c5d00a358`, "resolve activity feed showing channel UUID instead of message content") to stop `--content "$(cat /tmp/file)"` and `"$MESSAGE"` from being displayed as if the unexpanded shell expression were the sent text. That intent is correct and its tests should stay — this is **not** a request to delete the guard.
The problem is that `tokenizeShellCommand` discards quoting, so the guard cannot tell the two apart. Rows 2 and 3 above are the evidence: a **single-quoted** literal backtick, which the shell never substitutes and which is safe to display verbatim, is rejected identically to a real substitution. Distinguishing them needs quote context preserved through tokenization, not a looser character test.
## What is actually user-visible on main today
I checked this rather than assuming, and the blast radius is narrower than the classifier defect suggests.
A `messages.send` gets `renderClass: "message"`, so `ToolItem.tsx:60` routes it to `CompactMessageSummary`, which calls `useSentMessageBody(messageLink, preview)`. That hook fetches the event by id when `preview === null` (`useSentMessageBody.ts`, `shouldFetchSentMessage`), so the text usually reappears. Measured against those two exported helpers:
```
completed+accepted send, backticked fetches=true -> body recovered by fetch
in-flight / failed send, backticked fetches=false -> renders "Message content unavailable."
in-flight / failed send, plain prose fetches=false -> renders the text fine
```
`getSentMessageLink` (`messageLinks.ts`) returns `null` unless the item is `completed`, not an error, and the result carries an `event_id`. So for an **in-flight or failed** send the fetch is disabled and the body falls back to `"Message content unavailable."` — even though the content was sitting in the command all along, and the identical send with plain prose shows its text. That asymmetry is the on-main user-visible bug.
Also note the fetch is a round-trip to recover text the command already contained, for every markdown post.
## Second, latent defect: the fallback noun
`buzzOperationObject` (`:433-434`) returns singular `"message"` only when `isBuzzMessageSend(operation)`, but that predicate (`:613-615`) matches only `send_message` / `messages_send` — the underscore MCP forms — while the CLI path builds `operation` as `messages.send` (dot, `:365`). The predicate therefore misses and the generic branch returns the plural group name, which is where the `object="messages"` in the measurements comes from.
This is **currently latent**: for `presentation === "message"`, `CompactMessageSummary` does not consume `descriptor.action`, and `"message"` is not in `GROUPING_ELIGIBLE_RENDER_CLASSES` so these items never collapse into a group summary. I could not find a path on main that shows it to a user. It matters because any surface that renders `action`/`preview` directly, without the bubble's fetch, gets a bare `Sent messages` with no message text — which is exactly what the unmerged agent-activity work-block rail does. Worth fixing alongside the guard so the two are not re-derived separately.
## The stdin form
`--content -` returns `null` at the same function, so the multiline form the platform docs recommend (`printf '...' | buzz messages send ... --content -`) has no inline text either. Here the content genuinely is not in the command, so that case needs the fetch path and not better parsing.
## Not fixing yet
Filed without a fix on purpose. The right shape depends on an open design question — whether a mid-turn relay post should be a rail step at all — so the fix for the presentation half should wait on that call. The classifier half (quote-aware tokenization, and the dot/underscore predicate) is independent of it.
Repro is deterministic: no relay and no timing involved.
Contributor guide
Research direction
Start in desktop/src/features/agents/ui/agentSessionToolClassifier.ts and inspect extractBuzzCliInlineContent, buzzOperationObject, and isBuzzMessageSend; reproduce the listed cases with vite-node. Keep real shell substitutions rejected while preserving safe quoted literals, retain the stdin fetch behavior, and verify the dot-form send uses the singular message noun without deciding the separate rail-presentation question.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100