>);
+
+ const user = userEvent.setup();
+ const { findByTestId, getByLabelText } = render();
+ await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
+ await user.selectOptions(getByLabelText("Settings Section"), "authentication");
+
+ const subscriptionCard = (await findByTestId("auth-provider-icon-anthropic-subscription")).closest(".auth-provider-card") as HTMLElement;
+ const alert = within(subscriptionCard).getByRole("alert");
+ const header = subscriptionCard.querySelector(".auth-provider-header");
+ expect(alert).toHaveClass("auth-provider-login-error");
+ expect(alert).toHaveTextContent("Re-login to restore model access");
+ expect(header).not.toContainElement(alert);
+ expect(header?.nextElementSibling).toBe(alert);
+ expect(subscriptionCard.querySelector(".auth-provider-actions")).toBeTruthy();
+ });
+
it("renders notification provider cards responsively on mobile", async () => {
mockSettingsViewport(true);
Object.defineProperty(window, "innerWidth", { configurable: true, value: 375 });
@@ -895,7 +930,12 @@ describe("SettingsModal mobile adaptations", () => {
// Custom Provider cards render outside .auth-panel-body and retain their mobile gutter.
expectMobileRule(css, ".auth-provider-card", "margin: 0 var(--space-sm) var(--space-sm);");
expectMobileRule(css, ".auth-provider-header", "padding: var(--space-sm);");
- expectMobileRule(css, ".auth-provider-header > div:not(.auth-provider-info):not(.auth-apikey-section)", "margin-left: auto;");
+ expectMobileRule(css, ".auth-provider-header > div:not(.auth-provider-info):not(.auth-apikey-section):not(.auth-provider-actions)", "margin-left: auto;");
+ expectMobileRule(css, ".auth-provider-header > .auth-provider-actions", "width: 100%;");
+ expectMobileRule(css, ".auth-provider-header > .auth-provider-actions", "flex-basis: 100%;");
+ expectMobileRule(css, ".auth-provider-header > .auth-provider-actions", "min-width: 0;");
+ expectMobileRule(css, ".auth-provider-header > .auth-provider-actions", "max-width: 100%;");
+ expectMobileRule(css, ".auth-provider-header > .auth-provider-actions", "margin-left: 0;");
expectMobileRule(css, ".auth-apikey-section", "align-items: flex-end;");
expectMobileRule(css, ".auth-apikey-input-row", "justify-content: flex-end;");
expectMobileRule(css, ".auth-apikey-input-row .btn", "margin-left: auto;");
diff --git a/packages/dashboard/app/components/settings/sections/AuthenticationSection.css b/packages/dashboard/app/components/settings/sections/AuthenticationSection.css
index 2d4c0f995d..95534a9365 100644
--- a/packages/dashboard/app/components/settings/sections/AuthenticationSection.css
+++ b/packages/dashboard/app/components/settings/sections/AuthenticationSection.css
@@ -10,8 +10,36 @@ Settings modal so account controls stay reachable without introducing a second c
.auth-pending-instance-field { display: flex; flex: 1 1 var(--space-2xl); flex-direction: column; gap: var(--space-xs); }
.auth-pending-instance-label { color: var(--text-muted); font-size: var(--font-size-xs); font-weight: 600; }
.auth-instance-row > span { flex: 1; min-width: 0; }
+
+/*
+FNXC:ProviderAuth 2026-08-15-22:08:
+OAuth loginError is a durable card banner, not an inline hint. A wrapping block
+with a defined width keeps the Anthropic expiry sentence inside the provider
+card on a phone-width Settings modal; an inline sized the actions
+flex item to min-content and painted a broken per-line border past the card.
+Inset matches .auth-provider-header so Login and the banner share one edge.
+*/
+.auth-provider-login-error {
+ display: block;
+ box-sizing: border-box;
+ width: auto;
+ max-width: 100%;
+ min-width: 0;
+ margin: 0 var(--space-md) var(--space-sm);
+ padding: var(--space-sm);
+ border: 1px solid var(--color-error);
+ border-radius: var(--radius-md);
+ background: color-mix(in srgb, var(--color-error) 10%, transparent);
+ color: var(--color-error);
+ font-size: var(--font-size-xs);
+ line-height: 1.4;
+ overflow-wrap: anywhere;
+ word-break: break-word;
+}
+
@media (max-width: 768px) {
.auth-instance-row, .auth-instance-pending { align-items: stretch; }
.auth-instance-row > .btn, .auth-instance-pending > .btn { flex: 1; }
.auth-pending-instance-field { flex-basis: 100%; }
+ .auth-provider-login-error { margin-inline: var(--space-sm); }
}
diff --git a/packages/dashboard/app/components/settings/sections/AuthenticationSection.tsx b/packages/dashboard/app/components/settings/sections/AuthenticationSection.tsx
index 13e49ffdf9..bd5967724b 100644
--- a/packages/dashboard/app/components/settings/sections/AuthenticationSection.tsx
+++ b/packages/dashboard/app/components/settings/sections/AuthenticationSection.tsx
@@ -274,10 +274,13 @@ export function AuthenticationSection({ auth, form, setForm }: AuthenticationSec
};
/*
FNXC:ProviderAuth 2026-07-14-15:54:
- Provider authentication failures must remain visible on the affected card. Toasts are transient and can fire while Settings is closed, so render the server's loginError beside the provider actions as the durable re-auth remediation.
+ Provider authentication failures must remain visible on the affected card. Toasts are transient and can fire while Settings is closed, so render the server's loginError as the durable re-auth remediation.
+
+ FNXC:ProviderAuth 2026-08-15-22:08:
+ The loginError used to sit inline beside Login as ``. On a narrow Settings card that flex item sized to the sentence's min-content width, so the Anthropic expiry copy overflowed the card and the leaked global `.form-error` border painted as a broken per-line box. Keep it a wrapping block under the header so the banner stays inside the card at every Settings width.
*/
const renderProviderAuthError = (provider: AuthProvider) => provider.loginError
- ? ({provider.loginError})
+ ? ({provider.loginError}
)
: null;
const renderApiKeySection = (provider: AuthProvider, selectedInstanceId?: string, pendingLabel?: string, isPending = false) => {
const instanceId = selectedInstanceId ?? provider.instanceId;
@@ -356,9 +359,10 @@ export function AuthenticationSection({ auth, form, setForm }: AuthenticationSec
{renderAnthropicPrecedenceBadge(provider)}
{provider.authenticated && provider.keyHint && ({t("settings.authentication.key", "Key: ")}{provider.keyHint})}
- {provider.type !== "api_key" && !hasMultipleInstances(provider) && {renderAuthenticatedOAuthActions(provider)}{renderProviderAuthError(provider)}
}
+ {provider.type !== "api_key" && !hasMultipleInstances(provider) && {renderAuthenticatedOAuthActions(provider)}
}
{providerSupportsApiKey(provider) && !hasMultipleInstances(provider) && renderApiKeySection(provider)}
+ {provider.type !== "api_key" && !hasMultipleInstances(provider) && renderProviderAuthError(provider)}
{renderInstanceControls(provider)}
))}
{renderAnthropicPrecedenceRow()}
@@ -378,16 +382,17 @@ export function AuthenticationSection({ auth, form, setForm }: AuthenticationSec
{provider.keyHint && ({t("settings.authentication.key", "Key: ")}{provider.keyHint})}
- {provider.type !== "api_key" && !hasMultipleInstances(provider) && {renderAvailableOAuthActions(provider)}{renderProviderAuthError(provider)}
}
+ {provider.type !== "api_key" && !hasMultipleInstances(provider) && {renderAvailableOAuthActions(provider)}
}
{providerSupportsApiKey(provider) && !hasMultipleInstances(provider) && renderApiKeySection(provider)}
+ {provider.type !== "api_key" && !hasMultipleInstances(provider) && renderProviderAuthError(provider)}
{renderInstanceControls(provider)}
))}
)}
)}
{/*
FNXC:SettingsHelp 2026-07-16-12:45:
- The provider cards' ``s stay inline: they are all live state (save progress, key errors, provider loginError, OpenCode refresh status) that must stay visible where the operator is acting. The two DESCRIPTIVE blurbs this section carried — the panel-level "changes take effect immediately" hint and the reopen-onboarding hint — moved behind the shared "?" affordance per the operator requirement that no inline description paragraphs remain in Settings.
+ Save-progress, API-key, and OpenCode refresh ``s stay inline on the control they describe. Provider loginError is a wrapping block banner under the card header so a long OAuth expiry message cannot overflow the card. The two DESCRIPTIVE blurbs this section carried — the panel-level "changes take effect immediately" hint and the reopen-onboarding hint — moved behind the shared "?" affordance per the operator requirement that no inline description paragraphs remain in Settings.
*/}
{onReopenOnboarding && (