diff --git a/desktop/src/features/chats/lib/chatActivity.ts b/desktop/src/features/chats/lib/chatActivity.ts index e8ffb1636..44c1eeaf5 100644 --- a/desktop/src/features/chats/lib/chatActivity.ts +++ b/desktop/src/features/chats/lib/chatActivity.ts @@ -159,14 +159,17 @@ function addIntermediateAgentTurnMessageIds({ return; } - const keepMessage = - [...agentRun] - .reverse() - .find((message) => isHumanFacingAssistantText(message.content)) ?? - agentRun[agentRun.length - 1]; - + // Hide only interim narration: substantive messages (PR announcements, + // answers) all stay, and the run's FINAL message always stays even when + // narration-styled — a real reply must never lose to a phrasing + // heuristic, which is exactly how turns ended up visibly working but + // never answering. + const lastId = agentRun[agentRun.length - 1].id; for (const message of agentRun) { - if (message.id !== keepMessage.id) { + if ( + message.id !== lastId && + !isHumanFacingAssistantText(message.content) + ) { hiddenIds.add(message.id); } } diff --git a/desktop/src/features/chats/ui/ChatDetail.tsx b/desktop/src/features/chats/ui/ChatDetail.tsx index e45bdab92..746bb415f 100644 --- a/desktop/src/features/chats/ui/ChatDetail.tsx +++ b/desktop/src/features/chats/ui/ChatDetail.tsx @@ -302,22 +302,22 @@ export function ChatDetail({ ) { return false; } - const isAgent = - defaultAgent?.pubkey != null && - normalizePubkey(message.pubkey) === - normalizePubkey(defaultAgent.pubkey); + // NOTE: no narration heuristics here. A persisted agent message was + // deliberately SENT to the channel — filtering it through the + // transcript's internal-narration patterns dropped real replies + // ("Done! I've sent the summary…"), leaving turns that visibly + // worked but never answered. Narration shaping belongs to the + // activity transcript; persisted rows only dedup against it. return ( (eventHasTag(message, "chat_context", "source") || - (isAgent - ? isHumanFacingAssistantText(message.content) - : message.content.trim().length > 0)) && + message.content.trim().length > 0) && !shouldHidePersistedAgentMessage({ event: message, hiddenAgentMessageIds: chatActivity.hiddenAgentMessageIds, }) ); }), - [chatActivity.hiddenAgentMessageIds, defaultAgent?.pubkey, messages], + [chatActivity.hiddenAgentMessageIds, messages], ); const hasTranscriptActivity = chatActivity.totalBlockCount > 0; diff --git a/desktop/tests/e2e/chats-first-message.spec.ts b/desktop/tests/e2e/chats-first-message.spec.ts index c44b4b2f4..133a39c78 100644 --- a/desktop/tests/e2e/chats-first-message.spec.ts +++ b/desktop/tests/e2e/chats-first-message.spec.ts @@ -118,6 +118,15 @@ test("first message in a new chat is sent and rendered", async ({ page }) => { createdAt: base + 2, pubkey: pubkey ?? undefined, }); + // A narration-styled FINAL reply must still render: persisted + // messages are never filtered by the transcript's narration + // heuristics (this exact phrasing used to be silently dropped). + win.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({ + channelName: "Hello Fizz, first message", + content: "Done! I've sent the summary with details to the channel.", + createdAt: base + 3, + pubkey: pubkey ?? undefined, + }); // A human message with a mention tag: @bob must render as a chip // (alice's pubkey is a mock profile fixture; bob's resolves via the // message's p tag). @@ -135,6 +144,12 @@ test("first message in a new chat is sent and rendered", async ({ page }) => { { pubkey: fizzPubkey }, ); + await expect( + page + .getByLabel("Chat messages") + .getByText("Done! I've sent the summary with details to the channel."), + ).toBeVisible({ timeout: 10_000 }); + // Mentions in chat messages render as chips, same as channels. await expect( page.getByLabel("Chat messages").locator("[data-mention]", {