feat: initial Lam Agent extension for Vicinae

Agentic AI chat inside the Vicinae launcher, powered by Ollama.

- chat: the search bar is the composer (Return sends), cmd+return opens a
  multi-line composer, answers stream into a markdown detail panel, the
  newest turn renders first, and Tab reaches a conversation switcher in
  the search bar accessory
- sessions: browse, reopen, copy, and delete past conversations
- setup: verifies the Ollama connection and lists the models the key can
  actually reach, rather than trusting a hand-typed model name
- tools: get_datetime, get_location, web_search, web_fetch. All read-only,
  and the tool-calling loop is capped at 5 rounds

Notes on two constraints that shaped the design:

The search bar is single-line and there is no API to change it, so
"Return sends" and "cmd+return inserts a newline" cannot both live in one
field. They are split across the search bar and a pushed Form instead.

An extension can read preferences but cannot write them, so setup hands
over the exact model name to paste rather than saving it.

The logic layer (ollama, agent, tools, location, history, markdown) holds
no Vicinae imports and is unit tested without a runtime; a test enforces
that boundary. 77 tests cover NDJSON chunk-boundary parsing, the tool
loop, session caps, and timezone handling.
This commit is contained in:
2026-10-04 04:32:31 +08:00
parent 1d39aabb50
commit 121d644e90
36 changed files with 7136 additions and 2 deletions
+112
View File
@@ -0,0 +1,112 @@
{
"$schema": "https://raw.githubusercontent.com/vicinaehq/vicinae/refs/heads/main/extra/schemas/extension.json",
"name": "lam-agent",
"title": "Lam Agent",
"description": "Agentic AI chat powered by Ollama. Ask anything from a global hotkey, with streaming answers, persistent sessions, and web search.",
"icon": "extension-icon.png",
"author": "adrianbonpin",
"categories": ["Productivity", "Developer Tools"],
"license": "MIT",
"commands": [
{
"name": "chat",
"title": "Chat with Lam",
"subtitle": "Lam Agent",
"description": "Ask Lam anything. Answers stream in as they are generated, your session history is kept, and Lam can search the web when it needs current information.",
"mode": "view"
},
{
"name": "sessions",
"title": "Chat Sessions",
"description": "Browse, reopen, and clean up your past conversations with Lam.",
"mode": "view"
},
{
"name": "setup",
"title": "Set Up Lam Agent",
"description": "Check your Ollama connection, pick the model Lam should use, and tune how Lam answers.",
"mode": "view"
}
],
"preferences": [
{
"name": "apiKey",
"title": "Ollama API Key",
"description": "Your Ollama API key, created at ollama.com/settings/keys. Required to reach cloud models and the web search tools.",
"type": "password",
"required": true
},
{
"name": "model",
"title": "Model",
"description": "The model Lam should use, for example deepseek-v4.1-flash:cloud or gpt-oss:120b. Run \"Set Up Lam Agent\" to pick from the models your key can access.",
"type": "textfield",
"required": true
},
{
"name": "ollamaHost",
"title": "Ollama Host",
"description": "Base URL of the Ollama API. Leave this alone for Ollama Cloud, or point it at http://localhost:11434 to use a local Ollama server.",
"type": "textfield",
"required": false,
"default": "https://ollama.com"
},
{
"name": "systemPrompt",
"title": "System Prompt",
"description": "Extra instructions prepended to every conversation. Leave empty to use Lam's default prompt.",
"type": "textfield",
"required": false
},
{
"name": "webSearchEnabled",
"title": "Web Tools",
"label": "Let Lam search the web",
"description": "Let Lam search the web and fetch pages when a question needs up-to-date information.",
"type": "checkbox",
"required": false,
"default": true
},
{
"name": "thinking",
"title": "Reasoning Effort",
"description": "Ask supported models to reason before answering. Higher effort is slower but more accurate. Ignored by models that do not support it.",
"type": "dropdown",
"required": false,
"default": "off",
"data": [
{ "title": "Off", "value": "off" },
{ "title": "Low", "value": "low" },
{ "title": "Medium", "value": "medium" },
{ "title": "High", "value": "high" }
]
}
],
"dependencies": {
"@vicinae/api": "^0.29.0",
"zod": "^4.4.3"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
"@types/node": "22.13.10",
"@types/react": "19.1.17",
"eslint": "^9.22.0",
"eslint-config-prettier": "^10.1.8",
"globals": "^16.4.0",
"prettier": "^3.5.3",
"react": "19.1.1",
"typescript": "^5.8.2",
"typescript-eslint": "^8.45.0",
"vitest": "^4.1.5"
},
"scripts": {
"dev": "vici develop",
"build": "vici build",
"lint": "vici lint",
"lint:code": "eslint .",
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest"
}
}