Compare commits

...

1 commit

Author SHA1 Message Date
MengXi
bbacbf52a7
fix(options): preserve focused provider options drafts (#1419) 2026-04-28 22:08:00 -07:00
3 changed files with 67 additions and 3 deletions

View file

@ -0,0 +1,5 @@
---
"@read-frog/extension": patch
---
fix(options): preserve focused provider options drafts

View file

@ -22,17 +22,23 @@ vi.mock("@/components/ui/json-code-editor", () => ({
JSONCodeEditor: ({
value,
onChange,
onBlur,
onFocus,
placeholder,
}: {
value?: string
onChange?: (value: string) => void
onBlur?: () => void
onFocus?: () => void
placeholder?: string
}) => (
<textarea
aria-label="provider-options-editor"
value={value}
placeholder={placeholder}
onBlur={onBlur}
onChange={event => onChange?.(event.target.value)}
onFocus={onFocus}
/>
),
}))
@ -53,16 +59,22 @@ const baseProviderConfig: APIProviderConfig = {
function ProviderOptionsFieldHarness({
initialConfig,
externalProviderOptions,
submitDelayMs = 0,
}: {
initialConfig: APIProviderConfig
externalProviderOptions?: Record<string, unknown>
submitDelayMs?: number
}) {
const [providerConfig, setProviderConfig] = useState(initialConfig)
const form = useAppForm({
...formOpts,
defaultValues: providerConfig,
onSubmit: async ({ value }) => {
setProviderConfig(value)
if (submitDelayMs > 0) {
await new Promise(resolve => window.setTimeout(resolve, submitDelayMs))
}
setProviderConfig(submitDelayMs > 0 ? structuredClone(value) : value)
},
})
@ -105,6 +117,7 @@ describe("providerOptionsField", () => {
render(<ProviderOptionsFieldHarness initialConfig={baseProviderConfig} />)
const editor = screen.getByLabelText("provider-options-editor")
fireEvent.focus(editor)
fireEvent.change(editor, { target: { value: "{\"reasoningEffort\":\"minimal\"}" } })
await act(async () => {
@ -115,6 +128,29 @@ describe("providerOptionsField", () => {
expect(screen.getByLabelText("provider-options-editor")).toHaveValue("{\"reasoningEffort\":\"minimal\"}")
})
it("keeps focused draft edits when a delayed autosave echo arrives", async () => {
render(<ProviderOptionsFieldHarness initialConfig={baseProviderConfig} submitDelayMs={100} />)
const editor = screen.getByLabelText("provider-options-editor")
fireEvent.focus(editor)
fireEvent.change(editor, { target: { value: "{\"reasoningEffort\":\"minimal\"}" } })
await act(async () => {
vi.advanceTimersByTime(500)
await Promise.resolve()
})
fireEvent.change(editor, { target: { value: "{\"reasoningEffort\":\"low\"}" } })
await act(async () => {
vi.advanceTimersByTime(100)
await Promise.resolve()
await Promise.resolve()
})
expect(screen.getByLabelText("provider-options-editor")).toHaveValue("{\"reasoningEffort\":\"low\"}")
})
it("shows the matched recommended provider options as the placeholder when the value is empty", () => {
render(<ProviderOptionsFieldHarness initialConfig={baseProviderConfig} />)
@ -180,7 +216,9 @@ describe("providerOptionsField", () => {
)
const editor = screen.getByLabelText("provider-options-editor")
fireEvent.focus(editor)
fireEvent.change(editor, { target: { value: "{" } })
fireEvent.blur(editor)
await act(async () => {
fireEvent.click(screen.getByRole("button", { name: "apply-external" }))
await Promise.resolve()

View file

@ -39,6 +39,7 @@ export const ProviderOptionsField = withForm({
const [jsonInput, setJsonInput] = useState(() => externalJson)
const lastCommittedJsonRef = useRef(externalJson)
const pendingEditorCommitRef = useRef(false)
const editorFocusedRef = useRef(false)
const syncJsonInput = useEffectEvent((nextJson: string) => {
// eslint-disable-next-line react/set-state-in-effect
@ -55,6 +56,18 @@ export const ProviderOptionsField = withForm({
return jsonInput
})
const handleJsonInputChange = useCallback((nextJson: string) => {
setJsonInput(nextJson)
}, [])
const handleEditorFocus = useCallback(() => {
editorFocusedRef.current = true
}, [])
const handleEditorBlur = useCallback(() => {
editorFocusedRef.current = false
}, [])
useEffect(() => {
resetSyncStateForProvider()
}, [providerConfig.id])
@ -66,9 +79,15 @@ export const ProviderOptionsField = withForm({
}
pendingEditorCommitRef.current = false
const currentJsonInput = readJsonInput()
if (editorFocusedRef.current && currentJsonInput !== lastCommittedJsonRef.current) {
return
}
lastCommittedJsonRef.current = externalJson
if (readJsonInput() !== externalJson) {
if (currentJsonInput !== externalJson) {
syncJsonInput(externalJson)
}
}, [providerConfig.providerOptions, externalJson])
@ -127,7 +146,9 @@ export const ProviderOptionsField = withForm({
</FieldLabel>
<JSONCodeEditor
value={jsonInput}
onChange={setJsonInput}
onChange={handleJsonInputChange}
onFocus={handleEditorFocus}
onBlur={handleEditorBlur}
placeholder={placeholderText}
hasError={!!jsonError}
height="150px"