mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): resolve activity feed showing channel UUID instead of message content (#2201)
This commit is contained in:
@@ -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({
|
||||
>
|
||||
<Markdown
|
||||
className={isCompactPreview ? "text-xs leading-4" : "leading-5"}
|
||||
content={preview || "Message content unavailable."}
|
||||
content={resolvedContent || "Message content unavailable."}
|
||||
/>
|
||||
{hasBubbleOverflow ? (
|
||||
<span
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
QueryClient,
|
||||
QueryClientProvider,
|
||||
QueryObserver,
|
||||
} from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import {
|
||||
resolveSentMessageBody,
|
||||
sentMessageBodyQueryOptions,
|
||||
shouldFetchSentMessage,
|
||||
useSentMessageBody,
|
||||
} from "./useSentMessageBody.ts";
|
||||
|
||||
const link = { channelId: "ch-1", messageId: "ev-1" };
|
||||
|
||||
test("shouldFetchSentMessage returns true when messageLink present and no inline content", () => {
|
||||
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\./);
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import test from "node:test";
|
||||
|
||||
import {
|
||||
classifyTool,
|
||||
extractSimpleEchoPipeContent,
|
||||
parseBuzzCliCommand,
|
||||
tokenizeShellCommand,
|
||||
} from "./agentSessionToolClassifier.ts";
|
||||
@@ -32,27 +31,98 @@ 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',
|
||||
);
|
||||
assert.equal(
|
||||
extractSimpleEchoPipeContent(tokens, tokens.indexOf("buzz")),
|
||||
"Done. Eat my shorts.",
|
||||
);
|
||||
});
|
||||
|
||||
test("parseBuzzCliCommand promotes buzz message sends to message descriptors", () => {
|
||||
test("parseBuzzCliCommand returns null preview for echo-piped stdin sends", () => {
|
||||
const descriptor = parseBuzzCliCommand(
|
||||
'echo "Permission wired" | buzz messages send --channel agents --content -',
|
||||
);
|
||||
|
||||
assert.equal(descriptor?.renderClass, "message");
|
||||
assert.equal(descriptor?.label, "Send Message");
|
||||
assert.equal(descriptor?.preview, "Permission wired");
|
||||
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 returns null preview for heredoc/cat stdin sends", () => {
|
||||
const descriptor = parseBuzzCliCommand(
|
||||
'buzz messages send --channel some-uuid --content "$(cat /tmp/file)"',
|
||||
);
|
||||
|
||||
assert.equal(descriptor?.renderClass, "message");
|
||||
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", () => {
|
||||
const descriptor = classifyTool({
|
||||
title: "load_skill",
|
||||
|
||||
@@ -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, unknown>): string | null {
|
||||
const content = getToolString(args, ["content", "message", "text", "body"]);
|
||||
if (content) return content;
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user