Hide automation prompts from the chat timeline

The auto-fix-CI and address-comments nudges are sent as user messages
so the agent hears them through the normal pipeline — but rendering
them as the user's own bubbles broke the illusion. They now carry an
["automation", "work-panel"] tag (riding the outgoing tag path the
composer already uses) and the timeline renders only their anchored
activity: the agent's turn markers and reply appear with no visible
prompt, so armed automation reads as ambient.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
klopez4212
2026-07-07 07:51:57 +01:00
co-authored by Claude Fable 5
parent e63099c093
commit 47b67066c1
3 changed files with 54 additions and 2 deletions
@@ -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;
+12 -2
View File
@@ -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 ? (
<ChatContextRow event={message} />
) : (
<ChatMessageRow
@@ -676,7 +684,9 @@ export function ChatDetail({
<ChatWorkPanel
branch={workBranch}
chatId={chat.id}
onAutomationPrompt={(content) => void onSend(content, [])}
onAutomationPrompt={(content) =>
void onSend(content, [], [CHAT_AUTOMATION_TAG])
}
open={showWorkPanel}
prHref={workPanelHref}
projectPath={metadata?.projectPath ?? selectedProject?.path ?? null}
@@ -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();