Skip to content

Commit 0110a06

Browse files
cliffhallclaude
andcommitted
Reflect subscribed state in preview + compact subscribed tile labels
- Wire `isSubscribed` on the ReadResourceState passed to ResourcesScreen by deriving it from the live subscriptions list in App.tsx. The ResourcePreviewPanel's SubscribeButton already flips its label to "Unsubscribe" when subscribed; without this derivation isSubscribed was always false and the button looked stuck on "Subscribe". - In ResourceSubscribedItem, display only the last URI path segment (truncated with ellipsis if it still overflows) and surface the full URI via a hover tooltip. Keeps the Subscriptions pleat readable when resources have long path-style URIs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3e94585 commit 0110a06

3 files changed

Lines changed: 78 additions & 14 deletions

File tree

clients/web/src/App.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,22 @@ function App() {
562562
/* TODO: not wired yet */
563563
}, []);
564564

565+
// The Resources screen needs `isSubscribed` to flip the Subscribe button
566+
// label to "Unsubscribe". Derive it from the live subscriptions list rather
567+
// than threading it through every setReadResourceState site — that way the
568+
// button reflects state changes from any source (preview panel, subscribed
569+
// tile, or future server-initiated subscribe notifications).
570+
const effectiveReadResourceState = useMemo<
571+
ReadResourceState | undefined
572+
>(() => {
573+
if (!readResourceState) return undefined;
574+
if (!readResourceState.uri) return readResourceState;
575+
const isSubscribed = subscriptions.some(
576+
(s) => s.resource.uri === readResourceState.uri,
577+
);
578+
return { ...readResourceState, isSubscribed };
579+
}, [readResourceState, subscriptions]);
580+
565581
return (
566582
<InspectorView
567583
servers={servers}
@@ -580,7 +596,7 @@ function App() {
580596
history={messages}
581597
toolCallState={toolCallState}
582598
getPromptState={getPromptState}
583-
readResourceState={readResourceState}
599+
readResourceState={effectiveReadResourceState}
584600
currentLogLevel={currentLogLevel}
585601
sandboxPath={STUB_SANDBOX_PATH}
586602
bridgeFactory={stubBridgeFactory}

clients/web/src/components/groups/ResourceSubscribedItem/ResourceSubscribedItem.test.tsx

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,61 @@ import { renderWithMantine, screen } from "../../../test/renderWithMantine";
55
import { ResourceSubscribedItem } from "./ResourceSubscribedItem";
66

77
const subscription: InspectorResourceSubscription = {
8-
resource: { uri: "file:///x", name: "x" },
8+
resource: { uri: "file:///foo/bar/config.json", name: "config.json" },
99
lastUpdated: new Date("2024-01-01T12:00:00Z"),
1010
};
1111

1212
describe("ResourceSubscribedItem", () => {
13-
it("renders the resource name", () => {
13+
it("renders the last URI path segment, not the name or title", () => {
1414
renderWithMantine(
1515
<ResourceSubscribedItem
16-
subscription={{ resource: subscription.resource }}
16+
subscription={{
17+
resource: {
18+
uri: "file:///foo/bar/config.json",
19+
name: "ignored-name",
20+
title: "Ignored Title",
21+
},
22+
}}
1723
onUnsubscribe={() => {}}
1824
/>,
1925
);
20-
expect(screen.getByText("x")).toBeInTheDocument();
26+
expect(screen.getByText("config.json")).toBeInTheDocument();
27+
expect(screen.queryByText("ignored-name")).not.toBeInTheDocument();
28+
expect(screen.queryByText("Ignored Title")).not.toBeInTheDocument();
2129
});
2230

23-
it("prefers the resource title over the name", () => {
31+
it("falls back to the URI itself when it has no slash-separated segments", () => {
2432
renderWithMantine(
2533
<ResourceSubscribedItem
26-
subscription={{
27-
resource: { ...subscription.resource, title: "Display X" },
28-
}}
34+
subscription={{ resource: { uri: "opaque", name: "n" } }}
35+
onUnsubscribe={() => {}}
36+
/>,
37+
);
38+
expect(screen.getByText("opaque")).toBeInTheDocument();
39+
});
40+
41+
it("ignores trailing slashes when picking the last segment", () => {
42+
renderWithMantine(
43+
<ResourceSubscribedItem
44+
subscription={{ resource: { uri: "file:///foo/bar/", name: "n" } }}
2945
onUnsubscribe={() => {}}
3046
/>,
3147
);
32-
expect(screen.getByText("Display X")).toBeInTheDocument();
48+
expect(screen.getByText("bar")).toBeInTheDocument();
49+
});
50+
51+
it("shows the full URI in a tooltip on hover", async () => {
52+
const user = userEvent.setup();
53+
renderWithMantine(
54+
<ResourceSubscribedItem
55+
subscription={subscription}
56+
onUnsubscribe={() => {}}
57+
/>,
58+
);
59+
await user.hover(screen.getByText("config.json"));
60+
expect(
61+
await screen.findByText("file:///foo/bar/config.json"),
62+
).toBeInTheDocument();
3363
});
3464

3565
it("renders the last updated timestamp when present", () => {

clients/web/src/components/groups/ResourceSubscribedItem/ResourceSubscribedItem.tsx

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Button, Group, Stack, Text } from "@mantine/core";
1+
import { Button, Group, Stack, Text, Tooltip } from "@mantine/core";
22
import type { InspectorResourceSubscription } from "../../../../../../core/mcp/types.js";
33

44
export interface ResourceSubscribedItemProps {
@@ -9,6 +9,7 @@ export interface ResourceSubscribedItemProps {
99
const NameText = Text.withProps({
1010
size: "sm",
1111
fw: 500,
12+
truncate: "end",
1213
});
1314

1415
const TimestampText = Text.withProps({
@@ -24,25 +25,42 @@ const SubtleButton = Button.withProps({
2425
const ItemRow = Group.withProps({
2526
justify: "space-between",
2627
wrap: "nowrap",
28+
gap: "xs",
29+
});
30+
31+
const NameStack = Stack.withProps({
32+
gap: 2,
33+
flex: 1,
34+
miw: 0,
2735
});
2836

2937
function formatLastUpdated(date: Date): string {
3038
return date.toLocaleString();
3139
}
3240

41+
// Strip the URI down to its last non-empty path segment so the tile shows
42+
// a compact label (e.g. `file:///foo/bar/config.json` → `config.json`).
43+
// The full URI is restored via a tooltip on hover.
44+
function lastUriSegment(uri: string): string {
45+
const segments = uri.split("/").filter(Boolean);
46+
return segments[segments.length - 1] ?? uri;
47+
}
48+
3349
export function ResourceSubscribedItem({
3450
subscription,
3551
onUnsubscribe,
3652
}: ResourceSubscribedItemProps) {
3753
const { resource, lastUpdated } = subscription;
3854
return (
3955
<ItemRow>
40-
<Stack gap={2}>
41-
<NameText>{resource.title ?? resource.name}</NameText>
56+
<NameStack>
57+
<Tooltip label={resource.uri} withinPortal>
58+
<NameText>{lastUriSegment(resource.uri)}</NameText>
59+
</Tooltip>
4260
{lastUpdated && (
4361
<TimestampText>{formatLastUpdated(lastUpdated)}</TimestampText>
4462
)}
45-
</Stack>
63+
</NameStack>
4664
<SubtleButton onClick={onUnsubscribe}>Unsubscribe</SubtleButton>
4765
</ItemRow>
4866
);

0 commit comments

Comments
 (0)