From 3dcf6b0a62acf0e2848e5a61ce83335eeda4e91d Mon Sep 17 00:00:00 2001 From: klopez4212 Date: Sun, 5 Jul 2026 08:45:07 +0100 Subject: [PATCH] Never hide a real agent reply behind narration heuristics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Turns visibly worked but never answered: two filters could swallow a genuinely sent reply. The chat timeline ran persisted agent messages through the transcript's internal-narration patterns, so replies like "Done! I've sent the summary…" were dropped outright; and the consecutive-run collapse preferred the last "human-facing" message, hiding a narration-styled FINAL reply behind an earlier one. Persisted messages now render on content alone (narration shaping stays in the activity transcript, dedup still applies), and the run collapse hides only interim narration — every substantive message stays, and the run's final message always stays regardless of phrasing. Regression spec: a narration-styled final reply must render alongside the earlier PR announcement. Co-Authored-By: Claude Fable 5 --- desktop/src/features/chats/lib/chatActivity.ts | 17 ++++++++++------- desktop/src/features/chats/ui/ChatDetail.tsx | 16 ++++++++-------- desktop/tests/e2e/chats-first-message.spec.ts | 15 +++++++++++++++ 3 files changed, 33 insertions(+), 15 deletions(-) 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]", {