diff --git a/desktop/src/features/agents/ui/AgentSessionToolItem/CompactMessageSummary.tsx b/desktop/src/features/agents/ui/AgentSessionToolItem/CompactMessageSummary.tsx
index 89aeba920e..424837a068 100644
--- a/desktop/src/features/agents/ui/AgentSessionToolItem/CompactMessageSummary.tsx
+++ b/desktop/src/features/agents/ui/AgentSessionToolItem/CompactMessageSummary.tsx
@@ -13,6 +13,7 @@ import { useTranscriptBubbleOverflow } from "../activityRenderClasses/useTranscr
import { compactSummaryTone } from "./CompactToolSummaryRow";
import type { SentMessageLink } from "./messageLinks";
import { SentMessageContextDialog } from "./SentMessageContextDialog";
+import { useSentMessageBody } from "./useSentMessageBody";
export function CompactMessageSummary({
args,
@@ -46,6 +47,7 @@ export function CompactMessageSummary({
timestamp: string;
}) {
const [detailsOpen, setDetailsOpen] = React.useState(false);
+ const resolvedContent = useSentMessageBody(messageLink, preview);
const variant = useAgentSessionTranscriptVariant();
const { goChannel } = useAppNavigation();
const { openProfilePanel } = useProfilePanel();
@@ -159,7 +161,7 @@ export function CompactMessageSummary({
>
{hasBubbleOverflow ? (
{
+ assert.equal(shouldFetchSentMessage(link, null), true);
+});
+
+test("shouldFetchSentMessage returns false when inline content is present", () => {
+ assert.equal(shouldFetchSentMessage(link, "hello"), false);
+});
+
+test("shouldFetchSentMessage returns false when messageLink is null", () => {
+ assert.equal(shouldFetchSentMessage(null, null), false);
+});
+
+test("resolveSentMessageBody returns inline content over fetched content", () => {
+ assert.equal(
+ resolveSentMessageBody("inline text", "fetched text"),
+ "inline text",
+ );
+});
+
+test("resolveSentMessageBody returns fetched content when no inline content", () => {
+ assert.equal(resolveSentMessageBody(null, "fetched text"), "fetched text");
+});
+
+test("resolveSentMessageBody returns null when both inline and fetched are absent", () => {
+ assert.equal(resolveSentMessageBody(null, undefined), null);
+ assert.equal(resolveSentMessageBody(null, null), null);
+});
+
+// ── Integration: the real useQuery wiring, not just the pure helpers ──────────
+//
+// `sentMessageBodyQueryOptions` is the exact options object `useSentMessageBody`
+// hands to `useQuery`. Driving it through query-core's `QueryObserver` (what
+// `useQuery` constructs under the hood) exercises the production fetch/cache
+// wiring without a DOM — these tests fail if the hook stops calling the
+// injected fetcher, stops keying by `messageId`, or stops gating on
+// `enabled`.
+
+test("useSentMessageBody wiring fetches by messageId and resolves fetched content", async () => {
+ const queryClient = new QueryClient();
+ let calledWith = null;
+ const fetchEventById = async (eventId) => {
+ calledWith = eventId;
+ return { content: "hello from the event" };
+ };
+ const options = sentMessageBodyQueryOptions(link, null, fetchEventById);
+ const observer = new QueryObserver(queryClient, options);
+ const unsubscribe = observer.subscribe(() => {});
+ await new Promise((resolve) => setTimeout(resolve, 20));
+ unsubscribe();
+
+ assert.equal(calledWith, "ev-1");
+ assert.equal(
+ resolveSentMessageBody(null, observer.getCurrentResult().data?.content),
+ "hello from the event",
+ );
+});
+
+test("useSentMessageBody wiring never invokes the fetcher when inline content is present", async () => {
+ const queryClient = new QueryClient();
+ let called = false;
+ const fetchEventById = async () => {
+ called = true;
+ return { content: "unused" };
+ };
+ const options = sentMessageBodyQueryOptions(
+ link,
+ "inline text",
+ fetchEventById,
+ );
+ const observer = new QueryObserver(queryClient, options);
+ const unsubscribe = observer.subscribe(() => {});
+ await new Promise((resolve) => setTimeout(resolve, 20));
+ unsubscribe();
+
+ assert.equal(called, false);
+});
+
+test("useSentMessageBody wiring leaves data undefined on a rejected fetch (fetch-miss path)", async () => {
+ const queryClient = new QueryClient();
+ const fetchEventById = async () => {
+ throw new Error("relay miss");
+ };
+ const options = sentMessageBodyQueryOptions(link, null, fetchEventById);
+ const observer = new QueryObserver(queryClient, { ...options, retry: false });
+ const unsubscribe = observer.subscribe(() => {});
+ await new Promise((resolve) => setTimeout(resolve, 20));
+ unsubscribe();
+
+ assert.equal(observer.getCurrentResult().data, undefined);
+});
+
+// ── Render-level coverage ──────────────────────────────────────────────────
+//
+// Renders the hook through a real QueryClientProvider. Cache is pre-seeded
+// via `setQueryData` (equivalent to a settled fetch) for the render
+// assertion, since `renderToStaticMarkup` never commits and so never runs
+// the query's mount effect — the wiring tests above already prove the
+// fetcher is called through the real `useQuery` path.
+
+function SentMessageBodyProbe({ messageLink, inlineContent, fetchEventById }) {
+ const content = useSentMessageBody(
+ messageLink,
+ inlineContent,
+ fetchEventById,
+ );
+ return React.createElement(
+ "span",
+ null,
+ content ?? "Message content unavailable.",
+ );
+}
+
+function renderProbe(queryClient, props) {
+ return renderToStaticMarkup(
+ React.createElement(
+ QueryClientProvider,
+ { client: queryClient },
+ React.createElement(SentMessageBodyProbe, props),
+ ),
+ );
+}
+
+test("CompactMessageSummary renders fetched event content when a cached fetch has resolved", () => {
+ const queryClient = new QueryClient();
+ queryClient.setQueryData(["sent-message-body", link.messageId], {
+ content: "hello from the event",
+ });
+ const fetchEventById = async () => {
+ throw new Error("should not be called — cache hit");
+ };
+
+ const html = renderProbe(queryClient, {
+ messageLink: link,
+ inlineContent: null,
+ fetchEventById,
+ });
+
+ assert.match(html, /hello from the event/);
+});
+
+test("CompactMessageSummary renders the placeholder, never a raw flag value, on a fetch miss", () => {
+ const queryClient = new QueryClient();
+ const fetchEventById = async () => {
+ throw new Error("relay miss");
+ };
+
+ const html = renderProbe(queryClient, {
+ messageLink: link,
+ inlineContent: null,
+ fetchEventById,
+ });
+
+ assert.match(html, /Message content unavailable\./);
+});
diff --git a/desktop/src/features/agents/ui/AgentSessionToolItem/useSentMessageBody.ts b/desktop/src/features/agents/ui/AgentSessionToolItem/useSentMessageBody.ts
new file mode 100644
index 0000000000..e1b16dd862
--- /dev/null
+++ b/desktop/src/features/agents/ui/AgentSessionToolItem/useSentMessageBody.ts
@@ -0,0 +1,51 @@
+import { useQuery } from "@tanstack/react-query";
+
+import { getEventById } from "@/shared/api/tauri";
+import type { SentMessageLink } from "./messageLinks";
+
+export function shouldFetchSentMessage(
+ messageLink: SentMessageLink | null,
+ inlineContent: string | null,
+): boolean {
+ return messageLink !== null && inlineContent === null;
+}
+
+export function resolveSentMessageBody(
+ inlineContent: string | null,
+ fetchedContent: string | undefined | null,
+): string | null {
+ if (inlineContent) return inlineContent;
+ return fetchedContent ?? null;
+}
+
+/**
+ * Builds the exact options object `useSentMessageBody` passes to `useQuery`.
+ * Extracted so tests can drive the real fetch through query-core's
+ * `QueryObserver` (what `useQuery` constructs internally) without a DOM.
+ */
+export function sentMessageBodyQueryOptions(
+ messageLink: SentMessageLink | null,
+ inlineContent: string | null,
+ fetchEventById: (eventId: string) => Promise<{ content: string }>,
+) {
+ return {
+ queryKey: ["sent-message-body", messageLink?.messageId],
+ queryFn: () => fetchEventById(messageLink?.messageId ?? ""),
+ enabled: shouldFetchSentMessage(messageLink, inlineContent),
+ staleTime: Number.POSITIVE_INFINITY,
+ };
+}
+
+export function useSentMessageBody(
+ messageLink: SentMessageLink | null,
+ inlineContent: string | null,
+ fetchEventById: (
+ eventId: string,
+ ) => Promise<{ content: string }> = getEventById,
+): string | null {
+ const { data } = useQuery(
+ sentMessageBodyQueryOptions(messageLink, inlineContent, fetchEventById),
+ );
+
+ return resolveSentMessageBody(inlineContent, data?.content);
+}
diff --git a/desktop/src/features/agents/ui/agentSessionToolClassifier.test.mjs b/desktop/src/features/agents/ui/agentSessionToolClassifier.test.mjs
index 10efb79cb4..01a73c7698 100644
--- a/desktop/src/features/agents/ui/agentSessionToolClassifier.test.mjs
+++ b/desktop/src/features/agents/ui/agentSessionToolClassifier.test.mjs
@@ -3,7 +3,6 @@ import test from "node:test";
import {
classifyTool,
- extractSimpleEchoPipeContent,
parseBuzzCliCommand,
tokenizeShellCommand,
} from "./agentSessionToolClassifier.ts";
@@ -32,25 +31,96 @@ test("tokenizeShellCommand preserves quoted strings and command separators", ()
);
});
-test("extractSimpleEchoPipeContent reads the simple echo before a buzz pipe", () => {
- const tokens = tokenizeShellCommand(
- 'echo -n "Done. Eat my shorts." | buzz messages send --content - --channel agents',
+test("parseBuzzCliCommand returns null preview for echo-piped stdin sends", () => {
+ const descriptor = parseBuzzCliCommand(
+ 'echo "Permission wired" | buzz messages send --channel agents --content -',
);
- assert.equal(
- extractSimpleEchoPipeContent(tokens, tokens.indexOf("buzz")),
- "Done. Eat my shorts.",
+
+ assert.equal(descriptor?.renderClass, "message");
+ assert.equal(descriptor?.label, "Send Message");
+ assert.equal(descriptor?.preview, null);
+ assert.equal(descriptor?.operation, "messages.send");
+});
+
+test("parseBuzzCliCommand returns null preview for printf-piped stdin sends", () => {
+ const descriptor = parseBuzzCliCommand(
+ "printf 'hello\\n\\nworld\\n' | buzz messages send --channel a6e0737c-4205-4bcc-9741-2aad800e613f --content -",
);
+
+ assert.equal(descriptor?.renderClass, "message");
+ assert.equal(descriptor?.preview, null);
});
-test("parseBuzzCliCommand promotes buzz message sends to message descriptors", () => {
+test("parseBuzzCliCommand returns null preview for heredoc/cat stdin sends", () => {
const descriptor = parseBuzzCliCommand(
- 'echo "Permission wired" | buzz messages send --channel agents --content -',
+ 'buzz messages send --channel some-uuid --content "$(cat /tmp/file)"',
);
assert.equal(descriptor?.renderClass, "message");
- assert.equal(descriptor?.label, "Send Message");
- assert.equal(descriptor?.preview, "Permission wired");
- assert.equal(descriptor?.operation, "messages.send");
+ assert.equal(descriptor?.preview, null);
+});
+
+test("parseBuzzCliCommand returns null preview for --content with embedded command substitution", () => {
+ const descriptor = parseBuzzCliCommand(
+ 'buzz messages send --channel some-uuid --content "prefix $(cat /tmp/f)"',
+ );
+
+ assert.equal(descriptor?.renderClass, "message");
+ assert.equal(descriptor?.preview, null);
+});
+
+test("parseBuzzCliCommand returns null preview for --content with a bare variable", () => {
+ const descriptor = parseBuzzCliCommand(
+ 'buzz messages send --channel some-uuid --content "$MESSAGE"',
+ );
+
+ assert.equal(descriptor?.renderClass, "message");
+ assert.equal(descriptor?.preview, null);
+});
+
+test("parseBuzzCliCommand returns null preview for --content with a prefixed variable", () => {
+ const descriptor = parseBuzzCliCommand(
+ 'buzz messages send --channel some-uuid --content "prefix $MESSAGE"',
+ );
+
+ assert.equal(descriptor?.renderClass, "message");
+ assert.equal(descriptor?.preview, null);
+});
+
+test("parseBuzzCliCommand preserves inline --content for sends", () => {
+ const descriptor = parseBuzzCliCommand(
+ 'buzz messages send --channel agents --content "Hello from inline"',
+ );
+
+ assert.equal(descriptor?.renderClass, "message");
+ assert.equal(descriptor?.preview, "Hello from inline");
+});
+
+test("parseBuzzCliCommand preserves --content=inline for sends", () => {
+ const descriptor = parseBuzzCliCommand(
+ "buzz messages send --channel agents --content=Acknowledged",
+ );
+
+ assert.equal(descriptor?.renderClass, "message");
+ assert.equal(descriptor?.preview, "Acknowledged");
+});
+
+test("parseBuzzCliCommand never surfaces --channel as preview for sends", () => {
+ const commands = [
+ "printf 'msg' | buzz messages send --channel my-uuid --content -",
+ 'buzz messages send --channel my-uuid --content "$(cat /tmp/f)"',
+ "buzz messages send --channel my-uuid --content -",
+ ];
+
+ for (const cmd of commands) {
+ const descriptor = parseBuzzCliCommand(cmd);
+ assert.equal(descriptor?.renderClass, "message");
+ assert.notEqual(
+ descriptor?.preview,
+ "my-uuid",
+ `send preview leaked --channel for: ${cmd}`,
+ );
+ }
});
test("classifyTool promotes load_skill to skill-read descriptors", () => {
diff --git a/desktop/src/features/agents/ui/agentSessionToolClassifier.ts b/desktop/src/features/agents/ui/agentSessionToolClassifier.ts
index 67f6271344..10fb74b145 100644
--- a/desktop/src/features/agents/ui/agentSessionToolClassifier.ts
+++ b/desktop/src/features/agents/ui/agentSessionToolClassifier.ts
@@ -363,15 +363,13 @@ export function parseBuzzCliCommand(
const group = tokens[range.groupIndex];
const verb = tokens[range.verbIndex] ?? "run";
const operation = `${group}.${verb}`;
- const content =
- group === "messages" && verb === "send"
- ? extractBuzzCliSendMessageContent(tokens, range)
- : null;
- const preview = content ?? extractBuzzCliObjectPreview(tokens, range);
+ const isSend = group === "messages" && verb === "send";
+ const preview = isSend
+ ? extractBuzzCliInlineContent(tokens, range)
+ : extractBuzzCliObjectPreview(tokens, range);
const tone = buzzCliTone(group, verb);
return {
- renderClass:
- group === "messages" && verb === "send" ? "message" : "relay-op",
+ renderClass: isSend ? "message" : "relay-op",
label: titleForBuzzCli(group, verb),
preview,
action: actionForBuzzOperation(operation, preview, tone),
@@ -452,14 +450,14 @@ function buzzCliTone(group: string, verb: string): AgentActivityTone {
return "write";
}
-function extractBuzzCliSendMessageContent(
+function extractBuzzCliInlineContent(
tokens: string[],
range: BuzzCommandRange,
): string | null {
const content = getFlagValue(tokens, range.verbIndex + 1, "--content");
- if (!content) return null;
- if (content !== "-") return content;
- return extractSimpleEchoPipeContent(tokens, range.buzzIndex) ?? null;
+ if (!content || content === "-") return null;
+ if (content.includes("$") || content.includes("`")) return null;
+ return content;
}
function extractBuzzCliObjectPreview(
@@ -583,28 +581,6 @@ function getFlagValue(tokens: string[], start: number, flag: string) {
return null;
}
-export function extractSimpleEchoPipeContent(
- tokens: string[],
- buzzIndex: number,
-): string | null {
- const pipeIndex = tokens.lastIndexOf("|", buzzIndex);
- if (pipeIndex <= 0) return null;
- const echoStart = findSegmentStart(tokens, pipeIndex - 1);
- const leftSegment = tokens.slice(echoStart, pipeIndex);
- if (leftSegment[0] !== "echo") return null;
- const contentTokens = leftSegment
- .slice(1)
- .filter((token) => !token.startsWith("-"));
- return contentTokens.length > 0 ? contentTokens.join(" ") : null;
-}
-
-function findSegmentStart(tokens: string[], beforeIndex: number) {
- for (let i = beforeIndex; i >= 0; i--) {
- if (isCommandSeparator(tokens[i])) return i + 1;
- }
- return 0;
-}
-
function extractBuzzToolPreview(args: Record): string | null {
const content = getToolString(args, ["content", "message", "text", "body"]);
if (content) return content;
diff --git a/desktop/src/features/agents/ui/agentSessionToolSummary.test.mjs b/desktop/src/features/agents/ui/agentSessionToolSummary.test.mjs
index fab1f539ae..ca83ab8b43 100644
--- a/desktop/src/features/agents/ui/agentSessionToolSummary.test.mjs
+++ b/desktop/src/features/agents/ui/agentSessionToolSummary.test.mjs
@@ -56,7 +56,7 @@ test("buildCompactToolSummary treats buzz messages send commands as messages", (
assert.equal(summary.presentation, "message");
});
-test("buildCompactToolSummary extracts simple piped buzz message content", () => {
+test("buildCompactToolSummary returns null preview for piped stdin sends", () => {
const summary = buildCompactToolSummary(
makeTool({
toolName: "shell",
@@ -68,7 +68,7 @@ test("buildCompactToolSummary extracts simple piped buzz message content", () =>
);
assert.equal(summary.label, "Send Message");
- assert.equal(summary.preview, "hello from stdin");
+ assert.equal(summary.preview, null);
assert.equal(summary.presentation, "message");
});