Three things. 1. Two new display preferences. "Show Tool Calls" and "Show Reasoning" control whether each answer lists the tools Lam called and the model's thinking trace. Both default on, so nothing changes on upgrade. 2. Fix: starting a new chat left the previous conversation in the right pane. The pane rendered the highlighted row's transcript, and Vicinae exposes no way to set the selected row, so the selection could stay put when cmd+N prepended a new chat. The pane now derives from the active session rather than the row, so it follows the chat the user is actually in whatever the selection happens to be. A side benefit is that mount now reads only session summaries, not every transcript. 3. "Start New Chat After" (default 1 hour) starts a fresh chat when the command opens and the last conversation is older than the window. "Never" restores the old always-reopen behaviour, for people who want to stay in their last chat. Old conversations are never deleted; they just sort into their recency section. The setup screen lists all three so the current values are visible without opening preferences. Verified: setup renders "Show Tool Calls: Shown", "Show Reasoning: Shown", "Start New Chat After: After 1 hour" with 17 models reachable. typecheck, eslint, 127 tests and vici build all pass.
204 lines
7.0 KiB
TypeScript
204 lines
7.0 KiB
TypeScript
import { Action, ActionPanel, Icon, List, openExtensionPreferences } from "@vicinae/api";
|
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
import { describeResumeTimeout, 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.Item
|
|
icon={Icon.BulletPoints}
|
|
title="Show Tool Calls"
|
|
subtitle={preferences.showToolCalls ? "Shown" : "Hidden"}
|
|
actions={settingsActions}
|
|
/>
|
|
<List.Item
|
|
icon={Icon.SpeechBubble}
|
|
title="Show Reasoning"
|
|
subtitle={preferences.showReasoning ? "Shown" : "Hidden"}
|
|
actions={settingsActions}
|
|
/>
|
|
<List.Item
|
|
icon={Icon.ArrowClockwise}
|
|
title="Start New Chat After"
|
|
subtitle={describeResumeTimeout(preferences.resumeTimeoutMs)}
|
|
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>
|
|
);
|
|
}
|