Native Neovim frontend for Alma's local runtime. One Alma thread maps to one Neovim buffer; REST is used as the source of truth and WebSocket is used for fast updates and request submission.
alma.nvim is the Neovim adapter layer for Alma, the AI
coding and orchestration environment created by yetone.
It embeds Alma into Neovim with as little friction as possible: prompts, streamed
answers, tool calls, thread navigation, model selection, and workspace context all
live naturally inside the editor.
The plugin keeps Alma's memory, provider orchestration, skills, tools, agents, and local runtime as the source of intelligence, while Neovim stays responsible for the fast editing loop: buffers, windows, selections, diagnostics, quickfix, completion, and keyboard-native navigation.
With alma.nvim, you can skip the heavy Electron frontend when you are already
working in your favorite terminal editor. It brings Alma into the place where
you write, navigate, refactor, and review code every day, so AI assistance feels
like part of the editing flow rather than another app to context-switch into.
alma.nvim is also deeply influenced by
CopilotChat.nvim. Its
author is a loyal user and contributor of CopilotChat.nvim, and alma.nvim carries
forward that same belief that AI conversations should feel editor-native instead
of bolted on.
- Neovim 0.12.0 or newer.
snacks.pickerfromsnacks.nvimfor thread, workspace, buffer, event, model, tool, skill, and MCP navigation.blink.cmpfor Alma's completion source.curlinPATH.- Alma API running locally. The default is
http://127.0.0.1:23001; override withALMA_API_URLorrequire("alma").setup({ api_url = "..." }). - LuaRocks for dependency management. This MVP has no LuaRocks-managed runtime dependency beyond Neovim's Lua environment, but the rockspec is the canonical dependency/install manifest.
With a plugin manager, install snacks.nvim, blink.cmp, and this repository,
then call:
require("alma").setup({
api_url = vim.env.ALMA_API_URL or "http://127.0.0.1:23001",
model = nil,
reasoning_effort = nil,
window_layout = "float",
})With LuaRocks from this checkout:
luarocks make alma.nvim-scm-1.rockspec:AlmaHealthchecks Neovim,curl, and the Alma API.:Alma new [title]creates a new thread in the current workspace.:Alma pickand:AlmaThreadspick a thread from the current workspace.:AlmaThreadsGlobalkeeps the old global thread picker behavior explicit.:Alma open <thread_id>,:Alma toggle <thread_id>,:Alma float <thread_id>, and:Alma sidebar <thread_id>open or hide a specific Alma thread using the configured window layout behavior.:AlmaThreadOpen <thread_id>opens a thread with the configured Alma layout and fetches messages.:AlmaSubmit [prompt]submits prompt text. Without arguments it submits the editable bottom## Youcomposer in an Alma buffer.:AlmaStopsendsstop_generationover WebSocket.:AlmaThreads,:AlmaProjects,:AlmaBuffers,:AlmaEventsopensnacks.pickernavigation.:AlmaModels,:AlmaTools,:AlmaSkills,:AlmaMCPServersupdate thread-local request defaults.:AlmaToolDetails,:AlmaAgentCrew,:AlmaToggleBlock,:AlmaQuickfix,:AlmaBlockQuickfix,:AlmaDiffinspect, expand, or route tool output, native crew timelines, file locations, and patch-like output.
Register the source:
require("blink.cmp").setup({
sources = {
default = { "lsp", "path", "snippets", "buffer", "alma" },
providers = {
alma = {
name = "Alma",
module = "alma.completion.blink",
},
},
},
})Static completions work offline for /, @, $, and >. Dynamic models,
tools, skills, and MCP servers are fetched opportunistically from Alma API
catalog endpoints and cached with a TTL.
model and reasoning_effort default to nil, so each thread/request uses
Alma's backend default unless the user configures a default or selects one in the
thread composer. window_layout defaults to "float"; set it to "sidebar" to
open threads in the side panel by default.
By default alma.nvim resolves the current workspace as git root -> cwd -> current file directory. Override it with:
require("alma").setup({
resolve_workspace = function(ctx)
return { id = nil, name = "project", path = ctx.git_root or ctx.cwd or ctx.file_dir }
end,
})Token-only lines configure a request and are removed from the final prompt:
/skill:<id>enables a skill for the request./stopstops generation.@Bash,@Read,@Grep,@Glob,@Task,@mcp:<server>configure tools.$model:<id>,$reasoning:low|medium|high|xhigh,$temp:<n>,$no-toolsconfigure generation.>buffer,>selection,>diagnostics,>diff, and>file:<path>add structured ephemeral context metadata.
Unknown token-only lines are kept in the prompt and surfaced as warnings.
Markdown images like , ,
, , and data URI images are
sent as Alma file parts while the original markdown stays visible locally.
require("alma.hooks") provides a generic hook registry. Register callbacks with
hooks.on(name, callback) or hooks.register(name, callback); callbacks receive
one event table and are isolated with pcall, so one failing callback does not
stop later callbacks or the submit flow. Each dispatch also emits a matching
User autocmd whose event.data contains the same inspectable table.
Supported hooks and autocmds:
thread_opened->User AlmaThreadOpenedthread_changed->User AlmaThreadChangedbefore_submit->User AlmaBeforeSubmitrequest_compiled->User AlmaRequestCompiledafter_submit->User AlmaAfterSubmitgeneration_completed->User AlmaGenerationCompletedgeneration_error->User AlmaGenerationErrorproposal_received->User AlmaProposalReceived
local hooks = require("alma.hooks")
hooks.on("before_submit", function(event)
-- event.thread_id, event.thread, and event.spec are available here.
end)
vim.api.nvim_create_autocmd("User", {
pattern = "AlmaGenerationCompleted",
callback = function(event)
vim.print(event.data.thread_id)
end,
})require("alma.context") stores thread-scoped file or JSON attachments with
stable id-based dedupe. Pending attachments are appended to the next request's
ephemeralContext after before_submit hooks and before request compilation.
Inline JSON is sent as JSON context by default; set inline = false,
file_backed = true, provide path, or set max_inline_bytes to use a
file-backed JSON context. Submitted requests carry compact attachment metadata
labels/counts, not raw JSON content. once = true attachments are removed only
after the local submit dispatch succeeds, while persistent attachments remain
registered.
local context = require("alma.context")
context.attach("thread-id", {
type = "file",
id = "build-log",
path = "/tmp/build.log",
label = "Build log",
once = true,
})
context.attach("thread-id", {
type = "json",
id = "review-state",
title = "Review state",
content = { pending = 2 },
})
context.attach("thread-id", {
type = "json",
id = "review-snapshot",
title = "Review snapshot",
content = { files = 3 },
inline = false,
once = true,
})
local pending = context.list("thread-id")
local consumed = context.consume("thread-id")Proposal-like WebSocket events with generic proposal/files/diff payloads
are normalized to the proposal_received hook shape before dispatching
User AlmaProposalReceived.
Alma buffers use the dedicated alma filetype while registering markdown and
markdown_inline Tree-sitter services. If Tree-sitter is unavailable, they fall
back to legacy markdown syntax; otherwise they avoid stacking both highlighters.
They persist only chat text plus one-line placeholders for reasoning, tool
calls, raw events, and agent timeline events. Subagent streams render as
color-keyed Alma <agent> source titles without inserting delegated content into
the chat buffer, matching their role as specialized tool-call activity. Use
:AlmaAgentCrew or :Alma crews to inspect the current thread's native Alma
crew timeline, fetched from /api/threads/{id}/agent-crew and rendered as
missions, sprint progress, active steps, contracts, evaluations, handoffs, and
runs while hiding delegated message content. The same crew timeline also appears
as compact virtual progress lines below the bottom composer, so the current task
step stays visible without adding text to the buffer. Placeholder bodies are
rendered with extmark virtual lines when expanded via za or :AlmaToggleBlock,
so large tool payloads no longer inflate the markdown-like buffer. Use
:AlmaToolDetails for the full, untruncated payload.
Tool output rendering still uses a registry for expanded/detail views. Built-in
renderers format Bash, Read, Edit, Write, Grep, and Glob with
tool-specific code blocks, including standard ```diff blocks for editable
patches. Unknown tools fall back to raw Lua-style output.
require("alma").setup({
render = {
virtual_blocks = {
default_expanded = false,
max_lines = 80,
max_width = 180,
},
tool_outputs = {
mode = "smart", -- or "raw"
fallback = "raw",
renderers = {
MyTool = function(block)
return { "custom:", "```text", vim.inspect(block.output), "```" }
end,
},
},
},
})Submitting a request turns the bottom ## You composer into the sent user
message and shows progress with the lightweight spinner until the response
streams or reconciles. If Alma is already generating for the same thread, the
request is queued in Neovim and the buffer shows that queued state.
Completion or error events force REST reconciliation through
GET /api/threads/<id>/messages.
If no related WebSocket event arrives before the ack timeout, the buffer shows that the request was sent and starts REST polling fallback.
During streaming, the bottom ## You composer is only anchored when it is
already visible; if the user is reading elsewhere, alma.nvim preserves that view
and does not steal the scroll position. A lightweight TUI loading bar is rendered
near the active response while generation is in progress.
Run the local headless validation:
nvim --headless -u NONE -n -i NONE --cmd 'set rtp^=.' -l scripts/validate.luaRun a manual API check:
:AlmaHealth
:Alma new
:Alma pick
:Alma open <thread_id>
:Alma sidebar <thread_id>
:AlmaThreadOpen <thread_id>