Skip to content

Commit 9802e78

Browse files
committed
fix(webview): debounce IO Intelligence catalog refreshes while the key is typed
The API-key effect posted requestRouterModels on every keystroke, and because the IO Intelligence cache is keyed by the API key each partial value triggered a fresh catalog request. Use the same 250ms useDebounce as ApiOptions so only the settled key is sent, and drop a pending request on unmount.
1 parent 9949b13 commit 9802e78

2 files changed

Lines changed: 56 additions & 20 deletions

File tree

‎webview-ui/src/components/settings/providers/IOIntelligence.tsx‎

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { useCallback, useEffect } from "react"
1+
import { useCallback } from "react"
2+
import { useDebounce } from "react-use"
23
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
34

45
import {
@@ -47,15 +48,22 @@ export const IOIntelligence = ({
4748
[setApiConfigurationField],
4849
)
4950

50-
useEffect(() => {
51-
vscode.postMessage({
52-
type: RouterModelsMessageType.requestRouterModels,
53-
values: {
54-
provider: providerIdentifiers.ioIntelligence,
55-
ioIntelligenceApiKey: apiConfiguration.ioIntelligenceApiKey,
56-
},
57-
})
58-
}, [apiConfiguration.ioIntelligenceApiKey])
51+
// Debounced model refresh, only executed 250ms after the user stops
52+
// typing the key (same cadence as the provider refreshes in ApiOptions),
53+
// so each keystroke does not trigger a catalog request.
54+
useDebounce(
55+
() => {
56+
vscode.postMessage({
57+
type: RouterModelsMessageType.requestRouterModels,
58+
values: {
59+
provider: providerIdentifiers.ioIntelligence,
60+
ioIntelligenceApiKey: apiConfiguration.ioIntelligenceApiKey,
61+
},
62+
})
63+
},
64+
250,
65+
[apiConfiguration.ioIntelligenceApiKey],
66+
)
5967

6068
return (
6169
<>

‎webview-ui/src/components/settings/providers/__tests__/IOIntelligence.spec.tsx‎

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { fireEvent, render, screen } from "@testing-library/react"
1+
import { act, fireEvent, render, screen } from "@testing-library/react"
22

33
import {
44
type OrganizationAllowList,
@@ -99,6 +99,8 @@ describe("IOIntelligence", () => {
9999

100100
beforeEach(() => vi.clearAllMocks())
101101

102+
afterEach(() => vi.useRealTimers())
103+
102104
it("renders the secret key input, CTA, and dynamic model picker", () => {
103105
renderComponent({ ioIntelligenceApiKey: "stored" })
104106

@@ -129,25 +131,51 @@ describe("IOIntelligence", () => {
129131
expect(setApiConfigurationField).toHaveBeenCalledWith("ioIntelligenceApiKey", "new-secret")
130132
})
131133

132-
it("refreshes models with the unsaved cached key whenever it changes", () => {
134+
it("refreshes models with the unsaved cached key 250ms after it stops changing", () => {
135+
vi.useFakeTimers()
136+
const rerenderWithKey = (rerender: ReturnType<typeof render>["rerender"], ioIntelligenceApiKey: string) =>
137+
rerender(
138+
<IOIntelligence
139+
apiConfiguration={{ ioIntelligenceApiKey }}
140+
setApiConfigurationField={setApiConfigurationField}
141+
routerModels={{ ...routerModels, "io-intelligence": {} }}
142+
organizationAllowList={organizationAllowList}
143+
/>,
144+
)
145+
133146
const { rerender } = renderComponent({ ioIntelligenceApiKey: "first-key" })
147+
148+
act(() => vi.advanceTimersByTime(249))
149+
expect(postMessageMock).not.toHaveBeenCalled()
150+
act(() => vi.advanceTimersByTime(1))
151+
expect(postMessageMock).toHaveBeenCalledTimes(1)
134152
expect(postMessageMock).toHaveBeenLastCalledWith({
135153
type: RouterModelsMessageType.requestRouterModels,
136154
values: { provider: providerIdentifiers.ioIntelligence, ioIntelligenceApiKey: "first-key" },
137155
})
138156

139-
rerender(
140-
<IOIntelligence
141-
apiConfiguration={{ ioIntelligenceApiKey: "unsaved-key" }}
142-
setApiConfigurationField={setApiConfigurationField}
143-
routerModels={{ ...routerModels, "io-intelligence": {} }}
144-
organizationAllowList={organizationAllowList}
145-
/>,
146-
)
157+
// Each edit restarts the timer, so intermediate values never reach the host.
158+
rerenderWithKey(rerender, "unsaved-")
159+
act(() => vi.advanceTimersByTime(200))
160+
rerenderWithKey(rerender, "unsaved-key")
161+
act(() => vi.advanceTimersByTime(249))
162+
expect(postMessageMock).toHaveBeenCalledTimes(1)
147163

164+
act(() => vi.advanceTimersByTime(1))
165+
expect(postMessageMock).toHaveBeenCalledTimes(2)
148166
expect(postMessageMock).toHaveBeenLastCalledWith({
149167
type: RouterModelsMessageType.requestRouterModels,
150168
values: { provider: providerIdentifiers.ioIntelligence, ioIntelligenceApiKey: "unsaved-key" },
151169
})
152170
})
171+
172+
it("drops a pending model refresh when it unmounts", () => {
173+
vi.useFakeTimers()
174+
const { unmount } = renderComponent({ ioIntelligenceApiKey: "first-key" })
175+
176+
unmount()
177+
act(() => vi.advanceTimersByTime(250))
178+
179+
expect(postMessageMock).not.toHaveBeenCalled()
180+
})
153181
})

0 commit comments

Comments
 (0)