diff --git a/desktop/src/features/chats/lib/chatWorkAutomation.ts b/desktop/src/features/chats/lib/chatWorkAutomation.ts index a8e76af32..d9c753501 100644 --- a/desktop/src/features/chats/lib/chatWorkAutomation.ts +++ b/desktop/src/features/chats/lib/chatWorkAutomation.ts @@ -7,6 +7,17 @@ import * as React from "react"; const STORAGE_PREFIX = "buzz:chat-work-automation:v1"; const STORAGE_EVENT = "buzz:chat-work-automation-changed"; +/** + * Tag attached to automation-generated prompts (auto-fix CI, address + * comments). The message still reaches the agent like any user message, but + * the chat timeline renders only its activity — not the message bubble — so + * armed automation feels ambient instead of ventriloquized. + */ +export const CHAT_AUTOMATION_TAG: [string, string] = [ + "automation", + "work-panel", +]; + export type ChatWorkAutomation = { autoFixCi: boolean; addressComments: boolean; diff --git a/desktop/src/features/chats/ui/ChatDetail.tsx b/desktop/src/features/chats/ui/ChatDetail.tsx index 0338d6187..0fcf8c4f5 100644 --- a/desktop/src/features/chats/ui/ChatDetail.tsx +++ b/desktop/src/features/chats/ui/ChatDetail.tsx @@ -27,6 +27,7 @@ import { NO_PROJECT_SELECTION_ID, } from "@/features/chats/lib/chatSetup"; import { ChatActivityTranscript } from "@/features/chats/ui/ChatActivityTranscript"; +import { CHAT_AUTOMATION_TAG } from "@/features/chats/lib/chatWorkAutomation"; import { deriveBranchFromAgentMessages, deriveChatWorkBranch, @@ -562,6 +563,13 @@ export function ChatDetail({ "chat_context", "source", ); + // Automation prompts stay invisible: the agent's + // activity and reply anchor here, but no bubble. + const isAutomationMessage = eventHasTag( + message, + CHAT_AUTOMATION_TAG[0], + CHAT_AUTOMATION_TAG[1], + ); const isAgentMessage = defaultAgent?.pubkey != null && normalizePubkey(message.pubkey) === @@ -579,7 +587,7 @@ export function ChatDetail({ )} messageId={message.id} > - {isContextMessage ? ( + {isAutomationMessage ? null : isContextMessage ? ( ) : ( void onSend(content, [])} + onAutomationPrompt={(content) => + void onSend(content, [], [CHAT_AUTOMATION_TAG]) + } open={showWorkPanel} prHref={workPanelHref} projectPath={metadata?.projectPath ?? selectedProject?.path ?? null} diff --git a/desktop/tests/e2e/chats-first-message.spec.ts b/desktop/tests/e2e/chats-first-message.spec.ts index dc15e42dc..0c4935de8 100644 --- a/desktop/tests/e2e/chats-first-message.spec.ts +++ b/desktop/tests/e2e/chats-first-message.spec.ts @@ -187,6 +187,37 @@ test("first message in a new chat is sent and rendered", async ({ page }) => { await expect(page.getByTestId("automation-auto-fix-ci")).toBeVisible(); await expect(page.getByTestId("automation-address-comments")).toBeVisible(); + // Arming address-comments fires the automation prompt (2 open threads in + // the mock). The message goes out tagged — but no bubble renders: the + // automation stays invisible in the timeline. + await page.getByTestId("automation-address-comments").click(); + await expect + .poll(() => + page.evaluate(() => + ( + ( + window as Window & { + __BUZZ_E2E_COMMAND_PAYLOADS__?: Array<{ + command: string; + payload?: { content?: string; mediaTags?: string[][] }; + }>; + } + ).__BUZZ_E2E_COMMAND_PAYLOADS__ ?? [] + ).some( + (entry) => + entry.command === "send_channel_message" && + entry.payload?.content?.includes("unanswered review comments") && + entry.payload?.mediaTags?.some( + (tag) => tag[0] === "automation" && tag[1] === "work-panel", + ), + ), + ), + ) + .toBe(true); + await expect( + page.getByLabel("Chat messages").getByText("unanswered review comments"), + ).toHaveCount(0); + // The header's PR button toggles the panel. await page.getByTestId("toggle-work-panel").click(); await expect(workPanel).not.toBeVisible();