Files
lam-agent-vicinae/src/lib/preferences.ts
T

100 lines
3.5 KiB
TypeScript

import { getPreferenceValues } from "@vicinae/api";
import { DEFAULT_HOST, DEFAULT_MODEL } from "./constants";
/**
* Preferences, read and normalised.
*
* Declared locally rather than relying on the `vicinae-env.d.ts` that the
* `vici` toolchain generates, so this file typechecks and unit-tests without a
* codegen step. The manifest in `package.json` remains the source of truth; if
* a preference is added there, mirror it here.
*/
export interface LamPreferences {
apiKey?: string;
model?: string;
ollamaHost?: string;
systemPrompt?: string;
webSearchEnabled?: boolean;
thinking?: string;
showToolCalls?: boolean;
showReasoning?: boolean;
resumeTimeout?: string;
}
/** Preferences with every optional value filled in, so callers never branch on undefined. */
export interface ResolvedPreferences {
apiKey: string;
model: string;
host: string;
systemPrompt: string;
webEnabled: boolean;
/** Undefined when reasoning is off, which is what the API client expects. */
think?: string;
/** Whether the transcript lists the tools Lam called. */
showToolCalls: boolean;
/** Whether the transcript includes the model's thinking trace. */
showReasoning: boolean;
/** Idle time after which opening the command starts a fresh chat. Undefined means never. */
resumeTimeoutMs?: number;
}
const MINUTE = 60_000;
const HOUR = 60 * MINUTE;
const DAY = 24 * HOUR;
/** Keep in sync with the `resumeTimeout` dropdown in `package.json`. */
const RESUME_TIMEOUTS: Record<string, number> = {
"15m": 15 * MINUTE,
"1h": HOUR,
"6h": 6 * HOUR,
"1d": DAY,
};
/** `undefined` for "never" and for anything unrecognised, which both mean "always reopen the last chat". */
export function resolveResumeTimeout(value: string | undefined): number | undefined {
return RESUME_TIMEOUTS[(value ?? "").trim()];
}
/** Human-readable form of {@link ResolvedPreferences.resumeTimeoutMs}, for the setup screen. */
export function describeResumeTimeout(milliseconds: number | undefined): string {
if (milliseconds === undefined) return "Never — always reopen the last chat";
const minutes = Math.round(milliseconds / MINUTE);
if (minutes < 60) return `After ${minutes} minutes`;
const hours = Math.round(minutes / 60);
if (hours < 24) return `After ${hours} hour${hours === 1 ? "" : "s"}`;
const days = Math.round(hours / 24);
return `After ${days} day${days === 1 ? "" : "s"}`;
}
export function resolvePreferences(raw: LamPreferences): ResolvedPreferences {
const thinking = (raw.thinking ?? "").trim();
return {
apiKey: (raw.apiKey ?? "").trim(),
model: (raw.model ?? "").trim() || DEFAULT_MODEL,
host: (raw.ollamaHost ?? "").trim() || DEFAULT_HOST,
systemPrompt: (raw.systemPrompt ?? "").trim(),
// Defaults to on: the preference is a checkbox the user has to actively untick.
webEnabled: raw.webSearchEnabled ?? true,
think: thinking && thinking !== "off" ? thinking : undefined,
showToolCalls: raw.showToolCalls ?? true,
showReasoning: raw.showReasoning ?? true,
// Falls back to the manifest's declared default, so a missing preference
// behaves the same whether Vicinae substitutes it or not.
resumeTimeoutMs: resolveResumeTimeout(raw.resumeTimeout ?? "1h"),
};
}
export function getLamPreferences(): ResolvedPreferences {
return resolvePreferences(getPreferenceValues<LamPreferences>());
}
/** True when the extension cannot work until the user supplies a key. */
export function needsSetup(preferences: ResolvedPreferences): boolean {
return preferences.apiKey.length === 0;
}