From 4a7b6a888c643a6af4cc9fbc0ccb6b98bdfbbec6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 28 Mar 2026 00:56:24 +0000 Subject: [PATCH 1/6] Fix account secret detail routing Co-authored-by: Kent C. Dodds --- e2e/account-secrets.spec.ts | 80 +++++++++++++++++++ .../worker/client/routes/account-secrets.tsx | 10 ++- 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 e2e/account-secrets.spec.ts diff --git a/e2e/account-secrets.spec.ts b/e2e/account-secrets.spec.ts new file mode 100644 index 0000000000..785afc2358 --- /dev/null +++ b/e2e/account-secrets.spec.ts @@ -0,0 +1,80 @@ +import { expect, test } from './playwright-utils.ts' + +async function saveSecret( + page: Parameters< + Parameters[1] + >[0]['page'], + input: { + name: string + description: string + value: string + } +) { + const response = await page.request.post('/account/secrets.json', { + data: { + action: 'save', + name: input.name, + scope: 'user', + appId: null, + description: input.description, + value: input.value, + allowedHosts: [], + allowedCapabilities: [], + }, + headers: { 'Content-Type': 'application/json' }, + }) + expect(response.ok()).toBeTruthy() +} + +test('switching secrets updates detail view without a full reload', async ({ + page, + login, +}) => { + await login() + + const nonce = Date.now().toString(36) + const firstSecret = { + name: `secret-switch-a-${nonce}`, + description: `First router test secret ${nonce}`, + value: `value-a-${nonce}`, + } + const secondSecret = { + name: `secret-switch-b-${nonce}`, + description: `Second router test secret ${nonce}`, + value: `value-b-${nonce}`, + } + + await saveSecret(page, firstSecret) + await saveSecret(page, secondSecret) + + await page.goto(`/account/secrets/user/${firstSecret.name}`) + await expect( + page.getByRole('heading', { level: 2, name: firstSecret.name }), + ).toBeVisible() + await expect(page.getByLabel('Description')).toHaveValue(firstSecret.description) + + await page.evaluate(() => { + ;(window as typeof window & { __secretRouteMarker?: string }).__secretRouteMarker = + 'still-here' + }) + + await page.getByRole('button', { name: secondSecret.name }).click() + + await expect(page).toHaveURL( + new RegExp(`/account/secrets/user/${secondSecret.name}$`), + ) + await expect( + page.getByRole('heading', { level: 2, name: secondSecret.name }), + ).toBeVisible() + await expect(page.getByLabel('Description')).toHaveValue( + secondSecret.description, + ) + await expect(page.getByLabel('Secret value')).toHaveValue(secondSecret.value) + await expect( + page.evaluate( + () => + (window as typeof window & { __secretRouteMarker?: string }) + .__secretRouteMarker, + ), + ).resolves.toBe('still-here') +}) diff --git a/packages/worker/client/routes/account-secrets.tsx b/packages/worker/client/routes/account-secrets.tsx index d88e4be4d8..9d4086804e 100644 --- a/packages/worker/client/routes/account-secrets.tsx +++ b/packages/worker/client/routes/account-secrets.tsx @@ -4,7 +4,11 @@ import { parseAccountSecretId, parseAccountSecretPath, } from '@kody-internal/shared/account-secret-route.ts' -import { navigate, routerEvents } from '#client/client-router.tsx' +import { + listenToRouterNavigation, + navigate, + routerEvents, +} from '#client/client-router.tsx' import { createDoubleCheck } from '#client/double-check.ts' import { type AccountStatus, @@ -334,6 +338,10 @@ export function AccountSecretsRoute(handle: Handle) { const filterAppCombobox = TypeaheadCombobox(handle) const editorAppCombobox = TypeaheadCombobox(handle) + listenToRouterNavigation(handle, () => { + handle.update() + }) + function buildHrefWithUpdatedFilters( nextFilters: Partial, options?: { pathname?: string }, From 8271f76994e1842a83ad442377433edc7b6fd742 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 28 Mar 2026 01:21:27 +0000 Subject: [PATCH 2/6] Stabilize account secret detail refresh Co-authored-by: Kent C. Dodds --- e2e/account-secrets.spec.ts | 4 +- .../worker/client/routes/account-secrets.tsx | 46 +++++++++++++------ 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/e2e/account-secrets.spec.ts b/e2e/account-secrets.spec.ts index 785afc2358..8aac41da51 100644 --- a/e2e/account-secrets.spec.ts +++ b/e2e/account-secrets.spec.ts @@ -69,7 +69,9 @@ test('switching secrets updates detail view without a full reload', async ({ await expect(page.getByLabel('Description')).toHaveValue( secondSecret.description, ) - await expect(page.getByLabel('Secret value')).toHaveValue(secondSecret.value) + await expect( + page.getByPlaceholder('Enter the secret value').first(), + ).toHaveValue(secondSecret.value) await expect( page.evaluate( () => diff --git a/packages/worker/client/routes/account-secrets.tsx b/packages/worker/client/routes/account-secrets.tsx index 9d4086804e..3d11263c9f 100644 --- a/packages/worker/client/routes/account-secrets.tsx +++ b/packages/worker/client/routes/account-secrets.tsx @@ -5,7 +5,6 @@ import { parseAccountSecretPath, } from '@kody-internal/shared/account-secret-route.ts' import { - listenToRouterNavigation, navigate, routerEvents, } from '#client/client-router.tsx' @@ -333,15 +332,13 @@ export function AccountSecretsRoute(handle: Handle) { let submittingApprovalAction: ApprovalAction | null = null let saveState: 'idle' | 'saving' | 'deleting' = 'idle' let lastLoadedDataKey = '' + let loadingDataKey: string | null = null + let loadRequestId = 0 let showSecretValue = false const deleteSecretCheck = createDoubleCheck(handle) const filterAppCombobox = TypeaheadCombobox(handle) const editorAppCombobox = TypeaheadCombobox(handle) - listenToRouterNavigation(handle, () => { - handle.update() - }) - function buildHrefWithUpdatedFilters( nextFilters: Partial, options?: { pathname?: string }, @@ -401,11 +398,13 @@ export function AccountSecretsRoute(handle: Handle) { saveState = 'idle' } - async function loadAccountSecrets(signal: AbortSignal) { + async function loadAccountSecrets() { + const href = getCurrentHref() + const selection = getSelectionState(href) + const dataKey = getDataRefreshKey(href) + const requestId = ++loadRequestId + loadingDataKey = dataKey try { - const href = getCurrentHref() - const selection = getSelectionState(href) - lastLoadedDataKey = getDataRefreshKey(href) const requestUrl = new URL(accountSecretsApiPath, href) requestUrl.search = new URL(href).search if (selection.selectedSecretId) { @@ -417,9 +416,12 @@ export function AccountSecretsRoute(handle: Handle) { const response = await fetch(requestUrl.toString(), { headers: { Accept: 'application/json' }, credentials: 'include', - signal, }) - if (signal.aborted) return + if ( + requestId !== loadRequestId || + getDataRefreshKey(getCurrentHref()) !== dataKey + ) + return if (response.status === 401) { window.location.assign('/login') return @@ -430,14 +432,24 @@ export function AccountSecretsRoute(handle: Handle) { throw new Error('Unable to load your secrets.') } + lastLoadedDataKey = dataKey applyPayload(payload, selection, null) handle.update() } catch (error) { - if (signal.aborted) return + if ( + requestId !== loadRequestId || + getDataRefreshKey(getCurrentHref()) !== dataKey + ) + return + lastLoadedDataKey = dataKey status = 'error' message = error instanceof Error ? error.message : 'Unable to load your secrets.' handle.update() + } finally { + if (requestId === loadRequestId && loadingDataKey === dataKey) { + loadingDataKey = null + } } } @@ -686,10 +698,14 @@ export function AccountSecretsRoute(handle: Handle) { }, ...appOptions, ] + const currentDataKey = getDataRefreshKey(currentHref) const isRefreshingForLocationChange = - status !== 'loading' && - getDataRefreshKey(currentHref) !== lastLoadedDataKey - if (status === 'loading' || isRefreshingForLocationChange) { + status !== 'loading' && currentDataKey !== lastLoadedDataKey + const isLoadingCurrentLocation = loadingDataKey === currentDataKey + if ( + (status === 'loading' || isRefreshingForLocationChange) && + !isLoadingCurrentLocation + ) { handle.queueTask(loadAccountSecrets) } From f7176dd9f919622240a1135dce7072d71057ab09 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 28 Mar 2026 05:35:30 +0000 Subject: [PATCH 3/6] Do not mark failed secret loads as current Co-authored-by: Kent C. Dodds --- packages/worker/client/routes/account-secrets.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/worker/client/routes/account-secrets.tsx b/packages/worker/client/routes/account-secrets.tsx index 3d11263c9f..e565954de3 100644 --- a/packages/worker/client/routes/account-secrets.tsx +++ b/packages/worker/client/routes/account-secrets.tsx @@ -441,7 +441,6 @@ export function AccountSecretsRoute(handle: Handle) { getDataRefreshKey(getCurrentHref()) !== dataKey ) return - lastLoadedDataKey = dataKey status = 'error' message = error instanceof Error ? error.message : 'Unable to load your secrets.' From 7c8c7eb2e6c3b3b6f8ce5045abb81b6490b453f4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 28 Mar 2026 05:44:06 +0000 Subject: [PATCH 4/6] Prevent infinite retry on secrets load failure Co-authored-by: Kent C. Dodds --- packages/worker/client/routes/account-secrets.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/worker/client/routes/account-secrets.tsx b/packages/worker/client/routes/account-secrets.tsx index e565954de3..3d11263c9f 100644 --- a/packages/worker/client/routes/account-secrets.tsx +++ b/packages/worker/client/routes/account-secrets.tsx @@ -441,6 +441,7 @@ export function AccountSecretsRoute(handle: Handle) { getDataRefreshKey(getCurrentHref()) !== dataKey ) return + lastLoadedDataKey = dataKey status = 'error' message = error instanceof Error ? error.message : 'Unable to load your secrets.' From dbba38d91323c5cd59cd4d77567cc9662726f993 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 28 Mar 2026 05:59:53 +0000 Subject: [PATCH 5/6] Fix account secrets retry after load failure Co-authored-by: Kent C. Dodds --- .../worker/client/routes/account-secrets.tsx | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/worker/client/routes/account-secrets.tsx b/packages/worker/client/routes/account-secrets.tsx index 3d11263c9f..014391b254 100644 --- a/packages/worker/client/routes/account-secrets.tsx +++ b/packages/worker/client/routes/account-secrets.tsx @@ -332,8 +332,10 @@ export function AccountSecretsRoute(handle: Handle) { let submittingApprovalAction: ApprovalAction | null = null let saveState: 'idle' | 'saving' | 'deleting' = 'idle' let lastLoadedDataKey = '' + let lastFailedDataKey: string | null = null let loadingDataKey: string | null = null let loadRequestId = 0 + let retryTimeout: ReturnType | null = null let showSecretValue = false const deleteSecretCheck = createDoubleCheck(handle) const filterAppCombobox = TypeaheadCombobox(handle) @@ -433,6 +435,11 @@ export function AccountSecretsRoute(handle: Handle) { } lastLoadedDataKey = dataKey + lastFailedDataKey = null + if (retryTimeout) { + clearTimeout(retryTimeout) + retryTimeout = null + } applyPayload(payload, selection, null) handle.update() } catch (error) { @@ -441,11 +448,20 @@ export function AccountSecretsRoute(handle: Handle) { getDataRefreshKey(getCurrentHref()) !== dataKey ) return - lastLoadedDataKey = dataKey + lastFailedDataKey = dataKey status = 'error' message = error instanceof Error ? error.message : 'Unable to load your secrets.' handle.update() + if (!retryTimeout && typeof window !== 'undefined') { + retryTimeout = window.setTimeout(() => { + retryTimeout = null + if (lastFailedDataKey !== dataKey) return + if (getDataRefreshKey(getCurrentHref()) !== dataKey) return + lastFailedDataKey = null + handle.update() + }, 3000) + } } finally { if (requestId === loadRequestId && loadingDataKey === dataKey) { loadingDataKey = null @@ -700,7 +716,9 @@ export function AccountSecretsRoute(handle: Handle) { ] const currentDataKey = getDataRefreshKey(currentHref) const isRefreshingForLocationChange = - status !== 'loading' && currentDataKey !== lastLoadedDataKey + status !== 'loading' && + currentDataKey !== lastLoadedDataKey && + currentDataKey !== lastFailedDataKey const isLoadingCurrentLocation = loadingDataKey === currentDataKey if ( (status === 'loading' || isRefreshingForLocationChange) && From 7eee19d03b90dc5704e6b0014473c5d7892df784 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 28 Mar 2026 06:07:31 +0000 Subject: [PATCH 6/6] Fix retry timeout race Co-authored-by: Kent C. Dodds --- packages/worker/client/routes/account-secrets.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/worker/client/routes/account-secrets.tsx b/packages/worker/client/routes/account-secrets.tsx index 014391b254..1fbcbfb739 100644 --- a/packages/worker/client/routes/account-secrets.tsx +++ b/packages/worker/client/routes/account-secrets.tsx @@ -453,7 +453,11 @@ export function AccountSecretsRoute(handle: Handle) { message = error instanceof Error ? error.message : 'Unable to load your secrets.' handle.update() - if (!retryTimeout && typeof window !== 'undefined') { + if (typeof window !== 'undefined') { + if (retryTimeout) { + clearTimeout(retryTimeout) + retryTimeout = null + } retryTimeout = window.setTimeout(() => { retryTimeout = null if (lastFailedDataKey !== dataKey) return