feat: list, pick and manage Ollama models from a command
This commit is contained in:
@@ -18,6 +18,7 @@ Hit a global shortcut, type a question, get a streamed answer without leaving wh
|
||||
- **Real markdown.** Tables, bulleted and numbered lists, headings, blockquotes, bold, italics and links all render natively, and the default prompt asks for the shape that fits the answer rather than flattening everything into prose.
|
||||
- **Tools.** Lam decides when to call them: current time, location context, web search, and page fetching. A turn that uses up its tool budget still ends in an answer, written from what it gathered, rather than a notice that it stopped.
|
||||
- **Tool calls you can watch and stop.** What Lam is doing right now stays on screen, including when it asks the same question twice, the round limit is a preference, Force Stop is always one keystroke away, and a call that repeats itself with identical arguments ends the turn instead of running forever.
|
||||
- **Models, from a list.** Browse every model your host can reach, pick one and start using it immediately, and on a server you run, pull new ones or delete old ones. Tag names are picked, never typed.
|
||||
- **Optional auto-deletion.** Set a retention window and Lam clears out the conversations you have stopped using the next time you open a command, taking their screenshots with them and collecting any capture you never sent. Off by default, and nothing at all is read or written while it is off. The setup screen also has both tidy-ups as explicit actions, confirmed first, for when you would rather press the button yourself.
|
||||
- **Screenshots, two ways.** Snip a region into the conversation you are already in from the action panel, or run **Snip to Lam** to start a new chat about it. The launcher window gets out of the way first, so it never ends up in the shot, and the capture appears in the transcript as a thumbnail with Open Screenshot for the full image. Neither is offered unless the current model can actually see images.
|
||||
- **Chat only.** Lam answers questions. It does not touch your files or run commands. The one exception is capture, and it only ever happens because you pressed the key for it.
|
||||
@@ -64,12 +65,12 @@ npm run build
|
||||
Setup is two prompts on first launch, then one command you can revisit any time.
|
||||
|
||||
1. **The API key.** Vicinae prompts for required preferences before a command runs, so the very first launch asks for your Ollama API key.
|
||||
2. **The model.** The next prompt asks which model to use. Type it exactly, e.g. `deepseek-v4.1-flash:cloud` or `gpt-oss:120b`.
|
||||
3. **`Set Up Lam Agent`** verifies the connection, lists every model your key can actually reach, and copies a model name to the clipboard so you can paste it into preferences.
|
||||
2. **The model.** The next prompt asks which model to use. Type it exactly, e.g. `deepseek-v4.1-flash:cloud` or `gpt-oss:120b`. Typing it by hand is the fragile part, so run **Ollama Models** afterwards and pick the real one from the list.
|
||||
3. **`Set Up Lam Agent`** verifies the connection, lists every model your key can actually reach, and reports which one is in use and where that came from.
|
||||
|
||||
To change the model later, either edit the **Model** preference directly, or run **Set Up Lam Agent** and copy the name of a model you would rather use.
|
||||
To change the model later, run **Ollama Models** and pick one, or edit the **Model** preference directly.
|
||||
|
||||
> An extension can read preferences but not write them, which is why setup hands you the exact name to paste rather than saving it for you. It also means the model list can never drift out of sync with your account.
|
||||
> An extension can read preferences but not write them, which is why a pick made in **Ollama Models** is stored by the extension and layered over the preference rather than saved into it. See [Models](#models) for how the two are reconciled, and for what a reset does.
|
||||
|
||||
## Keyboard shortcuts
|
||||
|
||||
@@ -115,6 +116,7 @@ vicinae 'vicinae://launch/lam-agent/chat?fallbackText=summarise%20this'
|
||||
| **Chat with Lam** | The chat surface. Bind this to your global shortcut. |
|
||||
| **Snip to Lam** | Capture a region and start a new chat about it. A `no-view` command, so the launcher window closes before the selector appears. Bind this to a second shortcut for one-key "snip and ask". |
|
||||
| **Chat Sessions** | Browse, reopen, and delete past conversations. |
|
||||
| **Ollama Models** | Browse the models the configured host can reach, pick the one Lam uses, and pull or delete models on a server you run. See [Models](#models). |
|
||||
| **Set Up Lam Agent** | Verify the connection, list available models, check whether the selected model can see images, and clear out chats or unattached photos. |
|
||||
|
||||
## Configuration
|
||||
@@ -122,7 +124,7 @@ vicinae 'vicinae://launch/lam-agent/chat?fallbackText=summarise%20this'
|
||||
| Preference | Default | Notes |
|
||||
| --- | --- | --- |
|
||||
| **Ollama API Key** | None | Required. Stored by Vicinae as a password preference. |
|
||||
| **Model** | None | Required. Any model your key can reach. |
|
||||
| **Model** | None | Required. Any model your key can reach. **Ollama Models** can pick one for you, and a pick takes precedence over this value until you reset it from the setup screen. |
|
||||
| **Ollama Host** | `https://ollama.com` | Set to `http://localhost:11434` for a local server. |
|
||||
| **Location** | None | The place Lam should treat as yours, for example `Cebu City, Philippines`. Optional. The only exact location available, and the only one that stops anything being inferred. |
|
||||
| **IP Geolocation** | off | Off by default. When on, and only when **Location** is empty, your IP is sent to ipwho.is to resolve a rough area. |
|
||||
@@ -135,6 +137,44 @@ vicinae 'vicinae://launch/lam-agent/chat?fallbackText=summarise%20this'
|
||||
| **Delete Old Chats After** | Disabled | Permanently deletes a conversation once you have not opened it for this long, along with any screenshots it captured. Nothing is ever deleted while this is off. See [Auto-deletion](#auto-deletion). |
|
||||
| **Tool Call Limit** | 8 rounds | How many rounds of tool calls Lam may make before it has to answer with what it has. `Unlimited` removes the cap. A call that repeats itself with identical arguments ends the turn either way. |
|
||||
|
||||
## Models
|
||||
|
||||
**Ollama Models** lists everything the configured host can reach, filtered by the search bar. Each row shows the parameter count and how much disk it takes, with tags for the model in use and for anything that reports image input. Picking one writes it to LocalStorage and `loadPreferences` layers it over the `Model` preference, so it takes effect on the next question with no restart.
|
||||
|
||||
| Action | Notes |
|
||||
| --- | --- |
|
||||
| **Use This Model** | The primary action. Immediate, and survives restarts. |
|
||||
| **Copy Model Name** | For pasting into the `Model` preference. |
|
||||
| **Delete Model** | Confirmed first. A server you run only. |
|
||||
| **Pull a Model…** | Confirmed by the download itself. A server you run only. |
|
||||
|
||||
### Why a pick is not written to the preference
|
||||
|
||||
It cannot be. `@vicinae/api` exposes `getPreferenceValues` and nothing that sets one, so the only way to change the preference would be to write Vicinae's own config file. That does not work: preferences are read once at startup and held in memory, so an edit to `~/.config/vicinae/settings.json` is invisible until Vicinae restarts. This was measured rather than assumed. Pointing `ollamaHost` at a local listener that logged every request, then launching a command, showed the requests still going to the old host.
|
||||
|
||||
Writing that file would also be a lost update, because Vicinae owns it and rewrites it (its own header warns as much), and it is undocumented, so it would break on any release that changes the layout.
|
||||
|
||||
So the pick lives in LocalStorage and the preference is left alone. The cost is stated rather than hidden: **while a pick is active, editing the `Model` preference has no effect.** That is why the setup screen shows both values, `ornith-1.5:9b (chosen in Models; the preference says gpt-oss:120b)`, and offers **Reset to the Preference**.
|
||||
|
||||
### Local servers versus Ollama Cloud
|
||||
|
||||
Pull and delete only appear when the host is not `ollama.com`, because they are operations on a machine you control.
|
||||
|
||||
A local daemon's list is often mixed, and the two kinds are worth telling apart:
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| **local** | Real weights on disk. Deleting frees the gigabytes the size column shows. |
|
||||
| **cloud** | A `:cloud` entry the daemon proxies to ollama.com (`remote_model`/`remote_host` in the API). Only a few hundred bytes are stored, so there is no size to show, and deleting drops the reference rather than any weights. |
|
||||
|
||||
Deleting is confirmed with wording that matches the kind, since "delete this model" means something different in each case. Deleting the model that is currently in use clears the pick, so the next question does not fail on a name that no longer exists. If the preference is what pointed at it, that is said instead: a preference cannot be edited from here.
|
||||
|
||||
### Pull
|
||||
|
||||
`POST /api/pull` streams its progress, and the toast follows it: the manifest status first, then `downloading · 42% · 1.2 GB / 2.9 GB`, then success. A bad name comes back as **HTTP 200 with the error in the body**, so the stream is checked and not just the status, which is the same trap the chat stream already had.
|
||||
|
||||
The download runs inside this command's worker, so **closing the launcher cancels a pull.** The command description says so, and the form says so again next to the field.
|
||||
|
||||
## Tools
|
||||
|
||||
Lam calls these on its own when they would improve an answer. All four are read-only, and all four are the entire list of things Lam is allowed to do without being asked twice. Screen capture is deliberately not among them.
|
||||
@@ -211,6 +251,7 @@ When a conversation cannot be read there is no way to know which captures are st
|
||||
src/
|
||||
chat.tsx # "chat" command entrypoint, thin wrapper
|
||||
snip.tsx # "snip" command entrypoint, no-view: captures then hands over
|
||||
models.tsx # "models" command: browse, pick, pull, delete
|
||||
sessions.tsx # browse, reopen, delete conversations
|
||||
setup.tsx # connection check + live model list
|
||||
lib/
|
||||
@@ -230,6 +271,7 @@ src/
|
||||
code.ts # code fences and links, repaired
|
||||
preferences.ts # preferences, normalised
|
||||
hooks/useChat.ts # the chat state machine
|
||||
hooks/usePreferences.ts # preferences + the model pick, loaded once
|
||||
hooks/useVision.ts # capability check, for gating the snip action
|
||||
snip.ts # the snip -> chat handoff contract
|
||||
components/ChatView.tsx # the chat surface
|
||||
@@ -273,6 +315,8 @@ The rendered height stays capped at 200px whatever is asked for (`Math.min(_rawH
|
||||
|
||||
**The logic layer has no Vicinae imports.** `agent.ts`, `ollama.ts`, `tools.ts` and friends are plain TypeScript, which is why they unit test without a runtime. A test enforces this.
|
||||
|
||||
**Preferences have exactly one reader.** `loadPreferences` applies the model chosen in **Ollama Models** on top of what Vicinae holds, and it is asynchronous because that pick lives in LocalStorage. `getLamPreferences` is deliberately not exported, because a second, synchronous reader would look correct and silently ignore the pick. Views take preferences through `hooks/usePreferences`, and the chat view is split in two so that `useChat` is only ever created once the model is known, rather than being pinned to whatever the preference said a moment earlier.
|
||||
|
||||
**Sessions are capped twice over**, once per turn and once per session, because `LocalStorage` has no documented ceiling.
|
||||
|
||||
**Screen capture is owned, not borrowed.** There is no screenshot call anywhere in `@vicinae/api`, and `Clipboard` moves text, html, urls and a file path and nothing binary, so a bitmap can never be read back out of the clipboard either. The only way to get a region of the screen is to shell out, which is fair game because extensions run under Node. The useful consequence is that the extension picks the output path and reads the file it wrote, rather than guessing what some other application left on the clipboard.
|
||||
|
||||
Reference in New Issue
Block a user