feat: fully dynamic model picker in inline prompt

This commit is contained in:
tao.chen
2026-07-23 16:11:20 +08:00
parent 74de18646f
commit 74953d0ab8
3 changed files with 164 additions and 40 deletions
+65 -14
View File
@@ -1,12 +1,13 @@
/**
* Unit tests for OpenCodeCellActions (single AI button) and OpenCodeInlinePrompt
* (inline input panel inside the cell).
* (inline input panel inside the cell). v3-final: no settings — the model
* picker is fully dynamic; default selection is the first provider/model.
*
* Mocks @jupyterlab/cells, @jupyterlab/apputils, and @lumino/widgets at the
* test boundary so the real ESM packages are not loaded.
*/
jest.mock('@jupyterlab/cells', () => ({
CodeCell: class CodeCell { public node = document.createElement('div'); }
CodeCell: class CodeCell {}
}));
jest.mock('@jupyterlab/apputils', () => ({
@@ -32,21 +33,25 @@ jest.mock('@lumino/widgets', () => {
}
public dispose(): void {
this._isDisposed = true;
if (this.node.parentElement) {
this.node.parentElement.removeChild(this.node);
if (this.node.parentNode) {
this.node.parentNode.removeChild(this.node);
}
}
public onAfterAttach(msg: any): void {
// no-op for tests
}
public static attach(widget: Widget, host: HTMLElement): void {
host.appendChild(widget.node);
}
public onAfterAttach(_msg: any): void {
// no-op
}
}
return { Widget };
});
import { OpenCodeCellActions } from '../components/opencode_cell_actions';
import {
OpenCodeCellActions,
setOpenCodeProviders,
setOpenCodeServerSettings
} from '../components/opencode_cell_actions';
import { OpenCodeInlinePrompt } from '../components/opencode_inline_prompt';
import { CodeCell } from '@jupyterlab/cells';
@@ -81,6 +86,7 @@ function makeFakeCell(
};
const cell = new (CodeCell as unknown as new () => CodeCell)();
(cell as any).model = model;
(cell as any).node = document.createElement('div');
const notebook: any = { widgets: [] as CodeCell[] };
const panel: any = {
@@ -112,7 +118,11 @@ function makeFakeCell(
}
describe('OpenCodeCellActions', () => {
it('renders a single AI button (not 3 buttons)', () => {
it('allows server settings to be injected', () => {
setOpenCodeServerSettings({} as any);
});
it('renders a single AI button', () => {
const cell = makeFakeCell('x = 1');
const actions = new OpenCodeCellActions(cell);
const btns = actions.node.querySelectorAll('button');
@@ -138,7 +148,6 @@ describe('OpenCodeCellActions', () => {
it('clicking the button attaches an OpenCodeInlinePrompt to the cell', () => {
const cell = makeFakeCell('x = 1');
const actions = new OpenCodeCellActions(cell);
// Attach actions to cell so it can resolve parent chain
Object.defineProperty(actions, 'parent', { value: cell, configurable: true });
(actions as any).onAfterAttach({} as any);
@@ -160,8 +169,7 @@ describe('OpenCodeCellActions', () => {
const btn = actions.node.querySelector('button') as HTMLButtonElement;
btn.click();
btn.click();
const prompt = cell.node.querySelector('.opencode-inline-prompt');
expect(prompt).toBeNull();
expect(cell.node.querySelector('.opencode-inline-prompt')).toBeNull();
});
it('disconnects model signals on dispose', () => {
@@ -172,15 +180,58 @@ describe('OpenCodeCellActions', () => {
expect(model.contentChanged.disconnect).toHaveBeenCalled();
expect(model.stateChanged.disconnect).toHaveBeenCalled();
});
it('renders a model selector with the first option selected when providers are cached', () => {
setOpenCodeProviders({
providers: [
{ id: 'anthropic', models: [{ id: 'claude-sonnet-4-20250514' }] },
{ id: 'openai', models: [{ id: '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 select = 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');
setOpenCodeProviders(null);
});
it('does not render the selector when providers are not cached', () => {
setOpenCodeProviders(null);
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();
expect(
cell.node.querySelector('.opencode-inline-prompt .opencode-model-select')
).toBeNull();
});
});
describe('OpenCodeInlinePrompt', () => {
it('renders a textarea, a send and a cancel button', () => {
it('renders a textarea, a send and a cancel button (no selector when providers null)', () => {
const cell = makeFakeCell('x = 1');
const prompt = new OpenCodeInlinePrompt(cell, {
onSubmit: jest.fn(),
onCancel: jest.fn(),
disabled: false
disabled: false,
providers: null
});
expect(prompt.node.querySelector('textarea')).not.toBeNull();
expect(prompt.node.querySelectorAll('button').length).toBe(2);