fix: surface list-fetch failures in paginated mode (#1998) - #2047
Merged
Conversation
#1954 wired the list-load error off the aggregate managed stores only. With `paginatedLists` on, those stores deliberately skip their all-page walk, so their error is permanently null — while the paged stores that actually drive the sidebar had no error state at all and `loadPage` had a try/finally with no catch. A failing list showed an empty panel with no alert and no Retry, and the connect-time `void loadPage(undefined)` left an unhandled rejection. - Give PagedTools/Prompts/ResourcesState observable error state: an `errorChange` event plus `getError()`, set in a new catch in `loadPage`, cleared on the next success and on disconnect. The rejection is still re-thrown, matching ManagedListState — callers' auth-recovery wrappers key off it to detect a 401. - Catch the connect-time load's rejection (`.catch(() => {})`) now that the failure is recorded as state rather than lost. - Select the error by mode in `usePaginatedList`, the same way `items` already is, and read it in App.tsx from the pagination model. - Rename `useManagedListError` to `useListError`: both store families expose an identical `errorChange`, so the hook declares that one-event contract itself instead of importing ManagedListEventMap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0177mPHAdECD18nLLCTwR5rh Signed-off-by: cliffhall <cliff@futurescale.com>
There was a problem hiding this comment.
Pull request overview
Fixes #1998 by surfacing paginated list-fetch failures in the existing alert and Retry UI.
Changes:
- Adds observable error state to paged tools, prompts, and resources stores.
- Selects managed or paged errors based on pagination mode.
- Prevents connect-time load failures from becoming unhandled rejections and adds comprehensive tests.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
core/react/usePagedTools.ts |
Exposes paged tools errors. |
core/react/usePagedResources.ts |
Exposes paged resources errors. |
core/react/usePagedPrompts.ts |
Exposes paged prompts errors. |
core/react/useManagedTools.ts |
Uses the generalized error hook. |
core/react/useManagedResourceTemplates.ts |
Uses the generalized error hook. |
core/react/useManagedResources.ts |
Uses the generalized error hook. |
core/react/useManagedPrompts.ts |
Uses the generalized error hook. |
core/react/useListError.ts |
Generalizes error subscriptions across list-store families. |
core/mcp/state/pagedToolsState.ts |
Records, emits, clears, and rethrows tools errors. |
core/mcp/state/pagedResourcesState.ts |
Records, emits, clears, and rethrows resources errors. |
core/mcp/state/pagedPromptsState.ts |
Records, emits, clears, and rethrows prompts errors. |
clients/web/src/test/core/react/usePagedTools.test.tsx |
Tests paged tools error propagation. |
clients/web/src/test/core/react/usePagedResources.test.tsx |
Tests paged resources error propagation. |
clients/web/src/test/core/react/usePagedPrompts.test.tsx |
Tests paged prompts error propagation. |
clients/web/src/test/core/react/useListError.test.tsx |
Updates generalized error-hook coverage. |
clients/web/src/test/core/mcp/state/pagedToolsState.test.ts |
Tests tools error lifecycle and connect failures. |
clients/web/src/test/core/mcp/state/pagedResourcesState.test.ts |
Tests resources error lifecycle and connect failures. |
clients/web/src/test/core/mcp/state/pagedPromptsState.test.ts |
Tests prompts error lifecycle and connect failures. |
clients/web/src/hooks/usePaginatedList.ts |
Selects errors from the active list source. |
clients/web/src/hooks/usePaginatedList.test.tsx |
Tests mode-aware error selection. |
clients/web/src/App.tsx |
Routes active paginated errors into list panels. |
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1998
With Fetch Lists One Page at a Time on, a failing
tools/list/prompts/list/resources/listshowed an empty sidebar with no alert and no Retry — the exact behavior #1954 set out to remove — and the connect-time load left an unhandled rejection.The cause is that #1954 wired the error off the aggregate stores only. In paginated mode those stores deliberately skip their all-page walk (
deferWhenPaginated), so their error is permanentlynull; the paged stores that actually drive the sidebar had no error state at all, andloadPage()had atry/finallywith nocatch.What changed
core/mcp/state/paged{Tools,Prompts,Resources}State.ts): anerrorChangeevent plusgetError(), set in a newcatchinloadPage, cleared on the next successful load and on disconnect. The rejection is still re-thrown, matchingManagedListState— the state drives the panel, while the rejection is what a caller's auth-recovery wrapper keys off to detect a 401.void this.loadPage(undefined)becamevoid this.loadPage(undefined).catch(() => {}). Not a swallow —loadPagehas already recorded the failure, and the panel renders it. (This is the same shapeManagedListState's connect handler already used, and it keeps Enable @typescript-eslint/no-floating-promises so an unhandled rejection can't fail the gate again #1959'sno-floating-promiseshonest.)usePaginatedListselects the error by mode, the same way it already selectsitems:error: paginated ? pagedError : managedError.App.tsxnow passestoolsPagination.error/promptsPagination.error/resourcesPagination.errorinstead of always reading the managed stores.useManagedListError→useListError(core/react/useListError.ts). Both store families expose the identicalerrorChange: Error | null, so the hook now serves both and no longer importsManagedListEventMap— it declares the one-event contract it actually needs. The fouruseManaged*hooks and the threeusePaged*hooks share it.Retry already does the right thing in paginated mode:
onRefreshToolsroutes tousePaginatedList.onRefresh, which reloads page 1 from the paged store.Screenshots
Paginated mode against a server whose
tools/listrejects with-32603.Before — empty panel, no alert, no Retry (and an unhandled rejection from the connect-time load):
After — the same alert + Retry the aggregate path has shown since #1954:
Tests
paged{Tools,Prompts,Resources}State.test.ts: records + dispatches + re-throws, wraps a non-Errorrejection, clears on the next success, clears on disconnect, and records a connect-time auto-load failure rather than floating it.usePaged{Tools,Prompts,Resources}.test.tsx: the hook surfaces and clears the store's error, and reportsnullfor a null store.usePaginatedList.test.tsx: reports the managed error in all-pages mode and the paged error in paginated mode.Deliberately out of scope
ManagedListState.refreshalso callsmarkResponseRejectedfor an SDK decode rejection, so the Protocol entry stops rendering a rejected result as a clean success. The paged stores don't, and this PR doesn't add it — that is #1953's surface, and the issue's suggested shape doesn't ask for it. Worth a follow-up issue if we want the two paths identical there.npm run cipasses locally.