Skip to content

Commit 6d75874

Browse files
cliffhallclaude
andcommitted
Add close button to pending and error preview states
The OK-state preview already had its X button (inside the panel component itself). Pending and error states were inline cards in renderReadState / renderPreview with no way to dismiss them, so a user who submitted bad input to a resource template or prompt was stuck staring at the error until they picked a different sidebar item. Adds a top-left CloseButton row to both states on both screens, wired to the same handleClosePreview handler — so the template form or argument form re-appears on close just like it does from the OK state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b1b541b commit 6d75874

4 files changed

Lines changed: 119 additions & 14 deletions

File tree

clients/web/src/components/screens/PromptsScreen/PromptsScreen.test.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,28 @@ describe("PromptsScreen", () => {
201201
expect(screen.getByPlaceholderText("Enter y...")).toHaveValue("");
202202
});
203203

204+
it("closing the preview from the error state brings the form back", async () => {
205+
const user = userEvent.setup();
206+
renderWithMantine(
207+
<PromptsScreen
208+
{...baseProps}
209+
getPromptState={{
210+
status: "error",
211+
promptName: "summarize",
212+
error: "Bad input",
213+
}}
214+
/>,
215+
);
216+
await user.click(screen.getByText("summarize"));
217+
await user.type(screen.getByPlaceholderText("Enter topic..."), "x");
218+
await user.click(screen.getByRole("button", { name: "Get Prompt" }));
219+
expect(screen.getByText("Prompt Error")).toBeInTheDocument();
220+
await user.click(screen.getByRole("button", { name: "Close messages" }));
221+
expect(
222+
screen.getByRole("button", { name: "Get Prompt" }),
223+
).toBeInTheDocument();
224+
});
225+
204226
it("closing the preview for an arg-bearing prompt brings the form back", async () => {
205227
const user = userEvent.setup();
206228
renderWithMantine(

clients/web/src/components/screens/PromptsScreen/PromptsScreen.tsx

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import { useState } from "react";
2-
import { Alert, Card, Flex, Loader, Stack, Text } from "@mantine/core";
2+
import {
3+
Alert,
4+
Card,
5+
CloseButton,
6+
Flex,
7+
Group,
8+
Loader,
9+
Stack,
10+
Text,
11+
} from "@mantine/core";
312
import type {
413
GetPromptResult,
514
Prompt,
@@ -166,19 +175,35 @@ export function PromptsScreen({
166175
if (getPromptState.status === "pending") {
167176
return (
168177
<PreviewCard>
169-
<Stack align="center" py="xl">
170-
<Loader size="sm" />
171-
<Text c="dimmed">Loading prompt...</Text>
178+
<Stack gap="md">
179+
<Group justify="flex-start">
180+
<CloseButton
181+
aria-label="Close messages"
182+
onClick={handleClosePreview}
183+
/>
184+
</Group>
185+
<Stack align="center" py="xl">
186+
<Loader size="sm" />
187+
<Text c="dimmed">Loading prompt...</Text>
188+
</Stack>
172189
</Stack>
173190
</PreviewCard>
174191
);
175192
}
176193
if (getPromptState.status === "error") {
177194
return (
178195
<PreviewCard>
179-
<Alert color="red" variant="light" title="Prompt Error">
180-
{getPromptState.error ?? "Failed to get prompt"}
181-
</Alert>
196+
<Stack gap="md">
197+
<Group justify="flex-start">
198+
<CloseButton
199+
aria-label="Close messages"
200+
onClick={handleClosePreview}
201+
/>
202+
</Group>
203+
<Alert color="red" variant="light" title="Prompt Error">
204+
{getPromptState.error ?? "Failed to get prompt"}
205+
</Alert>
206+
</Stack>
182207
</PreviewCard>
183208
);
184209
}

clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.test.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,39 @@ describe("ResourcesScreen", () => {
210210
).toBeInTheDocument();
211211
});
212212

213+
it("closing the preview from the error state returns to the template form", async () => {
214+
const user = userEvent.setup();
215+
const templates: ResourceTemplate[] = [
216+
{ uriTemplate: "demo://resource/dynamic/text/{id}", name: "Dynamic" },
217+
];
218+
const { rerender } = renderWithMantine(
219+
<ResourcesScreen {...baseProps} templates={templates} />,
220+
);
221+
await user.click(screen.getByText("Templates (1)"));
222+
await user.click(screen.getByText("Dynamic"));
223+
await user.type(screen.getByLabelText("id"), "asdf");
224+
await user.click(screen.getByRole("button", { name: "Read Resource" }));
225+
226+
// Server rejects the URI.
227+
rerender(
228+
<ResourcesScreen
229+
{...baseProps}
230+
templates={templates}
231+
readState={{
232+
status: "error",
233+
uri: "demo://resource/dynamic/text/asdf",
234+
error: "MCP error -32603: Unknown resource",
235+
}}
236+
/>,
237+
);
238+
expect(screen.getByText("Read Error")).toBeInTheDocument();
239+
await user.click(screen.getByRole("button", { name: "Close preview" }));
240+
// The template form is restored so the user can fix their input.
241+
expect(
242+
screen.getByRole("button", { name: "Read Resource" }),
243+
).toBeInTheDocument();
244+
});
245+
213246
it("closing the preview for a plain resource returns to the empty state", async () => {
214247
const user = userEvent.setup();
215248
renderWithMantine(

clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.tsx

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import { useState } from "react";
2-
import { Alert, Card, Flex, Loader, Stack, Text } from "@mantine/core";
2+
import {
3+
Alert,
4+
Card,
5+
CloseButton,
6+
Flex,
7+
Group,
8+
Loader,
9+
Stack,
10+
Text,
11+
} from "@mantine/core";
312
import type {
413
ReadResourceResult,
514
Resource,
@@ -174,9 +183,17 @@ export function ResourcesScreen({
174183
if (readState.status === "pending") {
175184
return (
176185
<PreviewCard>
177-
<Stack align="center" py="xl">
178-
<Loader size="sm" />
179-
<Text c="dimmed">Reading resource...</Text>
186+
<Stack gap="md">
187+
<Group justify="flex-start">
188+
<CloseButton
189+
aria-label="Close preview"
190+
onClick={handleClosePreview}
191+
/>
192+
</Group>
193+
<Stack align="center" py="xl">
194+
<Loader size="sm" />
195+
<Text c="dimmed">Reading resource...</Text>
196+
</Stack>
180197
</Stack>
181198
</PreviewCard>
182199
);
@@ -185,9 +202,17 @@ export function ResourcesScreen({
185202
if (readState.status === "error") {
186203
return (
187204
<PreviewCard>
188-
<Alert color="red" variant="light" title="Read Error">
189-
{readState.error ?? "Failed to read resource"}
190-
</Alert>
205+
<Stack gap="md">
206+
<Group justify="flex-start">
207+
<CloseButton
208+
aria-label="Close preview"
209+
onClick={handleClosePreview}
210+
/>
211+
</Group>
212+
<Alert color="red" variant="light" title="Read Error">
213+
{readState.error ?? "Failed to read resource"}
214+
</Alert>
215+
</Stack>
191216
</PreviewCard>
192217
);
193218
}

0 commit comments

Comments
 (0)