Expose live resource subscriptions via a hook (#1325)#1326
Conversation
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>
|
@claude review |
|
Claude finished @cliffhall's task in 2m 0s —— View job Claude PR Review
Overall: clean, well-scoped, and consistent with the existing v2 state-manager/hook layering. The design mirrors Nothing I'd call a blocker. A handful of nits and two small correctness questions below. Notes1. if (!this.subscribedUris.includes(uri)) return;The comment says "the client already guards on subscribedResources before dispatching, but we re-check so out-of-order events can't resurrect a stale entry." Worth checking 2. Constructor does not prime from The state starts with this.subscribedUris = client.getSubscribedResources();
this.rebuild();Not required for this PR; flagging so future work on client reuse doesn't get a silent empty-list regression. 3. this.dispatchTypedEvent("subscriptionsChange", []);Functionally equivalent since 4. On This is consistent with the documented fallback for unknown URIs, but it may surprise users who subscribed when the resource was listed and then see the pretty name vanish after a list refresh. Two possible takes:
Either is defensible; the current choice is fine if it's deliberate. Worth a one-line rationale in the class doc comment if you keep it. 5. Subscription counts are tiny in practice so this is meaningless performance-wise. Only mentioning because the 6. Hook tests exercise the 7. VerdictShip it after deciding on the regression behavior in note 4 (keep or cache). The rest are nits that do not block merge. |
- 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>
|
Thanks for the review! Walking through each note: 1. Overstated hazard comment on 2. Prime from 3. 4. 5. 6. Hook test for state-prop transitioning to null — noted, thanks. 7. Hook result shape
|
|
@claude review |
|
Claude finished @cliffhall's task in 53s —— View job Claude finished @cliffhall's task — follow-up review
Follow-up reviewAll three in-scope fixes from the prior round landed correctly and the rationale for skipping 2, 5, 7 is sound. I re-read the full changeset cold and don't see anything new worth flagging. Ready to merge from my end. Verification of prior notes:
Fresh pass — nothing new I'd block on. Two very minor observations that you are welcome to ignore:
VerdictLGTM. Ship it. --- · Branch: |
- 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>
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>
- 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>
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>
- 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>
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>
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>
|
@claude review |
|
Claude finished @cliffhall's task in 2m 43s —— View job Claude PR Review (round 3)
LGTM in aggregate. The subscriptions hook work from the prior rounds is unchanged; the new material (markdown rendering, sized-to-content preview, template completions, auto-read, compact subscribed tile) is clean and consistent with the project's Mantine-first + theme-variant conventions. Three small correctness notes and some scope/nits below. ScopeThe PR title + body still describe only the subscriptions hook (#1325), but seven commits landed on top that expand into the resource preview/template UI. Worth either updating the PR description to reflect the full scope (so reviewers/future-you can see what actually shipped), or splitting into follow-up PRs. Not a blocker; just surfacing it so the commit history stays findable. Correctness notes1. Possible stale-read race in If the user clicks resource A, then clicks resource B before A's Easy guard: tag each call with a request-id ref and only commit 2. Completion "abort" is soft-abort only.
3. Fast-typing + pending fulfilled request ordering. Same file, same callback. Sequence: user types "a", 300ms debounce fires, Nits
TestsCoverage on the new surface looks solid. Missing but not critical: an explicit test for the VerdictShip it. The stale-read race in note 1 is the only thing I'd prioritize, and only as a follow-up — it's a corner case that's easy to hit now that sidebar click auto-reads. |
Summary
ResourceSubscriptionsState(core/mcp/state/resourceSubscriptionsState.ts): subscribes to the InspectorClient'sresourceSubscriptionsChange(URI list) andresourceUpdatedevents, resolves each URI against the optionalManagedResourcesStateso subscriptions carry the server-suppliedResource(name/title), stampslastUpdatedonnotifications/resources/updated, and clears on disconnect.useResourceSubscriptions(core/react/useResourceSubscriptions.ts): subscribes to the state manager and returns the liveInspectorResourceSubscription[].App.tsxto construct/destroy the state manager alongside the other managed stores and pass the hook's output toInspectorView(replacing thesubscriptions={[]}fallback added in Wire App.tsx to v2 core/ hook layer; remove demo stub from InspectorView #1244).Closes #1325.
Test plan
npm run validate(format, lint, build, unit + coverage gate)npm run test:integration(379 passed)npm run test:storybook(300 passed)🤖 Generated with Claude Code