mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Make Run now trustworthy and the sidebar shimmer unmissable
Run now: the toast only proved the click happened — the send's failure was swallowed, the prompt was fully invisible, and a stopped default agent silently ate it. Automation prompts now surface send errors, auto-start a stopped default agent (the backlog replay delivers the prompt once it connects), and leave a collapsed marker row at the spot the instruction fired — "Asked Fizz to fix the CI failures" — expandable to the full text, so the magic has an anchor. Shimmer: glyph recoloring alone is too subtle at sidebar sizes. The accent shimmer adds a translucent primary highlight wash sweeping behind the text on the same keyframes — a highlighter pass the text stays readable through. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
09b61fbc85
commit
73340ac6ce
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
<Message side="center">
|
||||
<MessageContent className="w-full max-w-full">
|
||||
<details
|
||||
className="group/automation-row"
|
||||
data-testid="chat-automation-row"
|
||||
>
|
||||
<summary className="flex cursor-pointer list-none items-center gap-2 text-sm text-muted-foreground/70 [&::-webkit-details-marker]:hidden">
|
||||
<Wand2 className="h-3.5 w-3.5 shrink-0" />
|
||||
<span className="min-w-0 truncate">
|
||||
{chatAutomationLabel(tag, agentName)}
|
||||
</span>
|
||||
</summary>
|
||||
<div className="mt-2 pl-6 text-sm text-muted-foreground">
|
||||
{event.content}
|
||||
</div>
|
||||
</details>
|
||||
</MessageContent>
|
||||
</Message>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChatContextRow({ event }: { event: RelayEvent }) {
|
||||
const isProjectSetup = event.content.startsWith("Project setup");
|
||||
const projectSetupContent = event.content
|
||||
|
||||
@@ -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 ? (
|
||||
<ChatAutomationRow
|
||||
agentName={defaultAgent?.name ?? "Fizz"}
|
||||
event={message}
|
||||
/>
|
||||
) : isContextMessage ? (
|
||||
<ChatContextRow event={message} />
|
||||
) : (
|
||||
<ChatMessageRow
|
||||
@@ -738,9 +765,7 @@ export function ChatDetail({
|
||||
branch={workBranch}
|
||||
chatId={chat.id}
|
||||
isTurnActive={isChatTurnActive}
|
||||
onAutomationPrompt={(content) =>
|
||||
void onSend(content, [], [CHAT_AUTOMATION_TAG])
|
||||
}
|
||||
onAutomationPrompt={handleAutomationPrompt}
|
||||
open={showWorkPanel}
|
||||
prHref={workPanelHref}
|
||||
projectPath={metadata?.projectPath ?? selectedProject?.path ?? null}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user