Three changes from testing the launcher: 1. Remove the multi-line composer. It rendered as a small box floating in a large empty form. That is structural, not a styling miss: Vicinae always renders a Form.TextArea inside FormField, which reserves two ninths of the width for a label gutter and two ninths as an empty right spacer. With no way to reclaim that width, and no clean full-width variant available, prompts stay single line. 2. Resume the most recent conversation when the command opens. Every launch used to start a fresh chat, so there was no way to continue anything. Starting over is now explicit: cmd+N, or "New chat" in the dropdown. 3. One row per message instead of one row per turn. Storage keeps a question and its answer together because that is what gets replayed to the model; thread.ts flattens that so each side gets its own icon and weight. Rows are a fixed single line and the renderer draws title bright and subtitle muted, so the question goes in the subtitle and the answer in the title -- that is what dims the user and brightens Lam. Icons are Person and Stars. The detail panel now shows the whole thread rather than the selected message. Vicinae resets list selection to the first row on every model change and streaming changes the model on every flush, so the selected row cannot be relied on to stay put while an answer arrives. Verified by running it: the existing saved session resumed and rendered as four rows (two dim questions, two bright answers) with the thread in the detail panel. typecheck, eslint, 107 tests and vici build all pass.
180 lines
8.8 KiB
Markdown
180 lines
8.8 KiB
Markdown
# Lam Agent for Vicinae
|
|
|
|
Agentic AI chat inside the [Vicinae](https://vicinae.com) launcher, powered by [Ollama](https://ollama.com).
|
|
|
|
Hit a global shortcut, type a question, get a streamed answer — without leaving whatever you were doing.
|
|
|
|
<!-- prettier-ignore -->
|
|
> **Status:** early. Chat, streaming, persistent sessions and the web tools work. Anything that *acts* on your machine (opening links, clipboard, launching apps) is deliberately not here yet — see [Roadmap](#roadmap).
|
|
|
|
---
|
|
|
|
## What it does
|
|
|
|
- **Global hotkey, instant input.** Bind a shortcut to the chat command and the search bar is focused before you finish the keystroke.
|
|
- **Streaming answers.** Tokens render as they arrive, in a markdown side panel.
|
|
- **Persistent conversations.** Every chat is saved locally and can be reopened later.
|
|
- **Tools.** Lam decides when to call them — current time, location context, web search, and page fetching.
|
|
- **Chat only.** Lam answers questions. It does not touch your files or run commands.
|
|
|
|
## Requirements
|
|
|
|
- Vicinae with the extension runtime enabled (Node.js 20+ on `PATH`)
|
|
- An [Ollama](https://ollama.com) account and API key — [create one here](https://ollama.com/settings/keys)
|
|
|
|
Ollama Cloud is the default. A local Ollama server works too: point **Ollama Host** at `http://localhost:11434`. Note that the web tools are a cloud feature and are not available from a local server.
|
|
|
|
## Install
|
|
|
|
```sh
|
|
npm install
|
|
npm run dev # build + hot reload into a running Vicinae
|
|
```
|
|
|
|
Or build once:
|
|
|
|
```sh
|
|
npm run build
|
|
```
|
|
|
|
## Setup
|
|
|
|
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.
|
|
|
|
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.
|
|
|
|
> 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.
|
|
|
|
## Keyboard shortcuts
|
|
|
|
| Keys | Action |
|
|
| --- | --- |
|
|
| `↩` | Send the message in the search bar |
|
|
| `Tab` | Jump to the conversation switcher in the search bar |
|
|
| `⌘ N` | New chat |
|
|
| `⌘ ⇧ C` | Copy the highlighted message |
|
|
| `⌘ ⌫` | Delete a conversation (in **Chat Sessions**) |
|
|
|
|
Prompts are single line. An earlier `⌘ ↩` multi-line composer was removed — see [Architecture](#architecture) for why a `Form.TextArea` cannot be laid out cleanly in a launcher.
|
|
|
|
### Suggested global shortcuts
|
|
|
|
Assign these in **Vicinae Settings → Extensions → Lam Agent**, or bind them in your compositor/window manager with [deeplinks](https://docs.vicinae.com/deeplinks):
|
|
|
|
```sh
|
|
vicinae 'vicinae://launch/lam-agent/chat?toggle=true'
|
|
```
|
|
|
|
```ini
|
|
# Hyprland
|
|
bind = $mainAlt, Space, exec, vicinae 'vicinae://launch/lam-agent/chat?toggle=true'
|
|
|
|
# niri
|
|
Mod+Shift+Space repeat=false { spawn "vicinae" "vicinae://launch/lam-agent/chat?toggle=true"; }
|
|
```
|
|
|
|
`fallbackText` pre-fills the prompt, which makes a handy "ask about this" command:
|
|
|
|
```sh
|
|
vicinae 'vicinae://launch/lam-agent/chat?fallbackText=summarise%20this'
|
|
```
|
|
|
|
## Commands
|
|
|
|
| Command | Purpose |
|
|
| --- | --- |
|
|
| **Chat with Lam** | The chat surface. Bind this to your global shortcut. |
|
|
| **Chat Sessions** | Browse, reopen, and delete past conversations. |
|
|
| **Set Up Lam Agent** | Verify the connection and list available models. |
|
|
|
|
## Configuration
|
|
|
|
| Preference | Default | Notes |
|
|
| --- | --- | --- |
|
|
| **Ollama API Key** | — | Required. Stored by Vicinae as a password preference. |
|
|
| **Model** | — | Required. Any model your key can reach. |
|
|
| **Ollama Host** | `https://ollama.com` | Set to `http://localhost:11434` for a local server. |
|
|
| **System Prompt** | Lam's default | Replaces the built-in prompt entirely. |
|
|
| **Web Tools** | on | Enables `web_search` and `web_fetch`. |
|
|
| **Reasoning Effort** | Off | `low`/`medium`/`high` for models that support thinking. |
|
|
|
|
## Tools
|
|
|
|
Lam calls these on its own when they would improve an answer. All four are read-only.
|
|
|
|
| Tool | What it does |
|
|
| --- | --- |
|
|
| `get_datetime` | Current local date, time, weekday, timezone, and UTC offset. |
|
|
| `get_location` | Timezone, UTC offset, locale, region, and city. |
|
|
| `web_search` | Searches the web; returns titles, URLs, and snippets. |
|
|
| `web_fetch` | Fetches one page and returns its content as markdown. |
|
|
|
|
`get_location` is derived from your device timezone rather than GPS or an IP lookup, so no third-party geolocation service sees your address. Treat it as "which region am I in", not "where am I".
|
|
|
|
Web search results and fetched pages can be large. Lam truncates them before they reach the model, and gives up after 5 rounds of tool calls rather than looping.
|
|
|
|
## Architecture
|
|
|
|
```
|
|
src/
|
|
chat.tsx # "chat" command entrypoint — thin wrapper
|
|
sessions.tsx # browse, reopen, delete conversations
|
|
setup.tsx # connection check + live model list
|
|
lib/
|
|
ollama.ts # the only file that talks HTTP
|
|
agent.ts # the tool-calling loop
|
|
tools.ts # tool schemas + dispatch
|
|
sessions.ts # LocalStorage persistence
|
|
history.ts # turns -> API messages
|
|
location.ts # time/place, derived locally
|
|
thread.ts # turns -> flat message list
|
|
markdown.ts # messages -> detail-panel markdown
|
|
preferences.ts # preferences, normalised
|
|
hooks/useChat.ts # the chat state machine
|
|
components/ChatView.tsx # the chat surface
|
|
```
|
|
|
|
A few decisions worth knowing about before you change things:
|
|
|
|
**The search bar is the composer.** It is the only always-focused text input in a `List`, which is what makes "hotkey, then just type" work. It is single-line and there is no API to change that. A multi-line `Form.TextArea` composer was built and then removed: Vicinae always renders it inside `FormField`, which reserves two ninths of the width for a label gutter and two ninths as an empty right spacer, so it shows up as a small box floating in a large empty form. `filtering` is off, because this list is a transcript — the builtin fuzzy filter would hide messages as you type a question.
|
|
|
|
**Every transcript row shares one primary action**, so `↩` always sends whatever is in the search bar regardless of which row is selected. That is what makes the window feel like a single input box rather than a menu.
|
|
|
|
**Rows are messages, not turns.** Storage keeps a question and its answer together because that is what gets replayed to the model. `thread.ts` flattens that into one message per row so each side can carry its own icon and weight.
|
|
|
|
**The user's line is dimmed by putting it in `subtitle`.** List rows are a fixed single line, and the renderer draws `title` in the foreground colour and `subtitle` in a muted one. That is the only lever the extension API offers for varying text weight, so questions go in the subtitle and answers in the title.
|
|
|
|
**Messages render oldest first, and the detail panel shows the whole thread.** Vicinae resets list selection to the first row on every model change, and streaming changes the model on every flush, so the selected row cannot be relied on to stay put while an answer arrives. Showing the thread keeps the live answer on screen regardless.
|
|
|
|
**Streamed deltas are buffered in refs and flushed to state on a timer** (`FLUSH_INTERVAL_MS`). Re-rendering the transcript per token makes the launcher visibly janky.
|
|
|
|
**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.
|
|
|
|
**Sessions are capped twice over** — per turn and per session — because `LocalStorage` has no documented ceiling.
|
|
|
|
## Development
|
|
|
|
```sh
|
|
npm run dev # watch + reload
|
|
npm run typecheck
|
|
npm run lint:code
|
|
npm test
|
|
```
|
|
|
|
`vicinae-env.d.ts` is generated by the toolchain and gitignored; `src/lib/preferences.ts` declares the preference shape by hand so the logic layer needs no codegen step.
|
|
|
|
## Roadmap
|
|
|
|
- **Actions.** Opening links, clipboard access, launching apps. These need an approval step first — an assistant that can silently act on your machine is a different risk profile from one that answers questions, and this release does not pretend otherwise.
|
|
- **`@` mentions** for files and clipboard content.
|
|
- **Per-conversation model** overrides.
|
|
- **Image input**, which Ollama's chat API already supports.
|
|
|
|
## License
|
|
|
|
MIT — see [LICENSE](LICENSE).
|