From 128a7c23e685cb70c44223b476effddc7768fffa Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Thu, 9 Apr 2026 13:55:40 +0200 Subject: [PATCH] feat(site): agents desktop recording thumbnail frontend (#24023) Frontend for https://github.com/coder/coder/pull/24022. From that PR's description: > The agents chat interface displays thumbnails for videos recorded by the computer use agent. Currently, to display a thumbnail, the frontend downloads the entire video and shows the first frame. #24022 adds a thumbnail file id to `wait_agent` tool results, and this PR displays it instead of fetching the entire video. --- site/.storybook/static/tiny-thumbnail.png | Bin 0 -> 69 bytes .../tools/RecordingPreview.stories.tsx | 55 ++++++++++++++++-- .../ChatElements/tools/RecordingPreview.tsx | 25 +++++--- .../ChatElements/tools/SubagentTool.tsx | 8 ++- .../components/ChatElements/tools/Tool.tsx | 2 + 5 files changed, 77 insertions(+), 13 deletions(-) create mode 100644 site/.storybook/static/tiny-thumbnail.png diff --git a/site/.storybook/static/tiny-thumbnail.png b/site/.storybook/static/tiny-thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..5e59e255cb2440b2c83cb6a64670ed7a41e9a7b9 GIT binary patch literal 69 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1SBVv2j2ryJf1F&Ar*6yGZHcw7?@ZX8P6K& RJ^+d{c)I$ztaD0e0sx#w4gCNB literal 0 HcmV?d00001 diff --git a/site/src/pages/AgentsPage/components/ChatElements/tools/RecordingPreview.stories.tsx b/site/src/pages/AgentsPage/components/ChatElements/tools/RecordingPreview.stories.tsx index 24cd6b96d0..06025adf64 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/tools/RecordingPreview.stories.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/tools/RecordingPreview.stories.tsx @@ -2,8 +2,9 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import { expect, fireEvent, userEvent, waitFor, within } from "storybook/test"; import { RecordingPreview } from "./RecordingPreview"; -// The file is stored in site/.storybook/static/tiny-recording.mp4. +// Static assets stored in site/.storybook/static/. const TINY_MP4 = "/tiny-recording.mp4"; +const TINY_THUMBNAIL = "/tiny-thumbnail.png"; const meta: Meta = { title: "pages/AgentsPage/ChatElements/tools/RecordingPreview", @@ -23,10 +24,13 @@ type Story = StoryObj; export const Default: Story = { args: { recordingFileId: "dummy-recording-id", + thumbnailFileId: "dummy-thumb-id", + thumbnailSrc: TINY_THUMBNAIL, src: TINY_MP4, }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); + expect(canvas.getByRole("img")).toBeInTheDocument(); expect( canvas.getByRole("button", { name: "View recording" }), ).toBeInTheDocument(); @@ -36,6 +40,8 @@ export const Default: Story = { export const LightboxOpen: Story = { args: { recordingFileId: "dummy-recording-id", + thumbnailFileId: "dummy-thumb-id", + thumbnailSrc: TINY_THUMBNAIL, src: TINY_MP4, }, play: async ({ canvasElement }) => { @@ -55,13 +61,14 @@ export const LightboxOpen: Story = { export const ThumbnailError: Story = { args: { recordingFileId: "dummy-recording-id", + thumbnailFileId: "bad-thumb-id", src: TINY_MP4, }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const video = canvasElement.querySelector("video"); - expect(video).not.toBeNull(); - fireEvent.error(video!); + const img = canvasElement.querySelector("img"); + expect(img).not.toBeNull(); + fireEvent.error(img!); await waitFor(() => { expect(canvas.getByText("Thumbnail unavailable")).toBeInTheDocument(); // The play button should still be available so the user can @@ -72,3 +79,43 @@ export const ThumbnailError: Story = { }); }, }; + +export const WithThumbnail: Story = { + args: { + recordingFileId: "rec-id", + thumbnailFileId: "thumb-id", + thumbnailSrc: TINY_THUMBNAIL, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const img = canvas.getByRole("img"); + expect(img).toBeInTheDocument(); + expect(img).toHaveAttribute("src", TINY_THUMBNAIL); + // No