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 = { "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()); } /** True when the extension cannot work until the user supplies a key. */ export function needsSetup(preferences: ResolvedPreferences): boolean { return preferences.apiKey.length === 0; }