Lock the AI assistant form while a prompt reset is pending
Closing the Reset sheet while the reset request was in flight left the form editable, so a prompt edit made then was replaced by the reset response and the leave guard could report the form as saved. The form's fieldset is now disabled while a prompt reset is pending as well as during a save, so no edit can be made that the reset response would overwrite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -305,8 +305,9 @@
|
|||||||
resize: none;
|
resize: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* A form locked while its save is pending, so no edit is made that the
|
/* A form locked while its save or prompt reset is pending, so no edit is
|
||||||
saved settings would then replace. It lays out as if it were not there. */
|
made that the response would then replace. It lays out as if it were not
|
||||||
|
there. */
|
||||||
.lock {
|
.lock {
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -431,6 +431,39 @@ describe('AISettingsPage', () => {
|
|||||||
await waitFor(() => expect(unload()).toBe(false))
|
await waitFor(() => expect(unload()).toBe(false))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('locks the form while a prompt reset is pending, even with the sheet closed', async () => {
|
||||||
|
let answer: (response: Response) => void = () => {}
|
||||||
|
stubAPI({
|
||||||
|
'POST /api/v1/settings/ai/reset-prompt': () => new Promise<Response>((resolve) => (answer = resolve)),
|
||||||
|
})
|
||||||
|
await renderPage()
|
||||||
|
const unload = () => {
|
||||||
|
const event = new Event('beforeunload', { cancelable: true })
|
||||||
|
act(() => void window.dispatchEvent(event))
|
||||||
|
return event.defaultPrevented
|
||||||
|
}
|
||||||
|
fireEvent.change(slider(), { target: { value: '0.2' } })
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Reset' }))
|
||||||
|
const confirm = screen.getByRole('dialog', { name: 'Reset the system prompt?' })
|
||||||
|
fireEvent.click(within(confirm).getByRole('button', { name: 'Reset prompt' }))
|
||||||
|
expect(await within(confirm).findByRole('button', { name: 'Resetting…' })).toBeDisabled()
|
||||||
|
fireEvent.click(within(confirm).getByRole('button', { name: 'Cancel' }))
|
||||||
|
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
|
||||||
|
|
||||||
|
for (const field of [provider(), model(), slider(), prompt()]) expect(field).toBeDisabled()
|
||||||
|
expect(screen.getByRole('button', { name: 'Reset' })).toBeDisabled()
|
||||||
|
expect(save()).toBeDisabled()
|
||||||
|
expect(unload()).toBe(true)
|
||||||
|
|
||||||
|
answer(jsonResponse(200, { ...AI_SETTINGS, system_prompt: DEFAULT_PROMPT }))
|
||||||
|
await waitFor(() => expect(prompt()).toBeEnabled())
|
||||||
|
expect(prompt()).toHaveValue(DEFAULT_PROMPT)
|
||||||
|
expect(slider()).toHaveValue('0.2')
|
||||||
|
expect(unload()).toBe(true)
|
||||||
|
fireEvent.change(prompt(), { target: { value: 'After the reset.' } })
|
||||||
|
expect(prompt()).toHaveValue('After the reset.')
|
||||||
|
})
|
||||||
|
|
||||||
it('keeps 44 px tap targets', async () => {
|
it('keeps 44 px tap targets', async () => {
|
||||||
stubAPI()
|
stubAPI()
|
||||||
await renderPage()
|
await renderPage()
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ function AISettingsForm({ saved, onDirtyChange }: AISettingsFormProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<fieldset className={styles.lock} disabled={save.isPending}>
|
<fieldset className={styles.lock} disabled={save.isPending || resetPrompt.isPending}>
|
||||||
<div className={styles.columns}>
|
<div className={styles.columns}>
|
||||||
<Select
|
<Select
|
||||||
label="Provider"
|
label="Provider"
|
||||||
|
|||||||
Reference in New Issue
Block a user