Files
coder/site/src/pages/AgentsPage/components/RightPanel/DesktopToolbar.stories.tsx
T
TJ ebf56ebd12 feat(site): desktop panel toolbar, zoom modes, and pop-out window (#25585)
Redesigns the agent desktop panel with a persistent toolbar, zoom modes,
and a detachable pop-out window.

## Changes

**Toolbar** (`DesktopToolbar`)
- Persistent top bar with right-aligned controls: Take/Release control,
Zoom toggle, Detach
- All buttons use consistent `subtle` variant with icon + label
- `h-8` height, `bg-surface-primary` background with bottom border

**Zoom modes**
- Defaults to fit-to-window (`scaleViewport = true`) so the full
1920x1080 desktop is visible
- Toggle to native 100% resolution via toolbar button or keyboard
shortcuts (`Ctrl+0` fit, `Ctrl+1` native)
- noVNC background color overridden from hardcoded `rgb(40,40,40)` to
`--surface-secondary` so letterbox margins match the app theme in light
and dark mode

**Pop-out window**
- New route at `/agents/:agentId/desktop` for a dedicated desktop window
- Opens via toolbar "Detach" button at 50% of screen size, centered
- BroadcastChannel coordination: sidebar shows placeholder with "Bring
back" button
- Closing the pop-out window automatically restores the sidebar panel

**Other**
- `useDesktopConnection` hook accepts `scaleViewport` option, synced to
the RFB instance via a secondary effect
- `DesktopPanelContext` extended with `agent` and `workspace` fields
- Replaces the previous hover-overlay take/release control UX with
toolbar buttons

> Generated by Coder Agents on behalf of @tracyjohnsonux
2026-05-28 09:26:05 -07:00

76 lines
2.0 KiB
TypeScript

import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, fn, userEvent, within } from "storybook/test";
import { DesktopToolbar } from "./DesktopToolbar";
const meta = {
title: "pages/AgentsPage/DesktopToolbar",
component: DesktopToolbar,
} satisfies Meta<typeof DesktopToolbar>;
export default meta;
type Story = StoryObj<typeof meta>;
export const ViewOnly: Story = {
args: {
scaleMode: "fit",
onScaleModeChange: fn(),
isControlling: false,
onTakeControl: fn(),
onReleaseControl: fn(),
onPopOut: fn(),
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const takeControl = canvas.getByText("Take control");
await userEvent.click(takeControl);
await expect(args.onTakeControl).toHaveBeenCalled();
const zoom = canvas.getByText("Zoom to 100%");
await userEvent.click(zoom);
await expect(args.onScaleModeChange).toHaveBeenCalledWith("native");
const detach = canvas.getByText("Detach");
await userEvent.click(detach);
await expect(args.onPopOut).toHaveBeenCalled();
},
};
export const Controlling: Story = {
args: {
...ViewOnly.args,
isControlling: true,
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const release = canvas.getByText("Release control");
await userEvent.click(release);
await expect(args.onReleaseControl).toHaveBeenCalled();
},
};
export const NativeZoom: Story = {
args: {
...ViewOnly.args,
scaleMode: "native",
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const zoom = canvas.getByText("Zoom to fit");
await userEvent.click(zoom);
await expect(args.onScaleModeChange).toHaveBeenCalledWith("fit");
},
};
export const PoppedOut: Story = {
args: {
...ViewOnly.args,
isPoppedOut: true,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Detach button should not render when popped out.
const detach = canvas.queryByText("Detach");
await expect(detach).toBeNull();
},
};