feat: initial Lam Agent extension (#1)
This commit was merged in pull request #1.
This commit is contained in:
+185
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user