feat: initial Lam Agent extension (#1)

This commit was merged in pull request #1.
This commit is contained in:
2026-10-03 20:32:40 +00:00
parent 1d39aabb50
commit 8b507e7953
36 changed files with 7136 additions and 2 deletions
+185
View File
@@ -0,0 +1,185 @@
import { Action, ActionPanel, Icon, List, openExtensionPreferences } from "@vicinae/api";
import { useCallback, useEffect, useMemo, useState } from "react";
import { getLamPreferences, needsSetup } from "./lib/preferences";
import { listModels } from "./lib/ollama";
import type { ModelInfo } from "./lib/types";
import { errorMessage } from "./lib/utils";
/**
* Setup and diagnostics.
*
* Two things are worth understanding about how configuration works here.
*
* First, Vicinae asks for `required` preferences with no default *before* the
* command starts. The API key and the model are both declared that way in the
* manifest, so the two-step "key, then model" onboarding happens on the very
* first launch without any code in this file.
*
* Second, an extension can read preferences but cannot write them: there is no
* `setPreferenceValues`. So this command does not save anything. It verifies
* the connection, lists the models the key can actually reach, and hands the
* exact model name to the clipboard so it can be pasted into settings. That is
* more useful than a form that silently fights the launcher's own settings UI,
* and it means the model list can never drift out of sync with the real one.
*/
/** Shows enough of a key to recognise it without printing the whole thing. */
function maskKey(apiKey: string): string {
if (apiKey.length === 0) return "Not set";
if (apiKey.length <= 8) return "•".repeat(apiKey.length);
return `${apiKey.slice(0, 4)}${"•".repeat(6)}${apiKey.slice(-4)}`;
}
export default function SetupCommand() {
const preferences = useMemo(() => getLamPreferences(), []);
const [models, setModels] = useState<ModelInfo[]>([]);
const [error, setError] = useState<string | undefined>(undefined);
const [isLoading, setIsLoading] = useState(false);
const [hasTested, setHasTested] = useState(false);
const missingKey = needsSetup(preferences);
const testConnection = useCallback(async () => {
setIsLoading(true);
setError(undefined);
try {
const result = await listModels({ host: preferences.host, apiKey: preferences.apiKey });
setModels(result);
} catch (thrown) {
setModels([]);
setError(errorMessage(thrown));
} finally {
setHasTested(true);
setIsLoading(false);
}
}, [preferences]);
// Verifying on open means the user sees something real immediately rather
// than having to guess whether their key works.
useEffect(() => {
if (!missingKey) void testConnection();
}, [missingKey, testConnection]);
const openPreferences = useCallback(() => {
void openExtensionPreferences();
}, []);
const settingsActions = (
<ActionPanel>
<Action title="Open Extension Preferences" icon={Icon.BulletPoints} onAction={openPreferences} />
<Action
title="Test Connection"
icon={Icon.ArrowClockwise}
onAction={() => void testConnection()}
/>
</ActionPanel>
);
const statusText = (() => {
if (missingKey) return "No API key yet";
if (isLoading) return "Checking…";
if (error) return "Connection failed";
if (hasTested) return `Connected · ${models.length} models`;
return "Not checked";
})();
const statusIcon = error || missingKey ? Icon.QuestionMarkCircle : Icon.SpeechBubble;
return (
<List
isLoading={isLoading}
navigationTitle="Lam Agent Setup"
searchBarPlaceholder="Search models…"
actions={settingsActions}
>
<List.Section title="Configuration">
<List.Item
icon={statusIcon}
title="Status"
subtitle={statusText}
actions={settingsActions}
accessories={error ? [{ tag: "Action needed" }] : []}
/>
<List.Item
icon={Icon.Person}
title="Ollama Host"
subtitle={preferences.host}
actions={settingsActions}
/>
<List.Item
icon={Icon.Person}
title="API Key"
subtitle={maskKey(preferences.apiKey)}
actions={settingsActions}
/>
<List.Item
icon={Icon.Stars}
title="Model"
subtitle={preferences.model}
actions={settingsActions}
/>
<List.Item
icon={Icon.BulletPoints}
title="Web Tools"
subtitle={preferences.webEnabled ? "Enabled" : "Disabled"}
actions={settingsActions}
/>
<List.Item
icon={Icon.Clock}
title="Reasoning Effort"
subtitle={preferences.think ?? "Off"}
actions={settingsActions}
/>
</List.Section>
{error ? (
<List.Section title="Last Error">
<List.Item
icon={Icon.QuestionMarkCircle}
title={error}
subtitle="Check the API key, then run Test Connection again"
actions={settingsActions}
/>
</List.Section>
) : null}
{missingKey ? (
<List.Section title="Next Step">
<List.Item
icon={Icon.QuestionMarkCircle}
title="Add your Ollama API key"
subtitle="Create one at ollama.com/settings/keys, then paste it into the extension preferences"
actions={
<ActionPanel>
<Action title="Open Extension Preferences" icon={Icon.BulletPoints} onAction={openPreferences} />
<Action.OpenInBrowser title="Create an API Key" url="https://ollama.com/settings/keys" />
</ActionPanel>
}
/>
</List.Section>
) : null}
{models.length > 0 ? (
<List.Section title="Available Models" subtitle={`${models.length} reachable with this key`}>
{models.map((model) => (
<List.Item
key={model.name}
id={model.name}
icon={Icon.Stars}
title={model.name}
subtitle={model.name === preferences.model ? "Currently selected" : undefined}
accessories={model.parameterSize ? [{ tag: model.parameterSize }] : []}
actions={
<ActionPanel>
<Action.CopyToClipboard title="Copy Model Name" content={model.name} />
<Action title="Open Extension Preferences" icon={Icon.BulletPoints} onAction={openPreferences} />
</ActionPanel>
}
/>
))}
</List.Section>
) : null}
</List>
);
}