Files
lam-agent-vicinae/src/setup.tsx
T
adrianbonpin 9a791475d3 feat: display toggles, smart session resume, fix stale transcript pane
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.
2026-10-04 05:21:35 +08:00

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>
);
}