feat: Ollama models command, with load/pull/delete and unload (#16)
This commit was merged in pull request #16.
This commit is contained in:
+75
-4
@@ -1,5 +1,5 @@
|
||||
import { getPreferenceValues } from "@vicinae/api";
|
||||
import { DEFAULT_HOST, DEFAULT_MAX_TOOL_ROUNDS, DEFAULT_MODEL } from "./constants";
|
||||
import { getPreferenceValues, LocalStorage } from "@vicinae/api";
|
||||
import { DEFAULT_HOST, DEFAULT_MAX_TOOL_ROUNDS, DEFAULT_MODEL, STORAGE_MODEL_KEY } from "./constants";
|
||||
|
||||
/**
|
||||
* Preferences, read and normalised.
|
||||
@@ -26,10 +26,29 @@ export interface LamPreferences {
|
||||
maxToolRounds?: string;
|
||||
}
|
||||
|
||||
/** Where the model in use came from, so the UI can explain a divergence from the preference. */
|
||||
export type ModelSource = "preference" | "selection";
|
||||
|
||||
/** Preferences with every optional value filled in, so callers never branch on undefined. */
|
||||
export interface ResolvedPreferences {
|
||||
apiKey: string;
|
||||
model: string;
|
||||
/**
|
||||
* Whether {@link ResolvedPreferences.model} came from the preference or from a
|
||||
* pick in the Models command.
|
||||
*
|
||||
* Carried rather than inferred, because the two disagree silently otherwise:
|
||||
* the setup screen would show one model while the chat used another, and the
|
||||
* user would have no way to tell which one to change.
|
||||
*/
|
||||
modelSource: ModelSource;
|
||||
/**
|
||||
* What the `Model` preference says, whatever is in use.
|
||||
*
|
||||
* Kept alongside `model` so a screen can answer the question a selection
|
||||
* raises: what would happen if I reset it?
|
||||
*/
|
||||
preferenceModel: string;
|
||||
host: string;
|
||||
systemPrompt: string;
|
||||
/**
|
||||
@@ -166,10 +185,15 @@ function describeSpan(milliseconds: number): string {
|
||||
|
||||
export function resolvePreferences(raw: LamPreferences): ResolvedPreferences {
|
||||
const thinking = (raw.thinking ?? "").trim();
|
||||
const model = (raw.model ?? "").trim() || DEFAULT_MODEL;
|
||||
|
||||
return {
|
||||
apiKey: (raw.apiKey ?? "").trim(),
|
||||
model: (raw.model ?? "").trim() || DEFAULT_MODEL,
|
||||
model,
|
||||
// Anything read straight from Vicinae is the preference by definition. The
|
||||
// selection is layered on top in `loadPreferences`.
|
||||
modelSource: "preference",
|
||||
preferenceModel: model,
|
||||
host: (raw.ollamaHost ?? "").trim() || DEFAULT_HOST,
|
||||
systemPrompt: (raw.systemPrompt ?? "").trim(),
|
||||
place: (raw.location ?? "").trim(),
|
||||
@@ -190,10 +214,57 @@ export function resolvePreferences(raw: LamPreferences): ResolvedPreferences {
|
||||
};
|
||||
}
|
||||
|
||||
export function getLamPreferences(): ResolvedPreferences {
|
||||
/**
|
||||
* Vicinae's own preferences, resolved and normalised. Not exported: everything
|
||||
* outside this module should go through {@link loadPreferences}, which also
|
||||
* applies the model chosen in the Models command.
|
||||
*/
|
||||
function getLamPreferences(): ResolvedPreferences {
|
||||
return resolvePreferences(getPreferenceValues<LamPreferences>());
|
||||
}
|
||||
|
||||
/**
|
||||
* The model chosen in the Models command, if there is one.
|
||||
*
|
||||
* Never throws. Storage that cannot be read means no selection rather than a
|
||||
* broken command, because the fallback (the preference) is always a valid
|
||||
* answer.
|
||||
*/
|
||||
export async function readModelSelection(): Promise<string | undefined> {
|
||||
const stored = await LocalStorage.getItem<string>(STORAGE_MODEL_KEY).catch(() => undefined);
|
||||
const trimmed = (stored ?? "").trim();
|
||||
return trimmed.length > 0 ? trimmed : undefined;
|
||||
}
|
||||
|
||||
/** Remembers a model as the one to use, taking precedence over the preference. */
|
||||
export async function selectModel(model: string): Promise<void> {
|
||||
await LocalStorage.setItem(STORAGE_MODEL_KEY, model.trim());
|
||||
}
|
||||
|
||||
/** Drops the selection, handing the model back to the `Model` preference. */
|
||||
export async function clearModelSelection(): Promise<void> {
|
||||
await LocalStorage.removeItem(STORAGE_MODEL_KEY).catch(() => undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* The preferences the rest of the extension should actually use.
|
||||
*
|
||||
* The only reader anyone else needs: it applies the model chosen in the Models
|
||||
* command on top of the preferences Vicinae holds. A second, synchronous reader
|
||||
* would be a trap, because it looks right and silently ignores the selection,
|
||||
* which is why `getLamPreferences` above is not exported.
|
||||
*
|
||||
* The model is the only thing a selection can override. Everything else here has
|
||||
* a single home in Vicinae's own settings, and adding a shadow copy of those
|
||||
* would make "where do I change this" a question with two answers.
|
||||
*/
|
||||
export async function loadPreferences(): Promise<ResolvedPreferences> {
|
||||
const base = getLamPreferences();
|
||||
const selected = await readModelSelection();
|
||||
|
||||
return selected ? { ...base, model: selected, modelSource: "selection" } : base;
|
||||
}
|
||||
|
||||
/** True when the extension cannot work until the user supplies a key. */
|
||||
export function needsSetup(preferences: ResolvedPreferences): boolean {
|
||||
return preferences.apiKey.length === 0;
|
||||
|
||||
Reference in New Issue
Block a user