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:
ss-dev-01
2026-08-24 11:18:24 -07:00
parent c91819706e
commit 5e3faf443a
2 changed files with 83 additions and 4 deletions

View File

@@ -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(

View File

@@ -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;