From d1a6045016b9eec8697269389b24906511f0f4a8 Mon Sep 17 00:00:00 2001 From: jackkav Date: Tue, 26 May 2026 08:50:19 +0200 Subject: [PATCH 1/4] fix: replace useOrganizationPermissions fetcher in AISettings with direct async fetch AISettings is rendered inside SettingsModal (always-mounted via in root.tsx). useOrganizationPermissions internally creates a React Router useFetcher, which participates in the navigation lifecycle even when the modal is closed. When navigating to the scratchpad after logout, the fetcher's pending state prevented navigation from committing, causing a 30s hang in the bundling test. Replaced with a plain useEffect + getOrganizationFeatures call so no fetcher is registered against the router. Co-Authored-By: Claude Sonnet 4.6 --- .../src/ui/components/settings/ai-settings.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/insomnia/src/ui/components/settings/ai-settings.tsx b/packages/insomnia/src/ui/components/settings/ai-settings.tsx index e390e19f2a24..a2938e6677dc 100644 --- a/packages/insomnia/src/ui/components/settings/ai-settings.tsx +++ b/packages/insomnia/src/ui/components/settings/ai-settings.tsx @@ -1,17 +1,31 @@ +import { getOrganizationFeatures, type FeatureList } from 'insomnia-api'; import { useCallback, useEffect, useState } from 'react'; import { Button, Switch } from 'react-aria-components'; +import { useParams } from 'react-router'; import type { AIFeatureNames, LLMBackend, LLMConfig } from '~/main/llm-config-service'; +import { models } from '~/insomnia-data'; +import { fallbackFeatures } from '~/routes/organization.$organizationId.permissions'; +import { useRootLoaderData } from '~/root'; import { Badge } from '~/ui/components/base/badge'; import { Claude } from '~/ui/components/settings/llms/claude'; import { Gemini } from '~/ui/components/settings/llms/gemini'; import { GGUF } from '~/ui/components/settings/llms/gguf'; import { OpenAI } from '~/ui/components/settings/llms/openai'; import { Url } from '~/ui/components/settings/llms/url'; -import { useOrganizationPermissions } from '~/ui/hooks/use-organization-features'; export const AISettings = () => { - const { features } = useOrganizationPermissions(); + const { organizationId } = useParams() as { organizationId?: string }; + const { userSession } = useRootLoaderData()!; + const [features, setFeatures] = useState(fallbackFeatures); + + useEffect(() => { + if (organizationId && userSession.id && !models.organization.isScratchpadOrganizationId(organizationId)) { + getOrganizationFeatures({ organizationId, sessionId: userSession.id }) + .then(res => setFeatures(res?.features || fallbackFeatures)) + .catch(() => setFeatures(fallbackFeatures)); + } + }, [organizationId, userSession.id]); const [currentLLM, setCurrentLLM] = useState(null); const [selectedBackend, setSelectedBackend] = useState('gguf'); const [configuredLLMs, setConfiguredLLMs] = useState([]); From 977677cd2d3e2cefd7062b8fafd2a5d662a0cbcd Mon Sep 17 00:00:00 2001 From: jackkav Date: Tue, 26 May 2026 09:20:18 +0200 Subject: [PATCH 2/4] fix: replace useOrganizationPermissions fetcher in AISettings with direct async fetch Co-Authored-By: Claude Sonnet 4.6 --- .../src/ui/components/settings/ai-settings.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/insomnia/src/ui/components/settings/ai-settings.tsx b/packages/insomnia/src/ui/components/settings/ai-settings.tsx index a2938e6677dc..e9f16a86e880 100644 --- a/packages/insomnia/src/ui/components/settings/ai-settings.tsx +++ b/packages/insomnia/src/ui/components/settings/ai-settings.tsx @@ -1,12 +1,12 @@ -import { getOrganizationFeatures, type FeatureList } from 'insomnia-api'; +import { type FeatureList, getOrganizationFeatures } from 'insomnia-api'; import { useCallback, useEffect, useState } from 'react'; import { Button, Switch } from 'react-aria-components'; import { useParams } from 'react-router'; -import type { AIFeatureNames, LLMBackend, LLMConfig } from '~/main/llm-config-service'; import { models } from '~/insomnia-data'; -import { fallbackFeatures } from '~/routes/organization.$organizationId.permissions'; +import type { AIFeatureNames, LLMBackend, LLMConfig } from '~/main/llm-config-service'; import { useRootLoaderData } from '~/root'; +import { fallbackFeatures } from '~/routes/organization.$organizationId.permissions'; import { Badge } from '~/ui/components/base/badge'; import { Claude } from '~/ui/components/settings/llms/claude'; import { Gemini } from '~/ui/components/settings/llms/gemini'; @@ -132,7 +132,7 @@ export const AISettings = () => { toggleAIFeature('aiMockServers', enabled)} + onChange={enabled => toggleAIFeature('aiMockServers', enabled)} isDisabled={isMockServerFeatureDisabled} className="group flex items-center gap-2" > @@ -158,7 +158,7 @@ export const AISettings = () => { toggleAIFeature('aiCommitMessages', enabled)} + onChange={enabled => toggleAIFeature('aiCommitMessages', enabled)} isDisabled={isCommitMessagesFeatureDisabled} className="group flex items-center gap-2" > @@ -184,7 +184,7 @@ export const AISettings = () => { toggleAIFeature('aiMcpClient', enabled)} + onChange={enabled => toggleAIFeature('aiMcpClient', enabled)} isDisabled={isMcpClientFeatureDisabled} className="group flex items-center gap-2" > From cd9ddb07ed8a58ae16171195a5ad48ce47eebb2d Mon Sep 17 00:00:00 2001 From: jackkav Date: Tue, 26 May 2026 11:01:25 +0200 Subject: [PATCH 3/4] fix: prevent stale features from race condition in AISettings useEffect Reset to fallbackFeatures when fetch condition is not met, and use a cancelled flag to ignore responses from outdated requests after unmount or dependency change. Co-Authored-By: Claude Sonnet 4.6 --- .../src/ui/components/settings/ai-settings.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/insomnia/src/ui/components/settings/ai-settings.tsx b/packages/insomnia/src/ui/components/settings/ai-settings.tsx index e9f16a86e880..9427c41343f8 100644 --- a/packages/insomnia/src/ui/components/settings/ai-settings.tsx +++ b/packages/insomnia/src/ui/components/settings/ai-settings.tsx @@ -20,11 +20,15 @@ export const AISettings = () => { const [features, setFeatures] = useState(fallbackFeatures); useEffect(() => { - if (organizationId && userSession.id && !models.organization.isScratchpadOrganizationId(organizationId)) { - getOrganizationFeatures({ organizationId, sessionId: userSession.id }) - .then(res => setFeatures(res?.features || fallbackFeatures)) - .catch(() => setFeatures(fallbackFeatures)); + if (!organizationId || !userSession.id || models.organization.isScratchpadOrganizationId(organizationId)) { + setFeatures(fallbackFeatures); + return; } + let cancelled = false; + getOrganizationFeatures({ organizationId, sessionId: userSession.id }) + .then(res => { if (!cancelled) { setFeatures(res?.features || fallbackFeatures); } }) + .catch(() => { if (!cancelled) { setFeatures(fallbackFeatures); } }); + return () => { cancelled = true; }; }, [organizationId, userSession.id]); const [currentLLM, setCurrentLLM] = useState(null); const [selectedBackend, setSelectedBackend] = useState('gguf'); From 22a9c7995ebcf53288752e35a511fda281b29ef9 Mon Sep 17 00:00:00 2001 From: jackkav Date: Tue, 26 May 2026 13:33:21 +0200 Subject: [PATCH 4/4] fix: cancel stale debounce on blur in OneLineEditor to prevent race condition When the user blurs a field immediately after typing (e.g. clicking Add Row), the 100ms debounce could fire after the subsequent write, overwriting the DB with stale kvPairs that didn't include the new row. Fix by: adding cancel() to misc.debounce, merging changes/blur into a single effect registered once via a ref (eliminating stale closures), and having blur explicitly cancel any pending debounce before flushing. Co-Authored-By: Claude Sonnet 4.6 --- packages/insomnia/src/common/misc.ts | 1 + .../.client/codemirror/one-line-editor.tsx | 27 +++++++++++-------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/insomnia/src/common/misc.ts b/packages/insomnia/src/common/misc.ts index dbd255c9c258..111760e4a51f 100644 --- a/packages/insomnia/src/common/misc.ts +++ b/packages/insomnia/src/common/misc.ts @@ -109,6 +109,7 @@ export const debounce = ) => ReturnType>( clearTimeout(timeout); timeout = setTimeout(() => func(...args), waitFor); }; + debounced.cancel = () => clearTimeout(timeout); return debounced; }; diff --git a/packages/insomnia/src/ui/components/.client/codemirror/one-line-editor.tsx b/packages/insomnia/src/ui/components/.client/codemirror/one-line-editor.tsx index 0a30a2c3e3d2..d5e186a792a3 100644 --- a/packages/insomnia/src/ui/components/.client/codemirror/one-line-editor.tsx +++ b/packages/insomnia/src/ui/components/.client/codemirror/one-line-editor.tsx @@ -65,6 +65,8 @@ export const OneLineEditor = forwardRef ) => { const textAreaRef = useRef(null); const codeMirror = useRef(null); + const onChangeRef = useRef(onChange); + onChangeRef.current = onChange; const { settings } = useRootLoaderData()!; const { isOwner, isEnterprisePlan } = usePlanData(); const { handleRender, handleGetRenderContext } = useNunjucks(); @@ -296,23 +298,26 @@ export const OneLineEditor = forwardRef useEffect(() => { const fn = misc.debounce((doc: CodeMirror.Editor) => { - if (onChange) { - onChange(doc.getValue() || ''); + if (onChangeRef.current) { + onChangeRef.current(doc.getValue() || ''); } }, DEBOUNCE_MILLIS); - codeMirror.current?.on('changes', fn); - return () => codeMirror.current?.off('changes', fn); - }, [onChange]); - - useEffect(() => { const flushOnBlur = (doc: CodeMirror.Editor) => { - if (onChange) { - onChange(doc.getValue() || ''); + // Cancel the pending debounce so a stale fire can't overwrite state after blur + fn.cancel(); + if (onChangeRef.current) { + onChangeRef.current(doc.getValue() || ''); } }; + codeMirror.current?.on('changes', fn); codeMirror.current?.on('blur', flushOnBlur); - return () => codeMirror.current?.off('blur', flushOnBlur); - }, [onChange]); + return () => { + fn.cancel(); + codeMirror.current?.off('changes', fn); + codeMirror.current?.off('blur', flushOnBlur); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); useEffect(() => { const unsubscribe = window.main.on(