feat: Ollama models command, with load/pull/delete and unload (#16)

This commit was merged in pull request #16.
This commit is contained in:
2026-10-05 09:43:00 +00:00
parent 05fd9ff13b
commit 26e05dea5b
22 changed files with 1948 additions and 65 deletions
+77 -6
View File
@@ -1,12 +1,14 @@
import { Action, ActionPanel, Alert, Icon, List, confirmAlert, openExtensionPreferences } from "@vicinae/api";
import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react";
import { Action, ActionPanel, Alert, Icon, List, Toast, confirmAlert, openExtensionPreferences, showToast } from "@vicinae/api";
import { useCallback, useEffect, useState, type ReactNode } from "react";
import { clearAllChats, clearOrphanedSnips, runAutoDelete } from "./lib/cleanup";
import { usePreferences } from "./lib/hooks/usePreferences";
import {
clearModelSelection,
describeAutoDelete,
describeMaxToolRounds,
describeResumeTimeout,
getLamPreferences,
needsSetup,
type ResolvedPreferences,
} from "./lib/preferences";
import { listModels } from "./lib/ollama";
import { forgetGeoPlace } from "./lib/geo";
@@ -40,7 +42,29 @@ function maskKey(apiKey: string): string {
}
export default function SetupCommand() {
const preferences = useMemo(() => getLamPreferences(), []);
const { preferences, reload } = usePreferences();
// The screen does nothing until the model is known, because half of what it
// reports is about that model: whether it can see images, and whether it is the
// one from preferences or from the Models command. Rendering it with the
// preference value and correcting it a moment later would flash a wrong answer
// at the user.
if (!preferences) {
return (
<List isLoading navigationTitle="Lam Agent Setup" searchBarPlaceholder="Loading preferences…" />
);
}
return <SetupScreen preferences={preferences} reloadPreferences={reload} />;
}
interface SetupScreenProps {
preferences: ResolvedPreferences;
/** Re-reads preferences after this screen changes the model selection. */
reloadPreferences: () => void;
}
function SetupScreen({ preferences, reloadPreferences }: SetupScreenProps) {
const [models, setModels] = useState<ModelInfo[]>([]);
const [error, setError] = useState<string | undefined>(undefined);
const [isLoading, setIsLoading] = useState(false);
@@ -98,6 +122,25 @@ export default function SetupCommand() {
void openExtensionPreferences();
}, []);
/**
* Hands the model back to the `Model` preference.
*
* The escape hatch for the one cost of keeping a selection: while one is set,
* editing the preference has no effect, and the only way to notice is this
* screen. Confirmed rather than immediate because it is a change the user
* cannot see the result of until the next question they ask.
*/
const resetModel = useCallback(async () => {
await clearModelSelection();
reloadPreferences();
await showToast({
style: Toast.Style.Success,
title: "Model reset to the preference",
message: preferences.preferenceModel,
});
}, [reloadPreferences, preferences.preferenceModel]);
/**
* Confirms, then runs a destructive tidy-up.
*
@@ -191,8 +234,36 @@ export default function SetupCommand() {
<List.Item
icon={Icon.Stars}
title="Model"
subtitle={preferences.model}
actions={settingsActions}
subtitle={
preferences.modelSource === "selection"
? `${preferences.model} (chosen in Models; the preference says ${preferences.preferenceModel})`
: preferences.model
}
actions={
preferences.modelSource === "selection" ? (
<ActionPanel>
<Action
title="Reset to the Preference"
icon={Icon.ArrowCounterClockwise}
onAction={() => void resetModel()}
/>
<ActionPanel.Section>
<Action
title="Open Extension Preferences"
icon={Icon.BulletPoints}
onAction={openPreferences}
/>
<Action
title="Test Connection"
icon={Icon.ArrowClockwise}
onAction={() => void testConnection()}
/>
</ActionPanel.Section>
</ActionPanel>
) : (
settingsActions
)
}
/>
<List.Item
icon={Icon.BulletPoints}