Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
30 changes: 24 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,8 +905,16 @@ 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;
}

if (!bypassCache && resourceContentMap[uri] && !resourceErrorMap[uri]) {
Comment thread
olaservo marked this conversation as resolved.
Outdated
setResourceContent(resourceContentMap[uri]);
Comment thread
olaservo marked this conversation as resolved.
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.
Expand Down Expand Up @@ -931,12 +942,18 @@ const App = () => {
...prev,
[uri]: content,
}));
setResourceErrorMap((prev) => {
if (!(uri in prev)) return prev;
Comment thread
olaservo marked this conversation as resolved.
Outdated
const next = { ...prev };
delete next[uri];
return next;
});
} 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,
}));
Comment on lines 957 to 962
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

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

errorString uses nullish coalescing (??), so an Error with an empty message (e.g. new Error("")) will produce an empty string and render a blank error. Prefer a fallback that treats empty messages as missing (e.g. use || and/or include error.name).

Copilot uses AI. Check for mistakes.
} finally {
setFetchingResources((prev) => {
Expand Down Expand Up @@ -1482,9 +1499,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 +1607,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) {
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 (
Comment thread
olaservo marked this conversation as resolved.
<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 })
}
Comment thread
olaservo marked this conversation as resolved.
>
<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] || ""}
Comment thread
olaservo marked this conversation as resolved.
Outdated
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