When the user picks a provider+model in the inline prompt, remember that choice across panel close/reopen AND across JupyterLab restarts, keyed by notebookPath. If the stored provider (or the model under it) is no longer present in the current providers payload, fall back to the default (first provider / default[pid] model) - never silently apply a stale value that would result in an empty or disabled select. - New module src/components/model_selection.ts with loadModelSelection / saveModelSelection / clearModelSelection. Best-effort: localStorage may be disabled (private mode), quota may be exhausted, value may be corrupt - all failure modes return null / no-op rather than throw. - IOpenCodeInlinePromptOptions gains notebookPath?: string. - Constructor resolves the initial selection: prefer stored+valid, else first provider / default[pid]. providerSelect.change and modelSelect.change listeners call _persistSelection() to keep storage in sync. - _rebuildModelSelect now accepts a preferredModelId; if it exists in the current provider's models it's used, else the default. - OpenCodeCellActions._showPrompt passes notebookPath = this._context?.notebookPath so the prompt can key the storage. Tests ----- - 10 unit tests in model_selection.spec.ts (load/save/clear + malformed JSON + per-notebook independence). - 4 prompt behavior tests in opencode_cell_actions.spec.ts (valid stored applied, provider removed -> default, model removed -> default, change -> saved). - 72 frontend jest (was 58), 52 backend pytest (unchanged), build green. Co-Authored-By: Claude <noreply@anthropic.com>
83 lines
2.2 KiB
TypeScript
83 lines
2.2 KiB
TypeScript
/**
|
|
* Persistence of the user's (provider, model) selection in the inline
|
|
* prompt, keyed by notebook path. Stored in localStorage so the
|
|
* preference survives panel close/reopen and JupyterLab restart.
|
|
*
|
|
* The selection is ALWAYS validated against the current providers
|
|
* payload before being applied: if the stored provider (or the model
|
|
* under it) is no longer present, the caller falls back to its
|
|
* default-selection logic. We never silently apply a stale value that
|
|
* would result in an empty / disabled <select>.
|
|
*
|
|
* Storage is best-effort: localStorage may be disabled (private mode),
|
|
* the quota may be exhausted, or the value may be corrupt. All
|
|
* failure modes return null / no-op rather than throw.
|
|
*/
|
|
const STORAGE_PREFIX = 'opencode_bridge:model-selection:';
|
|
|
|
export interface ModelSelection {
|
|
providerId: string;
|
|
modelId: string;
|
|
}
|
|
|
|
export function loadModelSelection(notebookPath: string): ModelSelection | null {
|
|
if (!notebookPath) {
|
|
return null;
|
|
}
|
|
try {
|
|
if (typeof localStorage === 'undefined') {
|
|
return null;
|
|
}
|
|
const raw = localStorage.getItem(STORAGE_PREFIX + notebookPath);
|
|
if (!raw) {
|
|
return null;
|
|
}
|
|
const parsed = JSON.parse(raw);
|
|
if (
|
|
parsed &&
|
|
typeof parsed === 'object' &&
|
|
typeof (parsed as ModelSelection).providerId === 'string' &&
|
|
typeof (parsed as ModelSelection).modelId === 'string'
|
|
) {
|
|
return {
|
|
providerId: (parsed as ModelSelection).providerId,
|
|
modelId: (parsed as ModelSelection).modelId
|
|
};
|
|
}
|
|
return null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function saveModelSelection(
|
|
notebookPath: string,
|
|
sel: ModelSelection
|
|
): void {
|
|
if (!notebookPath) {
|
|
return;
|
|
}
|
|
try {
|
|
if (typeof localStorage === 'undefined') {
|
|
return;
|
|
}
|
|
localStorage.setItem(STORAGE_PREFIX + notebookPath, JSON.stringify(sel));
|
|
} catch {
|
|
// Ignore quota exceeded / disabled storage.
|
|
}
|
|
}
|
|
|
|
export function clearModelSelection(notebookPath: string): void {
|
|
if (!notebookPath) {
|
|
return;
|
|
}
|
|
try {
|
|
if (typeof localStorage === 'undefined') {
|
|
return;
|
|
}
|
|
localStorage.removeItem(STORAGE_PREFIX + notebookPath);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|