diff --git a/packages/insomnia/src/common/misc.ts b/packages/insomnia/src/common/misc.ts index dbd255c9c25..111760e4a51 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 0a30a2c3e3d..d5e186a792a 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( diff --git a/packages/insomnia/src/ui/components/settings/ai-settings.tsx b/packages/insomnia/src/ui/components/settings/ai-settings.tsx index e390e19f2a2..9427c41343f 100644 --- a/packages/insomnia/src/ui/components/settings/ai-settings.tsx +++ b/packages/insomnia/src/ui/components/settings/ai-settings.tsx @@ -1,17 +1,35 @@ +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 { models } from '~/insomnia-data'; 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'; 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)) { + 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'); const [configuredLLMs, setConfiguredLLMs] = useState([]); @@ -118,7 +136,7 @@ export const AISettings = () => { toggleAIFeature('aiMockServers', enabled)} + onChange={enabled => toggleAIFeature('aiMockServers', enabled)} isDisabled={isMockServerFeatureDisabled} className="group flex items-center gap-2" > @@ -144,7 +162,7 @@ export const AISettings = () => { toggleAIFeature('aiCommitMessages', enabled)} + onChange={enabled => toggleAIFeature('aiCommitMessages', enabled)} isDisabled={isCommitMessagesFeatureDisabled} className="group flex items-center gap-2" > @@ -170,7 +188,7 @@ export const AISettings = () => { toggleAIFeature('aiMcpClient', enabled)} + onChange={enabled => toggleAIFeature('aiMcpClient', enabled)} isDisabled={isMcpClientFeatureDisabled} className="group flex items-center gap-2" >