* Expose live resource subscriptions via a hook (#1325) Add ResourceSubscriptionsState that mirrors InspectorClient's subscribed URIs as InspectorResourceSubscription[], resolving each URI against the managed resources list (so the subscription tile shows server-supplied name/title) and stamping lastUpdated on notifications/resources/updated. Pair it with useResourceSubscriptions and wire App.tsx so the Resources screen reflects subscribe/unsubscribe actions in real time. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Address review notes on resourceSubscriptionsState - Soften the onResourceUpdated comment: the client's dispatch is already guarded by subscribedResources.has(uri), so the re-check is true defense-in-depth rather than guarding a known hazard. - Use this.getSubscriptions() in the statusChange handler so every emit goes through the defensive-copy path. - Document the deliberate fallback to a synthetic Resource when a previously-listed resource is removed from the managed list while the user is still subscribed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 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> * Auto-read resource on sidebar click Clicking a resource in the URIs accordion now triggers onReadResource in addition to setting the selection, so the preview panel jumps straight to the pending → ok render path. Removes the unreachable "Click to read this resource" placeholder, since the loader / preview / error states always cover the rendered output for a selected resource. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Render markdown resources via react-markdown + fix preview overflow - Add react-markdown / remark-gfm dependencies. - ContentViewer accepts an optional mimeType prop; when it matches text/markdown (or text/x-markdown), text blocks render via react-markdown wrapped in a `.markdown-content` container. Non-markdown text now uses the existing Code "wrapping" variant so long lines stay inside the panel. - ResourcePreviewPanel infers `text/markdown` from a `.md` / `.markdown` URI suffix when the server didn't supply a mimeType, and threads the effective mime through to ContentViewer per content item. - ResourcesScreen flex children get miw=0 so a long unwrappable line in the resource body can no longer push the preview past the viewport's right edge. - App.css gains a `.markdown-content` ruleset constraining nested pre, table, code, and img elements to the container's width. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Contain resource preview: sticky header/footer, scrollable body Restructure ResourcePreviewPanel into a fixed-height column with the resource title + URI pinned to the top and the timestamp / annotations / subscribe-refresh actions pinned to the bottom. The content viewer area in the middle now owns its own ScrollArea, so a long markdown body scrolls within the panel instead of pushing the subscribe button below the viewport. In ResourcesScreen, the selectedResource branch (and the template branch's right pane) now hosts the panel inside a PreviewPane Flex column with the screen's max-height, and renderReadState wraps in a FillDetailCard sized to fill that column. The legacy outer ScrollArea.Autosize is removed for these panes since scrolling is internal now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Auto-hide template after read; size preview to content with cap - After the user clicks Read Resource on the template form, handleReadResource now clears selectedTemplateUri so the screen swaps from the template form to the resource preview. Previously both panels stayed mounted side-by-side. - The preview no longer hard-fills the viewport. The Card uses a new "preview" theme variant (overflow: hidden) and is content-sized, capped at SCROLL_MAX_HEIGHT. ResourcePreviewPanel's flex column marks the header / meta / footer rows as `flex: 0 0 auto` and the ContentScroll as `flex: 0 1 auto` with `mih: 0`, so: - short content → card hugs it, footer sits right under the body - long content → card caps at viewport, ContentScroll shrinks and scrolls internally, footer stays pinned at the cap - ScrollArea / Group imports in ResourcesScreen pruned to match the simpler single-pane layout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Cap template form panel at 40% width A bare text input + Read Resource button stretched across the whole main content area looks awkward on wide displays. Apply maw=40% to the PreviewPane in the template branch so the form keeps a reasonable form-field width regardless of viewport. The preview branch is unaffected — resource bodies still get the full main area for content. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Live completion/complete suggestions on resource template inputs Wires the server's `completions` capability through to the resource template form so each variable's input becomes an Autocomplete that fires `completion/complete` (debounced, 300ms) on every keystroke and surfaces the returned values as a dropdown — mirroring v1's behavior. - core/mcp/inspectorClientProtocol.ts: surface getCompletions on the protocol so non-runtime callers (state managers, hooks, tests) can depend on it. FakeInspectorClient gains a vi.fn-backed stub. - ResourceTemplatePanel: accepts onCompleteArgument + completionsSupported. When both are present it renders Mantine Autocomplete instead of TextInput, debounces keystrokes via per-arg timers, aborts in-flight requests on the next keystroke, and disables client-side filtering (the server already filtered for the typed prefix). - ResourcesScreen / InspectorView: thread the props through; the screen-level callback re-injects the active template's URI as the `ref: "ref/resource"` so the panel-level callback stays ref-free. - App.tsx: wires onCompleteArgument to inspectorClient.getCompletions and derives completionsSupported from `capabilities?.completions`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
46 lines
1.4 KiB
TypeScript
46 lines
1.4 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import type {
|
|
ResourceSubscriptionsState,
|
|
ResourceSubscriptionsStateEventMap,
|
|
} from "../mcp/state/resourceSubscriptionsState.js";
|
|
import type { InspectorResourceSubscription } from "../mcp/types.js";
|
|
import type { TypedEventGeneric } from "../mcp/typedEventTarget.js";
|
|
|
|
export interface UseResourceSubscriptionsResult {
|
|
subscriptions: InspectorResourceSubscription[];
|
|
}
|
|
|
|
/**
|
|
* React hook that subscribes to ResourceSubscriptionsState and returns the
|
|
* current InspectorResourceSubscription[]. When the state is null (no active
|
|
* server), returns an empty array.
|
|
*/
|
|
export function useResourceSubscriptions(
|
|
state: ResourceSubscriptionsState | null,
|
|
): UseResourceSubscriptionsResult {
|
|
const [subscriptions, setSubscriptions] = useState<
|
|
InspectorResourceSubscription[]
|
|
>(state?.getSubscriptions() ?? []);
|
|
|
|
useEffect(() => {
|
|
if (!state) {
|
|
setSubscriptions([]);
|
|
return;
|
|
}
|
|
setSubscriptions(state.getSubscriptions());
|
|
const onSubscriptionsChange = (
|
|
event: TypedEventGeneric<
|
|
ResourceSubscriptionsStateEventMap,
|
|
"subscriptionsChange"
|
|
>,
|
|
) => {
|
|
setSubscriptions(event.detail);
|
|
};
|
|
state.addEventListener("subscriptionsChange", onSubscriptionsChange);
|
|
return () => {
|
|
state.removeEventListener("subscriptionsChange", onSubscriptionsChange);
|
|
};
|
|
}, [state]);
|
|
|
|
return { subscriptions };
|
|
}
|