mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
ebf56ebd12
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
76 lines
2.0 KiB
TypeScript
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();
|
|
},
|
|
};
|