diff --git a/site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.stories.tsx b/site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.stories.tsx index 0d004362de..4ad5f75ea2 100644 --- a/site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.stories.tsx +++ b/site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.stories.tsx @@ -218,17 +218,20 @@ const buildUserMessage = ({ text, files = [], createdAt = baseMessage.created_at, + sentAsGoal = false, }: { id?: number; text?: string; files?: TypesGen.ChatFilePart[]; createdAt?: string; + sentAsGoal?: boolean; }): TypesGen.ChatMessage => ({ ...baseMessage, created_at: createdAt, id, role: "user", content: [...(text ? [buildTextPart(text)] : []), ...files], + ...(sentAsGoal ? { sent_as_goal: true } : {}), }); const buildStoryArgs = (...messages: TypesGen.ChatMessage[]) => ({ @@ -1041,6 +1044,41 @@ export const UserMessageTextOnly: Story = { }, }; +/** Goal-sent user messages show a durable transcript marker. */ +export const UserMessageSentAsGoalMarker: Story = { + args: buildStoryArgs( + buildUserMessage({ + id: 1, + text: "Use this screenshot as the goal", + files: [buildInlineAttachmentPart("image/png", TEST_PNG_B64)], + sentAsGoal: true, + }), + { + ...baseMessage, + id: 2, + role: "assistant", + content: [{ type: "text", text: "I will pursue that goal." }], + }, + ), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const marker = canvas.getByTestId("sent-as-goal-marker"); + expect(marker).toBeVisible(); + expect(marker).toHaveTextContent("Sent as goal"); + expect(canvas.getAllByTestId("sent-as-goal-marker")).toHaveLength(1); + + const markerActionRow = marker.closest('[data-testid="message-actions"]'); + if (!(markerActionRow instanceof HTMLElement)) { + throw new Error("Sent as goal marker action row not found"); + } + expect( + within(markerActionRow).queryByRole("button", { + name: "Copy message", + }), + ).not.toBeInTheDocument(); + }, +}; + /** Assistant-side images go through BlockList, not the user path. */ export const AssistantMessageWithImage: Story = { args: { diff --git a/site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.tsx b/site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.tsx index 83024550f3..02da8401ce 100644 --- a/site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.tsx +++ b/site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.tsx @@ -1,4 +1,9 @@ -import { ChevronLeftIcon, ChevronRightIcon, PencilIcon } from "lucide-react"; +import { + ChevronLeftIcon, + ChevronRightIcon, + PencilIcon, + TargetIcon, +} from "lucide-react"; import { type FC, Fragment, @@ -80,6 +85,16 @@ const getChatMessageTextContent = ( return textContent.length > 0 ? textContent : undefined; }; +const SentAsGoalMarker: FC = () => ( +