Keep the stored-key removal path open and verify after sheet dismissal
CI / ci (push) Successful in 1m57s
CI / ci (pull_request) Successful in 1m58s

- The API key row stays visible whenever a key is stored, Ollama included,
  and Replace (with Remove key) stays enabled after a provider or base URL
  change, so "remove the key first" can be followed without undoing it.
- One lock hint and one save-error guidance cover provider and base URL.
- Closing the key sheet no longer resets the store mutation, so the PUT's
  verify still runs; a store error after dismissal shows under the key.
- Reset and Save changes sit at the bottom of the screen (mock 1o).
- Tests: base URL lock guidance, recover by removing the key with Ollama
  chosen, and dismissing the sheet while the PUT is pending.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 10:13:52 -04:00
co-authored by Claude Opus 5.5
parent b0e31df099
commit 0403c9418f
3 changed files with 136 additions and 29 deletions
+46 -25
View File
@@ -104,6 +104,9 @@ function AISettingsForm({ saved }: { saved: AISettings }) {
const patch = patchOf(saved, draft)
const dirty = Object.keys(patch).length > 0
const providerChanged = draft.provider !== saved.provider
// The server keeps a stored key's provider and base URL fixed until the
// key is removed.
const keyLocked = saved.api_key_last4 !== null && (providerChanged || draft.base_url !== saved.base_url)
const temperatureId = useId()
// The model list is the saved provider's, so it is asked for only while
// that provider is chosen and has the key it needs.
@@ -200,12 +203,7 @@ function AISettingsForm({ saved }: { saved: AISettings }) {
)}
</div>
</div>
{providerChanged && saved.api_key_last4 !== null && (
<p className={styles.hint}>
{aiProviderLabels[saved.provider]}'s API key is stored. Remove it (Replace → Remove key) before switching
provider.
</p>
)}
{keyLocked && <p className={styles.hint}>{keyLockedGuidance}</p>}
{typedModel && (
<TextInput
label="Model id"
@@ -229,11 +227,7 @@ function AISettingsForm({ saved }: { saved: AISettings }) {
onChange={(event) => edit({ base_url: event.target.value })}
/>
)}
{aiKeyPolicy[draft.provider] === 'none' ? (
<p className={styles.hint}>{aiProviderLabels[draft.provider]} takes no API key.</p>
) : (
<APIKey saved={saved} providerChanged={providerChanged} />
)}
<APIKey saved={saved} draftProvider={draft.provider} />
<div className={styles.temperature}>
<div className={styles.temperatureHeader}>
@@ -318,7 +312,15 @@ export function formatVerifiedDate(timestamp: string): string {
return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric' }).format(date)
}
function APIKey({ saved, providerChanged }: { saved: AISettings; providerChanged: boolean }) {
// keyLockedGuidance is how to change the provider or base URL while a key
// is stored.
const keyLockedGuidance =
'An API key is stored. Remove it (Replace → Remove key) before changing the provider or base URL.'
// APIKey stays mounted whatever provider the draft names, so a key stored
// (and the verify that follows it) completes even if the household changes
// the provider meanwhile, and a stored key can always be removed.
function APIKey({ saved, draftProvider }: { saved: AISettings; draftProvider: AIProvider }) {
const [replacing, setReplacing] = useState(false)
const [key, setKey] = useState('')
const store = useSetAIKey()
@@ -326,11 +328,18 @@ function APIKey({ saved, providerChanged }: { saved: AISettings; providerChanged
const labelId = useId()
const sheetTitleId = useId()
const last4 = saved.api_key_last4
const providerChanged = draftProvider !== saved.provider
function open() {
store.reset()
setReplacing(true)
}
// close keeps the store mutation: a key already sent is still stored and
// verified after the sheet is dismissed.
function close() {
setReplacing(false)
setKey('')
store.reset()
}
function onStore() {
@@ -348,6 +357,21 @@ function APIKey({ saved, providerChanged }: { saved: AISettings; providerChanged
store.mutate('', { onSuccess: close })
}
const storeError = store.isError && (
<p className={styles.error} role="alert">
The key was not stored: {store.error.message}
</p>
)
if (last4 === null && aiKeyPolicy[draftProvider] === 'none') {
return (
<div className={styles.key}>
<p className={styles.hint}>{aiProviderLabels[draftProvider]} takes no API key.</p>
{!replacing && storeError}
</div>
)
}
return (
<div className={styles.key}>
<span id={labelId} className={styles.label}>
@@ -358,13 +382,16 @@ function APIKey({ saved, providerChanged }: { saved: AISettings; providerChanged
<button
type="button"
className={styles.replace}
disabled={providerChanged}
onClick={() => setReplacing(true)}
disabled={store.isPending || (last4 === null && providerChanged)}
onClick={open}
>
{last4 === null ? 'Add key' : 'Replace'}
{store.isPending ? 'Saving…' : last4 === null ? 'Add key' : 'Replace'}
</button>
</div>
{providerChanged && <p className={styles.note}>Save the provider change before adding its key.</p>}
{last4 === null && providerChanged && (
<p className={styles.note}>Save the provider change before adding its key.</p>
)}
{!replacing && storeError}
<KeyStatus saved={saved} verify={verify} />
<Sheet open={replacing} onClose={close} aria-labelledby={sheetTitleId}>
@@ -391,11 +418,7 @@ function APIKey({ saved, providerChanged }: { saved: AISettings; providerChanged
spellCheck={false}
onChange={(event) => setKey(event.target.value)}
/>
{store.isError && (
<p className={styles.error} role="alert">
The key was not stored: {store.error.message}
</p>
)}
{storeError}
<div className={styles.sheetActions}>
{last4 !== null && (
<Button variant="outline" disabled={store.isPending} onClick={onRemove}>
@@ -473,9 +496,7 @@ export function verifyErrorMessage(error: Error): string {
// stored key to another provider or base URL; it names how to fix that.
function saveErrorMessage(error: Error): string {
if (error instanceof ApiError && error.code === 'invalid_settings') {
const guidance = error.message.includes('while an API key is stored')
? ' To switch provider, remove the API key first (Replace → Remove key).'
: ''
const guidance = error.message.includes('while an API key is stored') ? ` ${keyLockedGuidance}` : ''
return `Not saved: ${error.message}.${guidance}`
}
return `Not saved: ${error.message}`