From 32baeecec474b8862bce8af2cdf6836db4203d8b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 01:44:59 +0000 Subject: [PATCH 1/2] fix(ui): draw a refused list or related-list read as no-access, not a retryable failure A 403 list read no longer offers Retry; a refused related-list read draws a neutral status line with the list's no-access sentence instead of a red action-refusal line. Claude-Session: https://claude.ai/code/session_01Utp6CqyAqjJmMJkdAQoyqn Co-authored-by: Claude --- ui/packages/plugin-detail/src/RelatedList.tsx | 24 +++++-- .../RelatedList.readFailed-26415.test.tsx | 55 ++++++++++++++ ui/packages/plugin-list/src/ListView.tsx | 11 +-- ...ListView.errorStateComponent-7143.test.tsx | 2 +- .../ListView.forbiddenNoRetry.test.tsx | 71 +++++++++++++++++++ 5 files changed, 151 insertions(+), 12 deletions(-) create mode 100644 ui/packages/plugin-list/src/__tests__/ListView.forbiddenNoRetry.test.tsx diff --git a/ui/packages/plugin-detail/src/RelatedList.tsx b/ui/packages/plugin-detail/src/RelatedList.tsx index 8504bb7aa4a..6f2f777146a 100644 --- a/ui/packages/plugin-detail/src/RelatedList.tsx +++ b/ui/packages/plugin-detail/src/RelatedList.tsx @@ -577,15 +577,22 @@ function serverSentence(err: unknown): string | null { * the shared fixed sentence and never the server's text (a refused read does * not name what it cannot see), unless the producer marked a `userMessage` for * the reader; every other failure says the server's sentence when it gave one. - * Retry for every kind but `api-disabled`, whose verdict a retry cannot change - * — the ListView precedent. + * Retry for every kind but `api-disabled` and `forbidden`, whose verdicts a + * retry cannot change — the ListView precedent. + * + * A `forbidden` read is not a failure of anything the reader did: it is drawn + * as a quiet neutral line (`role="status"`, no destructive styling) and always + * says the list's no-access sentence. A producer's `userMessage` on a 403 is + * the generic action refusal ("...perform this action"), which reads as a + * rebuke to someone who only opened the record to read it. */ const RelatedListLoadFailed: React.FC<{ error: unknown; onRetry: () => void }> = ({ error, onRetry }) => { const { t } = useDetailTranslation(); const kind = classifyLoadError(error); const copy = LOAD_FAILED_COPY[kind]; + const refused = kind === 'forbidden'; const message = - declaredUserMessage(error) + (refused ? null : declaredUserMessage(error)) ?? (copy.message ? t(copy.message[0], { defaultValue: copy.message[1] }) : serverSentence(error) @@ -593,15 +600,18 @@ const RelatedListLoadFailed: React.FC<{ error: unknown; onRetry: () => void }> = const Icon = kind === 'network' || kind === 'rejected' ? AlertTriangle : ShieldAlert; return (
- + {t(copy.title[0], { defaultValue: copy.title[1] })} {message} - {kind !== 'api-disabled' && ( + {kind !== 'api-disabled' && !refused && (