fix(web): convert provider-editor credential reads to TanStack Query#33173
Conversation
Replace imperative useCallback + useEffect secret reads with TQ useQuery hooks for both credential presence check and credentials list. Fixes: - Empty catch on secretsReadPost permanently sets hasStoredCredential=false on transient daemon errors (same class as WEB-2H) - No retry on either daemon call — transient 503/502 errors are fatal - No Sentry reporting on credential read failures Changes: - credentialPresenceQuery: TQ useQuery with shouldRetryDaemonError retry, isOrgReady gate, and bestEffort Sentry reporting - credentialsListQuery: TQ useQuery for available credentials list - Optimistic cache update after saving a key (queryClient.setQueryData) - Remove loadCredentialPresence/loadAvailableCredentials callbacks - Remove hasStoredCredential/isLoadingCredential/availableCredentials useState — derived from TQ query data instead - Remove imperative load calls from useEffect and onChange handlers — TQ auto-refetches when query keys change (credential ref, provider) Closes LUM-2205 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
There was a problem hiding this comment.
✦ APPROVE
Value: Closes the last instance of the imperative-daemon-read pattern in apps/web/src/domains/settings/ai/. Provider editor modal now matches the canonical TanStack Query shape established in PR #33165 (WEB-2H / LUM-2199) — same shouldRetryDaemonError predicate, same useIsOrgReady() gate, same bestEffort: true Sentry reporting. Transient daemon errors (503 boot, 502 proxy, 401 auth-race, 400-org-header hydration) no longer get latched into permanent hasStoredCredential = false / empty availableCredentials state.
What this does: Two useQuery conversions in provider-editor-modal.tsx. Imperative loadCredentialPresence + loadAvailableCredentials useCallbacks deleted; the matching useStates removed; void load*(v) calls in dropdown handlers removed (TQ auto-refetches when parsedCredRef query-key segments change). Save handler keeps its explicit try/catch with user-facing error string + optimistic setQueryData(credentialPresenceKey, true) + invalidateQueries(credentialsListKey). Single file, single commit, +112/-58.
Pattern verification vs LUM-2199 canonical
credentialPresenceQuery(L211–230): key[PROVIDER_CREDENTIAL_PRESENCE_QK, assistantId, parsedCredRef.service, parsedCredRef.field],enabled: !!assistantId && needsCredentialCheck && isOrgReady,retry: shouldRetryDaemonError,staleTime: 30_000. ThrowsApiError(response.status, extractErrorMessage(...))on non-OK so the retry predicate sees a typed status. ✓credentialsListQuery(L253–272): key[PROVIDER_CREDENTIALS_LIST_QK, assistantId],enabled: !!assistantId && needsCredentialsList && isOrgReady,retry: shouldRetryDaemonError,staleTime: 30_000. Parses throughparseCredentialEntries(data!.secrets ?? data!.accounts ?? []). ✓- Sentry reporting (L232–239, L274–281): both wrap
captureError(query.error, { context: "settings-provider-editor-…", bestEffort: true })in auseEffectkeyed onquery.error. ✓ Matches the #33165 pattern — transient errors silently drop after retry-exhaust, non-transient still ship. - Derived state (L241–242, L282):
hasStoredCredential = credentialPresenceQuery.data ?? false;isLoadingCredential = credentialPresenceQuery.isLoading && needsCredentialCheck;availableCredentials = credentialsListQuery.data ?? []. NouseStateshadows. ✓ - Optimistic save (L407–409):
queryClient.setQueryData(credentialPresenceKey, true)+void queryClient.invalidateQueries({ queryKey: credentialsListKey })after a successfulsecretsPost. ✓
Anti-pattern grep on diff
- Empty
.catch(() => {}): 0 hits in added lines. The save handler'scatch { setError("Failed to save API key. Please try again."); return; }is a user-facing error string, not silent. - Runtime-boundary
ascasts: 0 new. Pre-existingas ServiceModeon localStorage read is untouched (parity with sibling cards). @ts-ignore/@ts-nocheck: 0.eslint-disable react-hooks/*: 0.|| 0fallback: 0.- Raw
Sentry.captureException: 0 — usescaptureErrorwrapper everywhere. - Dead-state cleanup:
loadCredentialPresenceandloadAvailableCredentialscallbacks have 0 hits at HEAD; matchinguseStates forhasStoredCredential/availableCredentials/isLoadingCredentialare gone (now derived).void load*(v)dropdown handlers removed. Clean migration. data!.found/data!.secrets: both guarded byif (!response.ok) throwimmediately prior. Same pattern as #33165 and the rest of the daemon SDK call sites.
Merge gate:
- Vex APPROVED at HEAD
b35860c5✓ - Codex 👍 on PR description at HEAD
b35860c5(01:16:52Z, ~2.5 min after PR open) ✓ - CI 7/7 green at HEAD ✓
- No outstanding REQUEST_CHANGES ✓
LUM-2199 follow-up confirmed closed — the "(separate PR pending)" note on #33165 lands here. Merging.
Prompt / plan
Same pattern as WEB-2H fix in PR #33165 — the provider editor modal has imperative
useCallback+useEffectsecret reads with an empty catch that permanently setshasStoredCredential = falseon any transient daemon error. No retry, no Sentry reporting.What changed
Converted both imperative daemon calls to TQ
useQueryhooks:1. Credential presence check (
secretsReadPost)credentialPresenceQuery: TQ query keyed on[assistantId, service, field]parsed from the credential referenceenabledgate:assistantId && authType === "api_key" && parsedCredRef && isOrgReadyretry: shouldRetryDaemonError— retries 503/502/401/400-org-header with exponential backoffbestEffort: trueSentry reporting when retries exhausthasStoredCredentialderived fromcredentialPresenceQuery.data ?? falseinstead of manual useState2. Available credentials list (
secretsGet)credentialsListQuery: TQ query keyed on[assistantId]isOrgReadygate, retry predicate, and Sentry reportingavailableCredentialsderived from query data instead of manual useState3. Eliminated manual plumbing
loadCredentialPresenceandloadAvailableCredentialsimperative callbackshasStoredCredential,isLoadingCredential,availableCredentialsuseState — all derived from TQ query statevoid loadCredentialPresence(v)from dropdown onChange handlers — TQ auto-refetches when credential ref (query key) changesqueryClient.setQueryData)Root cause analysis
bestEffortandshouldRetryDaemonErrorconventions. Imperative fetches with empty catches were the only pattern available.hasStoredCredential(false)fallback is silently wrong.web-search-card.tsx(WEB-2H / PR fix(web): root-cause fix for daemon query retries + TQ conversion (WEB-7, WEB-2H) #33165). This modal wasn't covered because it's lower traffic (user-initiated modal vs always-mounted card).shouldRetryDaemonErrorconvention is now documented. New daemon queries should use TQ with the retry predicate andisOrgReadygate.Closes LUM-2205
Related: PR #33165 (WEB-2H fix, same pattern in
web-search-card.tsx)Test plan
bunx tsc --noEmit— cleanbun run lint— cleanLink to Devin session: https://app.devin.ai/sessions/c2e17ff1867f4ebd90aac007ea0f5453
Requested by: @ashleeradka