mirror of
https://github.com/block/buzz.git
synced 2026-10-30 05:51:46 +00:00
fix(desktop): never show a trigger title as the prompt author
The conversation variant's author row resolved its name through
`resolveUserLabel({ fallbackName: item.title })`, whose last fallback is
the prompt item's title. That title describes the *trigger* that started
the turn — "@Mention", "Prompt", "Buzz event", or any title-cased Buzz
event kind — and is never an identity. On `default`/`compactPreview` it
only seeds avatar initials, so it was harmless; the conversation author
row promotes it to displayed text, so an unresolved sender was rendered
as if "@Mention" were their name.
Identity resolution for the displayed author now stops at the profile:
display name, then NIP-05 handle, then the truncated pubkey. A truncated
pubkey is terse but true, and it keeps the utterance attributed in a
full-cover view where an unlabelled bubble would read as orphaned. The
title stays available as trigger chrome in the footer.
The avatar seed keeps the old chain verbatim, so `default` and
`compactPreview` markup is untouched — the byte-for-byte fixture still
passes unchanged.
The existing author-row test was asserting the bug: its expectation came
from `item.title`, with no profile in the lookup. It now supplies a
profile and asserts the resolved display name, and the fixture's prompt
title is the realistic "@Mention" rather than a person's name so no test
can silently depend on a title reading as an identity again. Two new
tests cover the unresolved sender (truncated pubkey, and an explicit
assertion that no trigger word appears) and NIP-05-only resolution.
Non-vacuous: reverting the fix fails the unresolved-sender test — and
only that test — with `actual: '@Mention'`.
Verified at this tree: full desktop suite 5381 passing / 0 failing,
typecheck clean, `pnpm check` findings identical to main's baseline,
file-size ratchet clean. Transcript file also green under UTC/de-DE and
Asia/Tokyo/ja-JP.
Signed-off-by: ss-dev-01 <11939edb7df583f855dbef923f2358f1184538f88ca452e19e7e35e42ad6d796@buzz.block.builderlab.xyz>
Co-authored-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: Bradley Axen <baxen@squareup.com>
This commit is contained in:
@@ -139,6 +139,23 @@ const AGENT = {
|
||||
agentPubkey: "f".repeat(64),
|
||||
};
|
||||
const AUTHOR = "a".repeat(64);
|
||||
const AUTHOR_TRUNCATED = `${AUTHOR.slice(0, 8)}…${AUTHOR.slice(-4)}`;
|
||||
/**
|
||||
* What the transcript builder actually puts in a prompt item's `title`: a
|
||||
* description of the trigger that started the turn, not an identity. Real values
|
||||
* are "Prompt", "Buzz event", and title-cased event kinds like "@Mention"
|
||||
* (`agentSessionTranscriptHelpers.ts` `parsePromptText`). The author row must
|
||||
* never display this as a name.
|
||||
*/
|
||||
const TRIGGER_TITLE = "@Mention";
|
||||
const AUTHOR_PROFILES = {
|
||||
[AUTHOR]: {
|
||||
displayName: "Ada Lovelace",
|
||||
avatarUrl: null,
|
||||
nip05Handle: null,
|
||||
ownerPubkey: null,
|
||||
},
|
||||
};
|
||||
|
||||
function items() {
|
||||
const shared = { channelId: "chan-1", sessionId: "sess-1", turnId: "turn-1" };
|
||||
@@ -149,7 +166,7 @@ function items() {
|
||||
type: "message",
|
||||
renderClass: "message",
|
||||
role: "user",
|
||||
title: "Ada",
|
||||
title: TRIGGER_TITLE,
|
||||
text: "please summarize the plan",
|
||||
timestamp: "2026-06-14T19:00:00.000Z",
|
||||
messageId: "event-1",
|
||||
@@ -426,12 +443,14 @@ test("conversation marks the transcript container and centers a reading column",
|
||||
});
|
||||
|
||||
test("conversation renders the prompt as a filled right-aligned bubble with an author label", async () => {
|
||||
const { container } = await renderTranscript("conversation");
|
||||
const { container } = await renderTranscript("conversation", {
|
||||
profiles: AUTHOR_PROFILES,
|
||||
});
|
||||
const author = container.querySelector(
|
||||
'[data-testid="transcript-user-message-author"]',
|
||||
);
|
||||
assert.ok(author, "conversation should label the prompt author");
|
||||
assert.equal(author.textContent, "Ada");
|
||||
assert.equal(author.textContent, "Ada Lovelace");
|
||||
|
||||
const row = container.querySelector(
|
||||
'[data-testid="transcript-user-message"]',
|
||||
@@ -443,6 +462,48 @@ test("conversation renders the prompt as a filled right-aligned bubble with an a
|
||||
assert.doesNotMatch(bubble.className, /max-h-36/);
|
||||
});
|
||||
|
||||
test("conversation never shows the trigger title as the prompt author when the sender is unresolved", async () => {
|
||||
// Regression guard. The label chain's last fallback used to be the prompt
|
||||
// item's `title`, which is a description of the trigger ("@Mention",
|
||||
// "Prompt", "Buzz event") rather than an identity. On the other variants that
|
||||
// string only seeds avatar initials, so it was harmless; the conversation
|
||||
// author row displays it, so an unresolved sender was rendered as "@Mention"
|
||||
// — a trigger presented as a person's name. With no profile for the author,
|
||||
// the row must fall back to the truncated pubkey instead.
|
||||
const { container } = await renderTranscript("conversation");
|
||||
const author = container.querySelector(
|
||||
'[data-testid="transcript-user-message-author"]',
|
||||
);
|
||||
assert.ok(author, "the author row should still render without a profile");
|
||||
assert.equal(author.textContent, AUTHOR_TRUNCATED);
|
||||
assert.doesNotMatch(
|
||||
author.textContent,
|
||||
/@Mention|Prompt|Buzz event/,
|
||||
"the trigger title must never be displayed as an author name",
|
||||
);
|
||||
});
|
||||
|
||||
test("conversation prefers the resolved profile over both the trigger title and the pubkey", async () => {
|
||||
// The truncated-pubkey fallback must not shadow a real identity: with a
|
||||
// profile present the row shows the display name, and with only a NIP-05
|
||||
// handle it shows that.
|
||||
const { container } = await renderTranscript("conversation", {
|
||||
profiles: {
|
||||
[AUTHOR]: {
|
||||
displayName: null,
|
||||
avatarUrl: null,
|
||||
nip05Handle: "ada@example.com",
|
||||
ownerPubkey: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.equal(
|
||||
container.querySelector('[data-testid="transcript-user-message-author"]')
|
||||
.textContent,
|
||||
"ada@example.com",
|
||||
);
|
||||
});
|
||||
|
||||
test("conversation renders agent messages as unboxed prose at full fidelity", async () => {
|
||||
const { container } = await renderTranscript("conversation");
|
||||
const message = container.querySelector(
|
||||
|
||||
@@ -50,13 +50,31 @@ export function UserMessageBubble({
|
||||
const authorProfile = item.authorPubkey
|
||||
? profiles?.[item.authorPubkey.toLowerCase()]
|
||||
: null;
|
||||
const authorLabel = item.authorPubkey
|
||||
// The other variants seed the avatar from `resolveUserLabel(…, fallbackName:
|
||||
// item.title)`, whose last resort is the prompt item's title. That title
|
||||
// describes the *trigger* that started the turn ("@Mention", "Prompt", "Buzz
|
||||
// event"), never a person — harmless when it only picks avatar initials, so
|
||||
// this chain is left exactly as it was for `default`/`compactPreview`.
|
||||
const triggerSeededLabel = item.authorPubkey
|
||||
? resolveUserLabel({
|
||||
pubkey: item.authorPubkey,
|
||||
fallbackName: item.title,
|
||||
profiles,
|
||||
})
|
||||
: item.title || "User";
|
||||
// Focus mode promotes the author to displayed text, where a trigger title
|
||||
// would read as a false identity — an unresolved sender showing up as
|
||||
// "@Mention". Identity resolution here therefore stops at the profile
|
||||
// (display name, then NIP-05 handle, then the truncated pubkey): a truncated
|
||||
// pubkey is a real, if terse, identity, and it keeps the utterance attributed
|
||||
// in a full-cover view. `item.title` stays available as trigger chrome in the
|
||||
// footer; it is never a name. Only when the item carries no author at all does
|
||||
// the row fall back to a generic placeholder.
|
||||
const authorLabel = isConversation
|
||||
? item.authorPubkey
|
||||
? resolveUserLabel({ pubkey: item.authorPubkey, profiles })
|
||||
: "User"
|
||||
: triggerSeededLabel;
|
||||
const handleBubbleClick = React.useCallback(
|
||||
(event: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!messageLink || isNestedInteractiveTarget(event)) return;
|
||||
|
||||
Reference in New Issue
Block a user