Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 29 additions & 6 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ const App = () => {
const [resourceContentMap, setResourceContentMap] = useState<
Record<string, string>
>({});
const [resourceErrorMap, setResourceErrorMap] = useState<
Record<string, string>
>({});
const [fetchingResources, setFetchingResources] = useState<Set<string>>(
new Set(),
);
Expand Down Expand Up @@ -902,13 +905,32 @@ const App = () => {
setPromptContent(JSON.stringify(response, null, 2));
};

const readResource = async (uri: string) => {
if (fetchingResources.has(uri) || resourceContentMap[uri]) {
const readResource = async (
uri: string,
{ bypassCache = false }: { bypassCache?: boolean } = {},
) => {
if (fetchingResources.has(uri)) {
return;
}

const hasOwn = Object.prototype.hasOwnProperty;
if (
!bypassCache &&
hasOwn.call(resourceContentMap, uri) &&
!hasOwn.call(resourceErrorMap, uri)
) {
setResourceContent(resourceContentMap[uri]);
return;
}

Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because resourceErrorMap[uri] is only cleared on success, any retry attempt will continue to have an error entry until the request completes. Since some UI paths render resourceError in preference to cached content, this can leave a stale error visible while the retry is in-flight. Consider clearing resourceErrorMap[uri] when starting a new read (after passing the cache check / before sending the request), not only when a read succeeds.

Suggested change
setResourceErrorMap((prev) => {
if (!hasOwn.call(prev, uri)) return prev;
const next = { ...prev };
delete next[uri];
return next;
});

Copilot uses AI. Check for mistakes.
console.log("[App] Reading resource:", uri);
setFetchingResources((prev) => new Set(prev).add(uri));
setResourceErrorMap((prev) => {
if (!hasOwn.call(prev, uri)) return prev;
const next = { ...prev };
delete next[uri];
return next;
});
lastToolCallOriginTabRef.current = currentTabRef.current;

try {
Expand All @@ -934,9 +956,9 @@ const App = () => {
} catch (error) {
console.error(`[App] Failed to read resource ${uri}:`, error);
const errorString = (error as Error).message ?? String(error);
setResourceContentMap((prev) => ({
setResourceErrorMap((prev) => ({
...prev,
[uri]: JSON.stringify({ error: errorString }),
[uri]: errorString,
}));
} finally {
setFetchingResources((prev) => {
Expand Down Expand Up @@ -1482,9 +1504,9 @@ const App = () => {
setResourceTemplates([]);
setNextResourceTemplateCursor(undefined);
}}
readResource={(uri) => {
readResource={(uri, options) => {
clearError("resources");
readResource(uri);
readResource(uri, options);
}}
selectedResource={selectedResource}
setSelectedResource={(resource) => {
Expand Down Expand Up @@ -1590,6 +1612,7 @@ const App = () => {
nextCursor={nextToolCursor}
error={errors.tools}
resourceContent={resourceContentMap}
resourceError={resourceErrorMap}
onReadResource={(uri: string) => {
clearError("resources");
readResource(uri);
Expand Down
28 changes: 22 additions & 6 deletions client/src/components/ResourceLinkView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ interface ResourceLinkViewProps {
description?: string;
mimeType?: string;
resourceContent: string;
resourceError?: string;
onReadResource?: (uri: string) => void;
}

Expand All @@ -17,25 +18,40 @@ const ResourceLinkView = memo(
description,
mimeType,
resourceContent,
resourceError,
onReadResource,
}: ResourceLinkViewProps) => {
const [{ expanded, loading }, setState] = useState({
expanded: false,
loading: false,
});

const expandedContent = useMemo(
() =>
expanded && resourceContent ? (
const expandedContent = useMemo(() => {
if (!expanded) return null;
if (resourceError !== undefined) {
return (
<div className="mt-2">
<div className="flex justify-between items-center mb-1">
<span className="font-semibold text-red-600">Error:</span>
</div>
<div className="text-sm text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-950/30 p-2 rounded break-all">
{resourceError}
</div>
</div>
);
}
if (resourceContent) {
return (
<div className="mt-2">
<div className="flex justify-between items-center mb-1">
<span className="font-semibold text-green-600">Resource:</span>
</div>
<JsonView data={resourceContent} className="bg-background" />
</div>
) : null,
[expanded, resourceContent],
);
);
}
return null;
}, [expanded, resourceContent, resourceError]);

const handleClick = useCallback(() => {
if (!onReadResource) return;
Expand Down
6 changes: 4 additions & 2 deletions client/src/components/ResourcesTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const ResourcesTab = ({
clearResources: () => void;
listResourceTemplates: () => void;
clearResourceTemplates: () => void;
readResource: (uri: string) => void;
readResource: (uri: string, opts?: { bypassCache?: boolean }) => void;
selectedResource: Resource | null;
setSelectedResource: (resource: Resource | null) => void;
handleCompletion: (
Expand Down Expand Up @@ -229,7 +229,9 @@ const ResourcesTab = ({
<Button
variant="outline"
size="sm"
onClick={() => readResource(selectedResource.uri)}
onClick={() =>
readResource(selectedResource.uri, { bypassCache: true })
}
>
<RefreshCw className="w-4 h-4 mr-2" />
Refresh
Expand Down
3 changes: 3 additions & 0 deletions client/src/components/ToolResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface ToolResultsProps {
toolResult: CompatibilityCallToolResult | null;
selectedTool: Tool | null;
resourceContent: Record<string, string>;
resourceError?: Record<string, string>;
onReadResource?: (uri: string) => void;
isPollingTask?: boolean;
}
Expand Down Expand Up @@ -63,6 +64,7 @@ const ToolResults = ({
toolResult,
selectedTool,
resourceContent,
resourceError,
onReadResource,
isPollingTask,
}: ToolResultsProps) => {
Expand Down Expand Up @@ -228,6 +230,7 @@ const ToolResults = ({
description={item.description}
mimeType={item.mimeType}
resourceContent={resourceContent[item.uri] || ""}
resourceError={resourceError?.[item.uri]}
onReadResource={onReadResource}
/>
)}
Expand Down
3 changes: 3 additions & 0 deletions client/src/components/ToolsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ const ToolsTab = ({
nextCursor,
error,
resourceContent,
resourceError,
onReadResource,
serverSupportsTaskRequests,
}: {
Expand All @@ -195,6 +196,7 @@ const ToolsTab = ({
nextCursor: ListToolsResult["nextCursor"];
error: string | null;
resourceContent: Record<string, string>;
resourceError?: Record<string, string>;
onReadResource?: (uri: string) => void;
serverSupportsTaskRequests: boolean;
}) => {
Expand Down Expand Up @@ -884,6 +886,7 @@ const ToolsTab = ({
toolResult={toolResult}
selectedTool={selectedTool}
resourceContent={resourceContent}
resourceError={resourceError}
onReadResource={onReadResource}
isPollingTask={isPollingTask}
/>
Expand Down
28 changes: 28 additions & 0 deletions client/src/components/__tests__/ResourcesTab.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -305,4 +305,32 @@ describe("ResourcesTab - Template Query Parameters", () => {
),
).toBeInTheDocument();
});

it("should call readResource with bypassCache: true when Refresh is clicked", () => {
renderResourcesTab({
selectedResource: mockResource,
resourceContent: '{"users": []}',
});

const refreshButton = screen.getByText("Refresh");
fireEvent.click(refreshButton);

expect(mockReadResource).toHaveBeenCalledWith(mockResource.uri, {
bypassCache: true,
});
});

it("should call readResource without bypassCache when selecting a resource from the list", () => {
renderResourcesTab({
resources: [mockResource],
});

fireEvent.click(screen.getByText("Users Resource"));

expect(mockReadResource).toHaveBeenCalledWith(mockResource.uri);
expect(mockReadResource).not.toHaveBeenCalledWith(
mockResource.uri,
expect.objectContaining({ bypassCache: true }),
);
});
});
Loading