diff --git a/desktop/src/features/chats/lib/chatWorkAutomation.ts b/desktop/src/features/chats/lib/chatWorkAutomation.ts
index 0035c5e9f..25e3127a0 100644
--- a/desktop/src/features/chats/lib/chatWorkAutomation.ts
+++ b/desktop/src/features/chats/lib/chatWorkAutomation.ts
@@ -18,6 +18,25 @@ export const CHAT_AUTOMATION_TAG: [string, string] = [
"work-panel",
];
+/** Tag for one automation prompt, carrying its kind for the marker row. */
+export function chatAutomationTag(kind: "ci" | "comments"): string[] {
+ return [...CHAT_AUTOMATION_TAG, kind];
+}
+
+/** Marker-row label for an automation message's tag. */
+export function chatAutomationLabel(
+ tag: readonly string[] | undefined,
+ agentName: string,
+) {
+ if (tag?.[2] === "ci") {
+ return `Asked ${agentName} to fix the CI failures`;
+ }
+ if (tag?.[2] === "comments") {
+ return `Asked ${agentName} to address the review comments`;
+ }
+ return `Sent ${agentName} automation instructions`;
+}
+
export type ChatWorkAutomation = {
autoFixCi: boolean;
addressComments: boolean;
diff --git a/desktop/src/features/chats/ui/ChatConversationRows.tsx b/desktop/src/features/chats/ui/ChatConversationRows.tsx
index a0ad5e838..96d6af663 100644
--- a/desktop/src/features/chats/ui/ChatConversationRows.tsx
+++ b/desktop/src/features/chats/ui/ChatConversationRows.tsx
@@ -1,6 +1,7 @@
import * as React from "react";
-import { Bot, FolderGit2, MessageCircle, Power } from "lucide-react";
+import { Bot, FolderGit2, MessageCircle, Power, Wand2 } from "lucide-react";
+import { chatAutomationLabel } from "@/features/chats/lib/chatWorkAutomation";
import { cleanAssistantMessageText } from "@/features/chats/ui/chatActivityText";
import type { UserProfileLookup } from "@/features/profile/lib/identity";
import type { RelayEvent } from "@/shared/api/types";
@@ -168,6 +169,42 @@ export function ChatScrollAnchor({
return null;
}
+/**
+ * Marker row for an invisible automation prompt (auto-fix CI / address
+ * comments): the instruction itself stays out of the timeline, but the spot
+ * where it fired needs an anchor — total invisibility made "Run now" feel
+ * like it did nothing. Expandable to the full instruction text.
+ */
+export function ChatAutomationRow({
+ agentName,
+ event,
+}: {
+ agentName: string;
+ event: RelayEvent;
+}) {
+ const tag = event.tags.find((candidate) => candidate[0] === "automation");
+ return (
+
+
+
+
+
+
+ {chatAutomationLabel(tag, agentName)}
+
+
+
+ {event.content}
+
+
+
+
+ );
+}
+
export function ChatContextRow({ event }: { event: RelayEvent }) {
const isProjectSetup = event.content.startsWith("Project setup");
const projectSetupContent = event.content
diff --git a/desktop/src/features/chats/ui/ChatDetail.tsx b/desktop/src/features/chats/ui/ChatDetail.tsx
index 73ec971ab..e45bdab92 100644
--- a/desktop/src/features/chats/ui/ChatDetail.tsx
+++ b/desktop/src/features/chats/ui/ChatDetail.tsx
@@ -27,7 +27,10 @@ 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 {
+ CHAT_AUTOMATION_TAG,
+ chatAutomationTag,
+} from "@/features/chats/lib/chatWorkAutomation";
import {
deriveBranchFromAgentMessages,
deriveChatWorkBranch,
@@ -38,6 +41,7 @@ import { isHumanFacingAssistantText } from "@/features/chats/ui/chatActivityText
import { entranceClassForCreatedAt } from "@/features/chats/ui/messageEntrance";
import {
AgentActivationCard,
+ ChatAutomationRow,
ChatContextRow,
ChatMessageRow,
ChatScrollAnchor,
@@ -501,6 +505,24 @@ export function ChatDetail({
setIsActivationPending(true);
onActivateAgent();
}, [onActivateAgent]);
+
+ // Automation prompts must not fail silently: surface send errors, and if
+ // the default agent is stopped, start it too — the backlog replay delivers
+ // the prompt once it connects.
+ const handleAutomationPrompt = React.useCallback(
+ (content: string, kind: "ci" | "comments") => {
+ onSend(content, [], [chatAutomationTag(kind)]).catch((error: unknown) => {
+ console.error("Failed to send automation prompt", error);
+ toast.error("Could not send the automation instructions");
+ });
+ if (defaultAgent != null && !isManagedAgentActive(defaultAgent)) {
+ setIsActivationPending(true);
+ onActivateAgent();
+ }
+ },
+ [defaultAgent, onActivateAgent, onSend],
+ );
+
React.useEffect(() => {
if (!isActivationPending) {
return;
@@ -637,7 +659,12 @@ export function ChatDetail({
)}
messageId={message.id}
>
- {isAutomationMessage ? null : isContextMessage ? (
+ {isAutomationMessage ? (
+
+ ) : isContextMessage ? (
) : (
- void onSend(content, [], [CHAT_AUTOMATION_TAG])
- }
+ onAutomationPrompt={handleAutomationPrompt}
open={showWorkPanel}
prHref={workPanelHref}
projectPath={metadata?.projectPath ?? selectedProject?.path ?? null}
diff --git a/desktop/src/features/chats/ui/ChatWorkPanel.tsx b/desktop/src/features/chats/ui/ChatWorkPanel.tsx
index 553b1b38d..574f6f7bc 100644
--- a/desktop/src/features/chats/ui/ChatWorkPanel.tsx
+++ b/desktop/src/features/chats/ui/ChatWorkPanel.tsx
@@ -68,7 +68,7 @@ export function ChatWorkPanel({
chatId: string;
/** Whether an agent turn is currently running in this chat. */
isTurnActive?: boolean;
- onAutomationPrompt?: (content: string) => void;
+ onAutomationPrompt?: (content: string, kind: "ci" | "comments") => void;
open?: boolean;
prHref?: string | null;
/** Project directory — enables PR discovery by branch via the git remote. */
@@ -146,6 +146,7 @@ export function ChatWorkPanel({
});
onAutomationPrompt(
`CI is failing on ${effectiveHref} (${checks.failed} of ${checks.total} checks). Investigate the failures and push fixes until the checks pass.`,
+ "ci",
);
// The prompt itself is invisible in the timeline — acknowledge it.
toast.success(`Asked ${agentName} to fix the CI failures`);
@@ -160,6 +161,7 @@ export function ChatWorkPanel({
});
onAutomationPrompt(
`There are unanswered review comments on ${effectiveHref}. Address each comment and its replies, push any needed changes, reply to the threads, and resolve every conversation that has been addressed.`,
+ "comments",
);
toast.success(`Asked ${agentName} to address the review comments`);
}, [agentName, chatId, effectiveHref, onAutomationPrompt, openThreads]);
diff --git a/desktop/src/shared/styles/globals/animations.css b/desktop/src/shared/styles/globals/animations.css
index 6c7daa537..b1efedbb2 100644
--- a/desktop/src/shared/styles/globals/animations.css
+++ b/desktop/src/shared/styles/globals/animations.css
@@ -246,6 +246,36 @@
--buzz-shimmer-highlight: color-mix(in oklch, hsl(var(--primary)) 60%, black);
}
+/* Glyph recoloring alone is too subtle at sidebar sizes — sweep a
+ translucent highlight WASH behind the text as well (same keyframes, so
+ band and wash move together). The wash is a soft primary tint the text
+ stays readable through, like a highlighter pass. */
+.buzz-shimmer-accent::after {
+ animation: buzz-shimmer var(--buzz-shimmer-duration) linear infinite;
+ background-image: linear-gradient(
+ 90deg,
+ transparent 0%,
+ transparent 40%,
+ hsl(var(--primary) / 0.28) 50%,
+ transparent 60%,
+ transparent 100%
+ );
+ background-repeat: no-repeat;
+ background-size: var(--buzz-shimmer-band) 100%;
+ border-radius: 0.25rem;
+ content: "";
+ inset: -0.125rem -0.25rem;
+ pointer-events: none;
+ position: absolute;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .buzz-shimmer-accent::after {
+ animation: none;
+ content: none;
+ }
+}
+
@media (prefers-reduced-motion: reduce) {
.buzz-shimmer,
.shimmer {
diff --git a/desktop/tests/e2e/chats-first-message.spec.ts b/desktop/tests/e2e/chats-first-message.spec.ts
index a45de1dfb..c44b4b2f4 100644
--- a/desktop/tests/e2e/chats-first-message.spec.ts
+++ b/desktop/tests/e2e/chats-first-message.spec.ts
@@ -214,9 +214,15 @@ test("first message in a new chat is sent and rendered", async ({ page }) => {
),
)
.toBe(true);
+ // The instruction renders as a collapsed marker row, not a bubble.
+ const automationRow = page.getByTestId("chat-automation-row");
+ await expect(automationRow).toBeVisible({ timeout: 10_000 });
+ await expect(automationRow).toContainText(
+ "Asked Fizz to address the review comments",
+ );
await expect(
page.getByLabel("Chat messages").getByText("unanswered review comments"),
- ).toHaveCount(0);
+ ).not.toBeVisible();
// Manual overrides: open comments expose "Run now"; green CI does not.
await expect(page.getByTestId("automation-run-comments-now")).toBeVisible();