diff --git a/src/__tests__/opencode_cell_actions.spec.ts b/src/__tests__/opencode_cell_actions.spec.ts index 886b18d..6852d52 100644 --- a/src/__tests__/opencode_cell_actions.spec.ts +++ b/src/__tests__/opencode_cell_actions.spec.ts @@ -181,17 +181,23 @@ describe('OpenCodeCellActions', () => { expect(model.stateChanged.disconnect).toHaveBeenCalled(); }); - it('renders a model selector with the first option selected when providers are cached', () => { + it('renders provider and model selects; model default uses default[provider] when it matches', () => { setOpenCodeProviders({ providers: [ { id: 'anthropic', + name: 'Anthropic', models: { - 'claude-sonnet-4-20250514': { id: 'claude-sonnet-4-20250514' } + 'claude-sonnet-4-20250514': { id: 'claude-sonnet-4-20250514' }, + 'claude-haiku-4-5': { id: 'claude-haiku-4-5' } } }, { id: 'openai', models: { 'gpt-5': { id: 'gpt-5' } } } - ] + ], + default: { + anthropic: 'claude-sonnet-4-20250514', + openai: 'gpt-5' + } }); const cell = makeFakeCell('x = 1'); const actions = new OpenCodeCellActions(cell); @@ -201,15 +207,103 @@ describe('OpenCodeCellActions', () => { const btn = actions.node.querySelector('button') as HTMLButtonElement; btn.click(); - const select = cell.node.querySelector( + const prompt = cell.node.querySelector( + '.opencode-inline-prompt' + ) as HTMLElement; + const providerSel = prompt.querySelector( + '.opencode-provider-select' + ) as HTMLSelectElement; + const modelSel = prompt.querySelector( + '.opencode-model-select' + ) as HTMLSelectElement; + expect(providerSel).not.toBeNull(); + expect(modelSel).not.toBeNull(); + + // Provider: first provider selected by default. + expect(providerSel.options.length).toBe(2); + expect(providerSel.options[0].value).toBe('anthropic'); + expect(providerSel.options[0].textContent).toContain('anthropic'); + expect(providerSel.options[0].textContent).toContain('Anthropic'); + expect(providerSel.value).toBe('anthropic'); + + // Model: options are that provider's model keys, and the default + // matches default['anthropic'] = 'claude-sonnet-4-20250514'. + expect(modelSel.options.length).toBe(2); + expect(modelSel.options[0].value).toBe('claude-sonnet-4-20250514'); + expect(modelSel.options[1].value).toBe('claude-haiku-4-5'); + expect(modelSel.value).toBe('claude-sonnet-4-20250514'); + + setOpenCodeProviders(null); + }); + + it('falls back to the first model when default[provider] is not in models', () => { + setOpenCodeProviders({ + providers: [ + { id: 'bailian', models: { 'kimi/kimi-k2.7-code': { id: 'kimi/kimi-k2.7-code' } } } + ], + // default['bailian'] references a modelID not present in models. + default: { bailian: 'qwen3.7-plus' } + }); + const cell = makeFakeCell('x = 1'); + const actions = new OpenCodeCellActions(cell); + Object.defineProperty(actions, 'parent', { value: cell, configurable: true }); + (actions as any).onAfterAttach({} as any); + + const btn = actions.node.querySelector('button') as HTMLButtonElement; + btn.click(); + + const modelSel = cell.node.querySelector( '.opencode-inline-prompt .opencode-model-select' ) as HTMLSelectElement; - expect(select).not.toBeNull(); - expect(select.options.length).toBe(2); - expect(select.options[0].textContent).toBe('anthropic / claude-sonnet-4-20250514'); - expect(select.options[1].textContent).toBe('openai / gpt-5'); - // v3-final: default = first option (no settings). - expect(select.value).toBe('anthropic|claude-sonnet-4-20250514'); + expect(modelSel.value).toBe('kimi/kimi-k2.7-code'); + setOpenCodeProviders(null); + }); + + it('rebuilds the model select when the provider select changes', () => { + setOpenCodeProviders({ + providers: [ + { + id: 'anthropic', + models: { 'claude-sonnet-4-20250514': { id: 'claude-sonnet-4-20250514' } } + }, + { + id: 'openai', + models: { 'gpt-5': { id: 'gpt-5' } } + } + ], + default: { openai: 'gpt-5' } + }); + const cell = makeFakeCell('x = 1'); + const actions = new OpenCodeCellActions(cell); + Object.defineProperty(actions, 'parent', { value: cell, configurable: true }); + (actions as any).onAfterAttach({} as any); + + const btn = actions.node.querySelector('button') as HTMLButtonElement; + btn.click(); + + const prompt = cell.node.querySelector( + '.opencode-inline-prompt' + ) as HTMLElement; + const providerSel = prompt.querySelector( + '.opencode-provider-select' + ) as HTMLSelectElement; + const modelSel = prompt.querySelector( + '.opencode-model-select' + ) as HTMLSelectElement; + + // Initial: anthropic, its only model. + expect(providerSel.value).toBe('anthropic'); + expect(modelSel.options.length).toBe(1); + expect(modelSel.value).toBe('claude-sonnet-4-20250514'); + + // Switch to openai: model select rebuilds with openai's model and + // default['openai'] = 'gpt-5' is in openai's models so it matches. + providerSel.value = 'openai'; + providerSel.dispatchEvent(new Event('change')); + expect(modelSel.options.length).toBe(1); + expect(modelSel.options[0].value).toBe('gpt-5'); + expect(modelSel.value).toBe('gpt-5'); + setOpenCodeProviders(null); }); @@ -223,6 +317,9 @@ describe('OpenCodeCellActions', () => { const btn = actions.node.querySelector('button') as HTMLButtonElement; btn.click(); + expect( + cell.node.querySelector('.opencode-inline-prompt .opencode-provider-select') + ).toBeNull(); expect( cell.node.querySelector('.opencode-inline-prompt .opencode-model-select') ).toBeNull(); diff --git a/src/components/opencode_inline_prompt.ts b/src/components/opencode_inline_prompt.ts index fd4b6f0..4ba2e11 100644 --- a/src/components/opencode_inline_prompt.ts +++ b/src/components/opencode_inline_prompt.ts @@ -1,12 +1,11 @@ /** * Inline prompt widget attached to a cell's DOM when the AI button is clicked. - * Renders a model selector, a textarea, and send/cancel buttons. The owning - * cell is passed so we can resolve the cell context at submit time. submit - * and cancel are callbacks owned by OpenCodeCellActions. + * Renders two