diff --git a/neode-ui/src/components/AIConnectionModal.vue b/neode-ui/src/components/AIConnectionModal.vue
index 8f216e0f..0121e56a 100644
--- a/neode-ui/src/components/AIConnectionModal.vue
+++ b/neode-ui/src/components/AIConnectionModal.vue
@@ -8,14 +8,17 @@
Pay for AI with ecash. Add funds to this node’s wallet and set the most it may spend.
@@ -57,6 +60,7 @@ const choices: { id: Choice; label: string }[] = [{ id: 'routstr', label: 'Routs
useModalHistory(computed(() => props.show), close)
const mode = ref
('routstr')
const key = ref('')
+const replacingKey = ref(false)
const model = ref('')
const status = ref(null)
const balance = ref(null)
@@ -101,7 +105,7 @@ async function refreshBalance() {
try { const result = await rpcClient.call<{ balance_sats: number }>({ method: 'wallet.ecash-balance', timeout: 8000 }); balance.value = Number.isFinite(result.balance_sats) ? result.balance_sats : null }
catch { balance.value = null }
}
-function choose(choice: Choice) { key.value = ''; error.value = ''; mode.value = choice; if (choice === 'routstr') void refreshBalance() }
+function choose(choice: Choice) { key.value = ''; replacingKey.value = false; error.value = ''; mode.value = choice; if (choice === 'routstr') void refreshBalance() }
function close() { key.value = ''; funding.value = false; scanning.value = false; emit('close') }
async function save() {
if ((mode.value !== 'claude' && mode.value !== 'openai') || saving.value) return
@@ -136,7 +140,7 @@ async function continueRoutstr() {
}
async function finishScan() { scanning.value = false; await refreshBalance() }
async function finishFunding() { funding.value = false; await refreshBalance() }
-watch(() => props.show, open => { if (open) { error.value = ''; void refresh(); if (mode.value === 'routstr') void refreshBalance() } else { key.value = ''; funding.value = false; scanning.value = false } })
+watch(() => props.show, open => { if (open) { error.value = ''; void refresh(); if (mode.value === 'routstr') void refreshBalance() } else { key.value = ''; replacingKey.value = false; funding.value = false; scanning.value = false } })
async function syncSelection() { const state = await refresh(); if (state) emit('configured', state.settings.provider, state.settings.provider === 'openai' ? state.settings.openai_model : undefined) }
defineExpose({ checkNeeded, syncSelection, showRoutstr: () => choose('routstr') })
diff --git a/neode-ui/src/components/__tests__/AIConnectionModal.test.ts b/neode-ui/src/components/__tests__/AIConnectionModal.test.ts
index aec2f26f..451f28fd 100644
--- a/neode-ui/src/components/__tests__/AIConnectionModal.test.ts
+++ b/neode-ui/src/components/__tests__/AIConnectionModal.test.ts
@@ -13,6 +13,22 @@ function mountModal() { return mount(AIConnectionModal, { props: { show: false }
function button(w: ReturnType, label: string) { return w.findAll('button').find(b => b.text() === label)! }
beforeEach(() => { vi.clearAllMocks(); vi.mocked(rpcClient.call).mockImplementation(async ({ method }) => method === 'system.settings.get' ? { value: state() } : {}) })
describe('AI connection setup', () => {
+ it('recognizes the existing Claude key and selects it without reading or rewriting the secret', async () => {
+ vi.mocked(rpcClient.call).mockImplementation(async ({ method }) => method === 'system.settings.get' ? { value: { ...state(), claude_configured: true } } : {})
+ const w = mountModal(); await w.setProps({ show: true }); await flushPromises()
+ await button(w, 'Claude API').trigger('click')
+ expect(w.text()).toContain('Your Claude API key from Settings is ready to use.')
+ expect(w.find('#ai-connection-key').exists()).toBe(false)
+ expect(button(w, 'Use Claude').attributes('disabled')).toBeUndefined()
+ await w.get('form').trigger('submit'); await flushPromises()
+ const writes = vi.mocked(rpcClient.call).mock.calls.map(([r]) => r).filter(r => r.method === 'system.settings.set')
+ expect(writes.map(r => r.params)).toEqual([{ key: 'ai_provider', value: JSON.stringify({ provider: 'claude', openai_model: '' }) }])
+ expect(w.emitted('configured')).toEqual([['claude', undefined]])
+ await button(w, 'Change API key').trigger('click')
+ expect((w.get('#ai-connection-key').element as HTMLInputElement).value).toBe('')
+ await w.setProps({ show: false }); w.unmount()
+ })
+
it('opens on Routstr with a top-up action without authorizing spending', async () => {
const w = mountModal(); await w.setProps({ show: true }); await flushPromises()
expect(button(w, 'Routstr · sats').attributes('aria-pressed')).toBe('true')
@@ -44,7 +60,7 @@ describe('AI connection setup', () => {
await w.get('form').trigger('submit'); await flushPromises()
const writes = vi.mocked(rpcClient.call).mock.calls.map(([r]) => r).filter(r => r.method === 'system.settings.set')
expect(writes.map(r => r.params)).toEqual([{ key: 'openai_api_key', value: 'test-private-key' }, { key: 'ai_provider', value: JSON.stringify({ provider: 'openai', openai_model: 'test-chat-model' }) }])
- expect((w.get('#ai-connection-key').element as HTMLInputElement).value).toBe('')
+ expect(w.find('#ai-connection-key').exists()).toBe(false)
expect(w.emitted('configured')).toEqual([['openai', 'test-chat-model']])
expect(JSON.stringify(w.emitted())).not.toContain('test-private-key')
w.unmount()