Start · README.md
Overview
Leader is a Neovim config on top of NvChad v2.5, designed as a small framework: you clone it, run a script and get an editor ready to work, with what a developer uses every day and nothing more. Minimalism, fast productivity and performance: everything loads lazily, one file = one responsibility, and whatever you don't use is turned off with one line.
latest release · Neovim ≥ 0.11 · Linux, WSL, macOS and Windows · documentation · wallpapers
Installation
One command, everything done (Linux, WSL, macOS):
curl -fsSL https://raw.githubusercontent.com/sazardev/leader/master/install.sh | bash
Windows (PowerShell): irm https://raw.githubusercontent.com/sazardev/leader/master/install.ps1 | iex
It asks one thing: which languages to set up, as a checklist of packs (Go, Dart / Flutter, TypeScript / JavaScript, Web, Python, Rust, SQL, Swift, Java / Kotlin, Docker / Make / Prisma…) with the ones this machine already uses checked; Enter keeps them. Only the languages you keep download their LSPs, formatters and linters (the base, Lua, Markdown, JSON, YAML, TOML, is always on). Change them later in <Space>Sp → Languages. No clone needed: it fetches the repo into ~/.local/share/leader and does everything else (idempotent: run it again any time to bring it up to date):
- installs whatever is missing from the system (git, curl, C compiler, ripgrep, node, cmake) and Neovim ≥ 0.11 if you don't have it (in
~/.local/opt, withoutsudo; with brew on macOS); - links
nvim/as your config (~/.config/nvim); an existing one is moved tonvim.bak-<date>, never deleted; - enables the git hooks (
.githooks: cheatsheet always up to date and conventional commits); - writes your per-machine settings (
nvim/lua/local.lua, git-ignored): the languages you chose and the options below; - installs the plugins at the versions pinned in
nvim/lazy-lock.json(in parallel), the LSPs/formatters/linters (Mason) and builds the treesitter parsers (tree-sitter-clicomes from Mason), reporting the time each step took; - sets up the AI agent with the best backend it finds (Claude Code, OpenCode, Ollama…);
--agent noneskips it.
Options go after bash -s --, for example a Go laptop, fully configured:
curl -fsSL https://raw.githubusercontent.com/sazardev/leader/master/install.sh | bash -s -- --lang go,sql --theme gruvbox
Prefer to clone it yourself (to hack on it)? git clone https://github.com/sazardev/leader && cd leader && ./install.sh does exactly the same. Override the fetch with NVIM_CONFIG_DIR, NVIM_CONFIG_REPO and NVIM_CONFIG_BRANCH.
Then open nvim. Press <Space> and which-key shows you the menus.
There is also a landing page, docs/index.html (flat and square like the config, no build, self-hosted Geist in docs/fonts/, dark/light with t), and the documentation at docs/docs/index.html (generated from the Markdown by scripts/gen-docs.sh); serve docs/ with GitHub Pages (cd docs && python3 -m http.server).
| Option | Effect |
|---|---|
--ask | prompts before installing system packages or Neovim (by default it asks nothing) |
--lang python,sql | those packs (go dart typescript web python rust sql swift jvm infra yarnspinner) or single languages, all for every one; skips the checklist |
--yes | asks nothing: the packs found on this machine (also what happens with no terminal) |
--theme catppuccin | default theme (local.lua) |
--light / --no-light | forces light mode (no resident LSP) or turns it off; without this it is detected by RAM (≤ 2 GB) |
--agent auto | the AI agent (auto is the default and picks the best available), claude/haiku, ollama/qwen2.5-coder:7b@light…; none skips it |
--no-deps | doesn't touch system packages or Neovim |
--no-plugins | only links; plugins and tools install themselves the first time you open nvim |
--dry-run | shows what it would do |
./install.sh --lang go,sql --theme gruvbox --agent claude/haiku # from a clone: a Go laptop, everything configured
./install.sh --dry-run --no-deps --lang rust # see what it would do
Single languages also work with --lang: lua go typescript html css tailwind json yaml toml markdown docker prisma astro dart rust python java kotlin sql swift make yarnspinner. If local.lua already exists it is left untouched: the installer tells you what to add (it is yours and may hold other settings).
Native Windows (PowerShell): ./install.ps1 [-Lang go,typescript] [-Theme x] [-Light|-NoLight] [-Agent auto] [-NoPlugins] creates the junction at %LOCALAPPDATA%\nvim and does the same steps (written but not tested on real Windows; on WSL use install.sh).
Uninstall: delete the ~/.config/nvim link (and, if you want, ~/.local/share/nvim, ~/.local/state/nvim, ~/.cache/nvim).
Desktop app (Linux, WSL)
The same editor in its own window: leader, a GPU front-end for Neovim written in Rust (app/). It runs your Neovim with this config, so everything above works the same, but it draws the screen itself instead of a terminal: one draw call per frame, nothing running between keystrokes, and the flat design's half cells and borders drawn as exact rectangles. Add --app to the installer (or run app/install.sh from a clone): it installs Rust with rustup if needed, builds leader and adds Leader to the application menu (~/.local/bin/leader, ~/.local/share/applications).
From a release, in one command (Linux x86_64, glibc 2.31+: Ubuntu 20.04, Debian 11, Fedora 32 and newer; WSL with WSLg): it downloads the package, checks it, installs the app and, when they are missing, Neovim and this config; the first launch shows the welcome screen.
curl -fsSL https://github.com/sazardev/leader/releases/latest/download/leader-installer.sh | bash
From a package: Debian, Ubuntu, Mint, Pop!_OS download leader-editor-amd64.deb from the latest release and sudo apt install ./leader-editor-amd64.deb; Arch, CachyOS, Manjaro use the PKGBUILDs in packaging/arch (leader-editor-bin prebuilt, leader-editor from source; makepkg -si). The packages add the menu entry, the icons, the manual (man leader) and leader-setup, which installs the config for your user (the first launch with no Neovim config runs it in the window). Every release is signed and checksummed: how to verify it in packaging/README.md.
From a clone:
./install.sh --app # the config + the desktop app (built from source)
app/install.sh --uninstall # remove the app (the config stays)
- Open it with
leader [files…](anynvimargument works) or from the menu; files dropped on the window open in it. - The system clipboard works on its own (
"+y,"+p,Ctrl+Shift+V), no wl-copy/xclip/OSC 52 needed. - Its own title bar, painted with the editor's background so it is part of it (it steps back while a modal such as Telescope is open), in a smaller type than the code, laid out like Zed's. Leader's dotted N (it ripples on hover; also
F10) opens the app menu: File, Edit, Selection, View, Go, Run, Git (inside a repo), Agent, Window and Help, each a submenu whose items do exactly what Leader's own keys do and show them, plus the command palette (Ctrl+Shift+P: every menu item and every key of the config in one search). The project's name opens the projects menu (every folder you worked in, newest first; switching saves the session and restores the other project's, unsaved files are never dropped) and Open Project… (Ctrl+Shift+O) a full-screen picker: your recent projects and every git project under your home, with the one under the cursor described on the right (branch and changes, last commit, when you opened it, whether a session brings your windows back, its stack, its README); ⏎ opens it,oin a new window,dforgets it, plus a folder by path, a new project or a clone. ⌂ goes to the start screen, ↺ opens your recent files. Center: the current file's path, every part clickable. Right, live: what is running (LSP indexing with its progress, installs, a save: a dot spinner and a thin progress line), what is left to install, a macro being recorded, the file's git changes, unusual encodings or line endings; then minimize · maximize · close as three small flat squares. Configure it ingui.appbar:controls,buttons = { mark, project, home, history },activity,show,enabled = falsefor the system's title bar. - Full mouse: everything you can choose follows the pointer and answers a click. Menus light up the item under the pointer and open submenus as you pass over them; the project picker (and every full-screen list of the config) selects the row under the pointer with its details beside it, opens it on a click, and its key chips are buttons; modals light up their buttons; Telescope (the palette and every picker) selects on hover and opens on a click; the file tree opens a file or a folder with one click; the start screen's buttons follow the pointer. Over menus and pickers the pointer is an arrow, over text an I-beam.
- A guided setup opens on the first launch, full screen, in seven steps (each one says what it still needs and Next waits for it; keyboard or mouse): what this machine is (distro, package manager, GPU), a theme (previewed live as you move over it), your languages (the ones this machine uses already checked), a few features, an AI agent (the ones found here with their models, or one to install), what to install (missing system packages with the exact command for pacman, apt, dnf, zypper or apk, Leader's plugins, language tools and parsers, and the agent you asked for: nothing runs until you press Install, sudo asks in a terminal, each item shows its status) and a review with the install checks. It comes back with
Ctrl+Shift+W, Help › Welcome or the bar's "to finish" chip. Full-screen lists such as Open a project mark the chosen row with a bar and move with the arrows or hjkl. - Updates, connected: Leader looks for a newer version now and then (one tiny request, never more than every 12 hours, off with
update.enabled) and shows a↑ 1.9.0badge in the bar and a line at the top of the logo's menu. Click it for what changed, version by version, and update on request: the config is pulled with git (never merged or discarded), the plugins restored to the lockfile, the desktop app updated through its installer, and Leader offers to restart.Skiphides a version until a newer one; Help › Check for Updates looks right now. - Projects first, like an IDE: started from the menu, Leader asks which project to open (or reopens the last one, or shows the start screen: your choice);
leader ~/code/appopens that folder as the project. Pin the ones you use (pin the picker) to keep them at the top, or walk your folders Yazi-style (bin the picker:hup,lin,⏎open). Recent files (Space f o, the start screen) are the ones of the project you are in, not everything you ever opened;Ctrl+Athere widens it to every project. The window comes back with its size, place and maximized state, and its frame is yours to pick (Leader's square bar or the system's). - A toolbar you decide: a right click on the bar (or View › Customize Toolbar) turns each tool and chip on or off: project, home, recent files, command palette, file tree, terminal, problems, agent (pick the agent and model right there), theme (light/dark, the terminal's theme, a dark and a light list previewed as you hover, the whole catalog), settings, setup. Every tool shows a tooltip with its key.
- Window keys:
F10app menu,Ctrl+Shift+Pcommand palette,Ctrl+Shift+Oopen a project,F11fullscreen,Ctrl+Shift+Entermaximize,Ctrl+Shift+Mminimize,Ctrl+Shift+Nnew window,Ctrl+Shift+Hhome,Ctrl+Shift+Rrecent files,Ctrl+Shift+Wwelcome and checks. - Font:
gui.fontincore/settings.lua(guifontsyntax,"Family:h12");Ctrl+=/Ctrl+-/Ctrl+0resize it. - WSL: it opens through WSLg on the Windows GPU (Mesa's d3d12 driver), with no setup.
LEADER_TIMING=1 leaderprints where the startup time goes (~170 ms to the first frame on WSLg, GPU start included).
Philosophy
- Fast: ~20 ms startup (measured headless, with
--startuptime);defaults.lazy = true, plugins loaded by event/filetype/command/key, bytecode cache (vim.loader) and unneeded built-in plugins disabled. - Minimal: 54 entries in
lazy-lock.json, counting dependencies and NvChad's own. No heavy session manager, no tab bar, no embedded AI chat, no LSP dependency tree for things you don't use. - Modular: the config is declared, not wired. Adding a language, a plugin, an agent action or a Go helper means creating one file; nothing else is touched.
- Spend the minimum: whatever can be decided without a model is decided beforehand (gates, local analysis, treesitter completion); when one has to be called, only what is needed is sent and the cost is shown.
- Light where it matters: with ≤ 2 GB of RAM the resident processes (LSP, golangci-lint) are turned off automatically; force it with
NVIM_LIGHT=1|0. - Everything by keyboard, with keys documented and generated from the real code.
What's included
| Area | Contents |
|---|---|
| Languages | Go, TypeScript/JS/TSX, HTML/CSS/Tailwind/Astro, Python, Rust, SQL (PostgreSQL, SQLite), Swift (SwiftPM), Dart/Flutter, Java/Kotlin, Lua, Markdown, JSON/YAML/TOML, Docker, Make, Prisma, YarnSpinner. Each one declares its LSP, formatter, linter, parsers and its <leader> menu (it only exists in buffers of that filetype). Python, TypeScript, Rust and Swift share tests in the editor (the test under the cursor, marks per test, failures on their line), project setup presets and debugging; SQL runs the statement under the cursor against your database |
| Editing | undo word by word (like VS Code), U/Ctrl+Y redo, undo tree, the file at the previous save, a clipboard that x/c don't overwrite and a yank history ([y/]y after a paste, <leader>uy), blink.cmp, mini.pairs/surround/move, multicursor, flash (jumps), treesitter textobjects, folds (ufo) with foldable long comments, custom snippets |
| Navigation | Telescope (+fzf), nvim-tree, harpoon, aerial (symbols), grug-far (global replace), file history |
| Git | gitsigns, diffview, git-conflict, git-blame (off by default) and conventional commits (see below) |
| Diagnostics | inline blocks under their line, line number colored by severity, unique colors across the whole UI, line popup, copy as text (line, file or project), Trouble |
| UI | flat design (floats, menus and pickers are filled blocks, no boxes; every color derived from the theme, notifications, yank/accept flash), lualine, gruvbox ↔ light theme or live-inherited from the terminal (Alacritty), dashboard with braille art, render-markdown, inline colors |
| Sessions | one per folder and branch, periodic autosave |
| Go without an LLM | completion helpers that read your code and your go.mod (see below) |
| Dart / Flutter | run with an entry-point picker, debugging, tests inside the editor, build_runner watch, Riverpod/freezed completion (see below) |
| AI agent (optional) | <leader>a: one-shot actions on the function under the cursor, with an inline diff (see below) |
| Herdr (optional) | <leader>W: the workspace of agents — what every one of them is doing, live, and the setup (see below) |
Commits (<leader>G)
A single panel for committing: what goes in (staged vs not staged, with +/− per file), the message with live Conventional Commits validation (✔ feat(scope) · 38/72), assisted type and scope, and a line saying what the agent will do before spending anything. Gc opens the panel, Gm opens it with the AI draft, GM commits on its own, in the background (stages what is modified except sensitive files, the AI writes ONE message in English, it is validated and committed; if anything fails, it undoes what it staged) and Ga rewrites the last one. The AI message is always in English, on one line, in ~2 s with a light model.
AI agent (<leader>a)
It is not a plugin: it is Lua code in this config, with no dependencies, and it does nothing until you invoke it. Claude Code, OpenCode, Ollama (CPU/CUDA), LM Studio/llama.cpp/vLLM and other CLIs; it detects and configures its backends on its own (<leader>am → Setup, or ./scripts/agent-setup.sh). A short generic menu and one menu per level, each with its own folder and actions:
| Menu | What it does |
|---|---|
<leader>a | implement, review, explain, ask, document, refactor, t convert (JSON, SQL or curl into code for the file's language), hand off to Herdr; f inline fix (the line's diagnostic, with the type of what it points at) and F block fix (the whole function) |
aT tests | w writes only the missing tests at the end of the test file · m mock · f fixes a test by running it first (if it already passes, 0 tokens) · g gaps (local) |
aL lenses | security, performance, concurrency, errors, API compatibility: single-focus reviews as diagnostics; each one first discards what is not its concern |
aD / aP / aU | hexagonal architecture: domain, repositories and services, with local 0-token analysis |
aS setup | the documents agents read before they code: AGENTS.md (from the project, or a 0-token template), STACK.md (82 stacks in 10 kinds of project, games, data / AI and Neovim plugins included: Go stdlib/chi/Fiber/Huma, Bun + Hono, FastAPI, Axum, Next.js, Vite, Solid, Alpine.js, Flutter (Riverpod, Bloc, Supabase, offline-first, desktop), Bubble Tea and Ratatui TUIs, self-hosted Go + SQLite / PocketBase / Docker Compose…; then customize: Bun or npm, TypeScript 7, Biome/Oxlint, Tailwind 4, deploy (Docker self-hosted, Coolify, Fly, systemd, Kubernetes, Cloudflare, Vercel, stores, GoReleaser), CI, observability, quality; plus checked principles: SOLID, Clean Code, hexagonal, one file one responsibility…), DESIGN.md (17 design systems (Material 3, Ant, Fluent, Carbon, Atlassian, Polaris, Radix, Mantine, Geist…), 13 styles and 9 terminal designs; styles: neo-brutalism, Swiss, retro pixel, glass, mono minimal; terminal / TUI: ASCII art, Braille, brutal, classic, retro CRT, modern; then customize platforms, theme, palette, motion, effects, glyphs, density and accessibility; or described in your words) and SPEC.md (write it any way, it comes back with requirements, acceptance criteria and open questions; then add modules for what every app repeats: accounts, roles, lists, workspaces, notifications, audit log, uploads, import/export, offline, payments, subscriptions, with their edge cases and the questions only you can answer); then build from them: the whole project, one layer (foundation from STACK.md, domain from SPEC.md, design from DESIGN.md), one feature (a requirement of SPEC.md, or a new one you describe) or a recipe (prefab tasks per stack: a Flutter screen with its state, a Go endpoint, an entity with its repository…); coverage for free (which requirements have a test, what drifted from the documents), and every build leaves its decisions as ADRs and a PROGRESS.md the next agent reads |
aA advanced | several at once (a fix all, o / O fix from output, p plan; kept out of the everyday <leader>a): fix all the file's findings (one request per function), fix what a build printed (the last terminal, the quickfix or a command), a plan across files you edit before it runs; the proposals wait in a queue (] [ y l) |
GR | release: the changelog and the next version from your Conventional Commits (0 tokens), a PR description (one light request, to the clipboard), a release checklist from STACK.md's quality gates |
Gv | review what is staged before committing: secrets and leftovers for free, the code once, findings as diagnostics |
Around the proposals: Tab refines one instead of starting over, accepted code is verified for free (the LSP's new errors are compared with the old ones and it offers one more attempt with them), and a spend guard checks every request against your own history (it asks above a size, blocks at the caps you set). Also: clear findings one at a time, per file or all of them (<leader>ac, aC, :AgentClear), history in markdown with the code sent, filters and back navigation (<leader>ah), models per tier (cheap where it is enough). Details in docs/cheatsheets/agent.md.
Herdr (<leader>W)
Herdr is the terminal workspace that runs the coding agents. Leader reads it through its CLI: <leader>Wa opens the breakdown of the workspace — one row per agent (its state, its kind, where it is), the transcript of the one under the cursor and why Herdr thinks it is in that state, refreshed on its own. f brings the agent's pane to the front, s sends it a prompt. <leader>Ws is the setup: it detects the binary, the server and the agents' integrations, and installs what is missing (nothing without pressing the button).
It also goes the other way: with the registration on, this nvim appears in Herdr's Agents panel saying what it is doing (working while the agent runs, blocked while a proposal waits for you), and Herdr 0.10+ brings the pane back with your session after a server restart. Details in docs/cheatsheets/herdr.md.
Go without an LLM (smart completion, 0 tokens)
On top of a shared index (utils/goindex.lua: treesitter + go.mod, cached by date):
- missing methods to satisfy an interface, with the exact signature and bodies that call your dependency (
return s.repo.FindByID(ctx, id)); if err != nilwith the zero values of the function's returns and a message taken from what is called (the menu opens by itself afterx, err := …);- mappers between layers (
NoteResponse↔Note), constructor wiring (NewNoteService(noteRepo)with the arguments resolved), enum helpers (IsValid,Parse,UnmarshalText…), interface from a struct andvar _ I = (*T)(nil), HTTP handlers from a service (net/http, chi, gin, echo); - getters of the private fields (slices and maps as copies),
Reconstitute(the persistence constructor), the domain's repository port andpackageranked by the folder (mainonly where it makes sense); - struct tags, constructors, filled literals, sentinel errors, statement snippets. Details in docs/cheatsheets/go.md.
Dart / Flutter (<leader>d)
- run and debug through nvim-dap, with a picker of entry points (
lib/main*.dart, the last one first) and the device in the statusline; hot reload on save, the app's log in a split that keeps your focus; - tests inside the editor: the test under the cursor, the file or everything, a mark per test line, failures as diagnostics on the
expectthat failed, quickfix;<leader>dTjumps between a file and its test; - code generation:
build_runner watchin the background with its state in the statusline and its errors on their line; saving an.arbrunsgen-l10n; - completion for your stack, read from
pubspec.yaml: Riverpod providers, freezed classes, Consumer widgets, named after the file and with theirpartlines; pub outdated written next to each dependency,pub add. Details in docs/cheatsheets/dart-flutter.md.
Customizing
Everything configurable is in nvim/lua/core/settings.lua with its default value and a comment. Your changes go in nvim/lua/local.lua (git-ignored, merged on top; the installer can generate it with --lang, --theme, --light):
-- nvim/lua/local.lua
return {
theme = { name = "catppuccin" },
languages = { disabled = { "astro", "dart", "java", "kotlin" } },
agent = { backend = "ollama", tiers = { lens_security = "light" } },
git = { commit = { subject_max = 60 } },
}
You do not have to edit a file for the everyday ones: <leader>Sp opens the settings panel (theme, motion, cursor, format on save, diagnostics layout, answer language, light mode, plugins, tools, update…). Enter flips a value and applies it at once when it can (the few that need a restart say so); your choices are kept apart (stdpath("state")/prefs.json), win over local.lua, and "Reset my choices" forgets them. Keys of their own: St theme, Sf format on save, Sl plugins, SM tools, Si info, Su update the config.
Files have the same treatment: <leader>F (and a / r / d inside the tree) creates, renames, moves, duplicates and deletes in flat modals that show where the file will land. Several files at once (a.go; b/c.go; d/), a name with / moves it, the move is a folder picker, and delete goes to a trash (<leader>Fu brings the last one back). Open buffers, language servers and the tree follow a rename; your unsaved changes survive it.
To add (each one is a single file and, at most, a name in its NAMES list):
- a language:
nvim/lua/languages/<name>.lua(contract at the top oflanguages/init.lua); - a plugin: a file in
nvim/lua/plugins/<group>/; - a dashboard art:
nvim/lua/ui/arts/<name>.lua(braille only); - an AI backend: an entry in
agent.custom(no code) ornvim/lua/agent/backends/<name>.lua; - an agent action, lens or test:
agent/actions/,agent/lenses/actions/oragent/tests/actions/; - a per-language test convention:
agent/tests/convention.lua; - a Go completion helper: a
utils/go<name>.luamodule on top ofgoindexand its call ingosmart.
How it is organized
The principles (the same across the whole repo) and where they show:
- One responsibility per module: the pure parts (
commit/conventional.lua,utils/goindex.lua,agent/protocol.lua) kept apart from what touches git, the LSP or the UI (commit/git.lua,commit/panel.lua); one presenter per output kind (agent/output/). - Open for extension, closed for modification: discovered registries (
languages/,agent/actions/,agent/backends/,agent/lenses/); adding one doesn't force editing the others. The agent pipeline is extended with action hooks (retarget,merge,reveal,prepare), with guards (agent/guards/) and with events (agent/events.lua: verification, batches and the proposal queue subscribe; the presenter that accepts a proposal knows none of them). - Dependencies point to abstractions: each piece talks to a small contract (a backend's
build/parser, a context provider'scollect(target, done), a Go helper'sdeclaration_items(range, frag)), not to its neighbor's implementation. - Configurable things in a single place:
core/settings.lua; the scripts (install.sh,install.ps1) share the same settings logic (scripts/install-local.lua) instead of duplicating it.
Keys and documentation
- The documentation site: everything below in one searchable page (
/to search). It is generated from these Markdown files (scripts/gen-docs.sh; the pre-commit hook regenerates it). CHEATSHEET.md: all the keys. It is generated from the real keymaps (scripts/gen-cheatsheet.sh; the pre-commit hook regenerates it). Inside nvim, the dashboard's Mappings button shows the same.docs/cheatsheets/: what the code doesn't say (workflows, tools, pitfalls), one page per language and the agent's.AGENTS.md/CLAUDE.md: architecture and pitfalls for whoever touches the config (people or agents).
Structure
install.sh · install.ps1 installation (Linux/WSL/macOS · Windows)
VERSION config version
nvim/ the config (what gets linked)
init.lua bootstrap only
lua/core/ settings, options, keymaps and startup autocmds
lua/languages/ one file per language (declared; not wired)
lua/plugins/<group>/ one lazy spec per plugin
lua/menus/ contextual leader menus
lua/agent/ AI agent: backends, actions, context, output, tests, lenses, ddd, setup
lua/commit/ commits: validation (pure), git, AI draft, panel
lua/utils/ utilities; go*.lua = Go completion helpers on top of goindex
lua/ui/ appearance/ themes/ dashboard, theme, colors
lua/prefs/ settings panel (<leader>S): one file per group of toggles in items/
lua/files/ file operations (<leader>F): paths, file system, buffers, modals
snippets/ ftdetect/ syntax/ custom snippets and filetypes
scripts/ installation (install-*.lua), cheatsheet, agent setup and tests
docs/cheatsheets/ per-language and agent documentation
.githooks/ up-to-date cheatsheet + Conventional Commits
Maintenance
- Update: re-run the install command (or
git pull && ./install.sh): it brings the plugins to the versions pinned inlazy-lock.json; the config reloads itself when nvim opens. - Check:
scripts/test.shruns everything (agent and commits, Go helpers, installer and up-to-date cheatsheet; no network or tokens); each suite can also be run on its own:scripts/test-agent.sh,scripts/test-go.sh,scripts/test-install.sh,scripts/gen-cheatsheet.sh --check. - Commits: Conventional Commits format (
feat(scope): subject); thecommit-msghook validates it and<leader>Gc/Gm/GMassist with it.
Start · docs/cheatsheets/README.md
Workflows
Keys:CHEATSHEET.md(repo root) — the only source. It is generated by reading the real keymaps (scripts/gen-cheatsheet.sh) and regenerated on every commit; it is not edited by hand and its tables are not duplicated here. Inside nvim, the dashboard's Mappings button (NvCheatsheet) shows the same thing live. These pages hold what the code doesn't say: what each thing does, workflows, tools, pitfalls.<leader>=Space.
| Cheatsheet | What it covers |
|---|---|
| go.md | Go in depth: struct tags, snippets, contextual completion, <leader>g menu |
| agent.md | Inline agent (<leader>a): one-shot implement/review/explain/convert... with Claude, OpenCode or a local model (Ollama, OpenAI-compatible servers) |
| herdr.md | Herdr (<leader>W): the workspace of agents — what every one of them is doing, and the setup |
| editing.md | Undo, clipboard and yank history (<leader>u): undo word by word, redo keys, undo tree, the file at the previous save, yank list |
| gitignore.md | .gitignore (<leader>Gi): what it detects, the topics, how it merges with an existing file |
| tasks.md | Makefile tasks (<leader>R): the picker, writing targets it can list, the preset Makefiles per language |
| release.md | Release kit (<leader>GR): changelog and next version from Conventional Commits, PR description, release checklist |
| typescript.md | TypeScript / JavaScript (<leader>t): scripts, typecheck to quickfix, tests in the editor (vitest, jest, bun), biome or prettier, setup |
| python.md | Python (<leader>p): basedpyright + ruff, uv, tests in the editor (pytest), debugpy, setup |
| rust.md | Rust (<leader>r): rust-analyzer with clippy, cargo, tests in the editor, macros, codelldb, setup |
| sql.md | SQL (<leader>Q): run the statement under the cursor against PostgreSQL or SQLite, explain, schema |
| swift.md | Swift (<leader>i): SwiftPM, sourcekit-lsp, tests in the editor (XCTest, Swift Testing), setup |
| dart-flutter.md | Dart / Flutter (<leader>d): run and debug, tests inside the editor, build_runner, completion for Riverpod/freezed, pub |
| java-kotlin.md | Java and Kotlin (Gradle / Maven) |
| lua.md | Lua (the config itself) |
| markdown.md | Markdown |
| web.md | HTML, CSS, Tailwind, Astro |
| data-formats.md | JSON, YAML, TOML, Docker, Prisma, YarnSpinner |
How the leader works
Each language has its own letter under <leader> (g Go, d Dart, t TypeScript, p Python, r Rust, Q SQL, i Swift…) and only exists in buffers of that filetype; <leader>G (Git) only inside a repo, and <leader>a (agent), <leader>R (Makefile tasks), <leader>P (sessions), <leader>u (undo and yank history), <leader>S (settings), <leader>F (files), <leader>C (comments) and <leader>W (Herdr) are global. Press <leader> and which-key shows only what applies to the current file (no delay).
The language menus open an integrated terminal (toggleterm) and run the commands at the project root (they detect go.mod, package.json, Cargo.toml, pubspec.yaml, pom.xml/build.gradle...).
Workflows worth knowing
The exact keys are in CHEATSHEET.md; here, how they combine.
- Jump and come back: after
gd/grr/Ctrl+click,Escreturns to where you were (if you didn't edit there); it also closes popups and clears the highlight.Ctrl+o/Ctrl+iwalk the jump history. - Line diagnostic (
<leader>xd): opens a popup; press it again to enter it and copy the text. - Diagnostic blocks (
utils/diagblocks.lua):<leader>xadraws all of them under their line (a bar in the severity color, source/code and a2/5counter);<leader>xctoggles between all expanded and one by one (each one a single line, only the one on the cursor line opens);<leader>xn/xpjump to the next/previous and turn the view on in one-by-one mode. They show up on their own in every buffer with diagnostics (the text on the right,virtual_text, is off) and<leader>xaturns them off in that buffer; then only the tinted line number remains. In insert mode the blocks are hidden (nothing appears or pushes the code while you type) and come back when you leave withEsc;diagnostics.update_in_insert = truekeeps them live. Matched colors (ui/diagcolors.lua): theE2/W1label in the status bar, the line number and each block's label are the SAME pill (background = the theme'sDiagnosticError/Warn/Info/Hint, text = the theme background, bold), and the block's side bar uses that same accent; this way they change together with the theme (also with the theme inherited from the terminal and when Alacritty changes live:diagcolors.sync()detects the change on every status bar refresh and recomputes number and blocks, because base46 loads its cache without notifying through an event). Gutter: no icons either on the left or next to the code. The line number is drawn with the pill of the highest severity on the line (red error, yellow warning, blue info, cyan hint); the pill fits the digits, with a space on each side, so the number ends up centered (it is drawn by'statuscolumn', defined insettings.options, and lines split bywraponly leave the gap) and git is a thin┃bar attached to the number (signcolumn=no: the statuscolumn draws the sign in ONE cell; withsigncolumn=yeseach sign takes two and an extra gap is left) (green added, blue changed, red deleted), so they are not confused. Your choice (<leader>xaon/off,<leader>xcexpanded/collapsed) is global and saved instdpath("state")/diagblocks.json, so it comes back the same when you reopen nvim;settings.diagnostic_blocks(auto,mode,number) only gives the initial value.<leader>xn/xpwith the view off turn it on only in that buffer and don't touch what is saved. It updates on its own when diagnostics or the cursor change. The bottom panel (<leader>xx, Trouble) opens with focus:Enterjumps to the diagnostic and opens its popup, like<leader>xd. - Delete without clobbering the clipboard:
<leader>d+ motion (<leader>ddline,<leader>dwword) or in visual mode. - Multicursor (
multicursor.nvim, real cursors like VS Code/Helix; loaded on the first key): add cursors by line (Ctrl+↑/↓), by match of the word or selection (Alt+n/Alt+p,Alt+Shift+n/pskips one) or all at once (Alt+a);Alt+clickadds or removes one with the mouse. With several cursors everything (motions, operators, insert, undo) is repeated on each one;Escclears them,Ctrl+qturns the main cursor off/on,←/→switch cursor andAlt+xdeletes the current one. In insert mode, the secondary cursors replicate what was typed ON LEAVING insert; if the completion menu is open,Esc/jkclose it first and you have to press them again. Ideas:Alt+aon a word to rename it in the file;Ctrl+↓several times +I/Ato edit columns. The native visual block (Ctrl+v+I/A/c) is still there for simple columns. Don't useCtrl+n(it is nvim-tree). - Textobjects (treesitter): combine them with operators —
dafdeletes the function,difthe body,cifchanges it,yafcopies it,vafselects it;dacdeletes the class. For braces of any block:di{(inside),da{(with braces),%jumps to the pair. PageDown/PageUp jump block by block (function, method, class, struct/interface in Go); they don't stop at closures inside a function, accept a count (3<PageDown>) and, with no blocks or no parser, do the normal scroll. Home/End go to the start/end of the current block; pressing them again goes up to the enclosing block (method → class → file). For start/end of line use^/$(or0), and in insert they remain native. - Alt+hjkl move a line or block (mini.move); in Normal mode
Alt+his NvChad's horizontal terminal instead (the visual block still moves left). - Format:
<leader>fmformats (conform); it also formats on save. - Telescope: the preview is always visible.
flexlayout: on the right in windows ≥ 110 columns, at the bottom in narrow ones (lua/plugins/navigation/telescope.lua). - nvim-tree: opening a file closes the tree;
Ishows/hides what git ignores. Deleting a folder warns how many files it contains. - Terminal:
Alt+i(float),Alt+v(vertical) andAlt+h(horizontal) open NvChad's terminals,<leader>g…the toggleterm one.Esc(Terminal or its Normal mode) closes the window and the shell keeps running;Shift+Esconly hands the focus back to the previous window and leaves the terminal open.Shift+Escneeds a terminal that tells it apart fromEsc; if yours doesn't, it behaves likeEsc. - Completion (blink.cmp): the documentation appears instantly on selection and, for snippets, shows what will be inserted. Nothing comes preselected, except inside a Go struct tag (there
Enteraccepts the first suggestion). Sources: LSP, path, snippets, buffer; in Gogotagsandgosmartare added; in Lua,lazydev.- With the menu open,
Tab/Shift+Tabpick a suggestion (andEnterinserts it). Without a menu, inside a snippet (e.g.gorm.Open(dialector, opts)), they jump to the next / previous placeholder. - Keep what you typed and move on:
Esc(closes the menu without leaving the snippet) and thenTab. Esc(orjk) with the menu open only closes it and undoes the suggestion that was inserted (back to what you typed); you stay in insert mode and in your snippet. Without a menu, they leave insert as usual.
- With the menu open,
- Sessions: one per folder (and per git branch;
main/masteruse the base). They are not restored automatically: Restore Session button (rs) on the dashboard or its menu key. Opening a single file (nvim x) and quitting overwrites that folder's session; there is a key to not save on exit. - Git: git-blame is off by default (no cost); it is toggled from the Git menu.
- Conventional commits (
<leader>G, a single panel):Gcopens the panel,Gmopens it and immediately asks the AI for the draft,GMcommits on its own, without a panel (see below),Garewrites the last commit (warns if it is already on the remote). At the top you see what gets committed: what is staged (●, with +/− per file) and, separately, what does not go in (○, modified or new);Spacestages/unstages the file (or the whole group if you are on its header),astages everything,uunstages everything,porEntershow its diff. Below you write the message and the title is validated live (✔ feat(scope) · 38/72, or why it fails);Ctrl+tpicks the type (suggested from the files) andCtrl+othe scope (from the paths and the history). Underneath, a line says what the agent will do before spending anything (✦ AI claude/haiku · ⌃g to draft · reads 2 files, 3.1k chars: light model, only reads what is staged: its compact diff without lockfiles capped atgit.commit.diff_max_chars, the branch and the list of types) and, when done, what it did (drafted by … · 2.5s · 1.2k→39 tok · $0.0001);Ctrl+gasks for the draft (never on its own, except withGm), replaces the message and is undone withu, and repeating it gives another one. The message is always in English and on a single line (Conventional header; ~2 s with haiku).GMautomatic commit: no panel and in the background; if you already staged files it commits only those, otherwise it stages everything modified except sensitive files (git.commit.sensitive:.env, keys…), the AI writes the message, it is validated and committed, and a one-line notice gives hash, message, files, +/−, time and cost. If something fails (no backend, AI, invalid header, a hook) it undoes what it staged and leaves nothing half done (Gmto do it by hand).Ctrl+s(orEnterin normal mode) commits; with nothing staged or with an invalid message it warns and does nothing; if a hook rejects the commit the panel stays open so you can fix it.Tabtoggles files ↔ message,q/Esccancel (and stop the agent). The.githooks/commit-msghook requires the same format on anygit commit(types and length ingit.commit; if you change the list of types, change it in the hook too). - Comments (
<leader>C):Cdinserts the idiomatic documentation template for the function/type under the cursor (godoc, LuaCATS, JSDoc, docstring, rustdoc) with name, parameters and return already filled in;Cmlists in quickfix the public items that have no doc comment (for other languages use the agent:<leader>ad). Snippets in any language, with the language's own comment style:todo,fixme,note,hack,warn,perf,deprecated(and withoat the end, e.g.todoo,TODO(name):); they live innvim/snippets/all.json. - Theme: gruvbox ↔ gruvbox_light, or inherit the terminal's (Alacritty), which is followed live.
- Other:
:TSContext togglepins the function/class context at the top;za/zc/zofold (treesitter folds with nvim-ufo, everything open on start) and<leader>zfolds by what the code is:<leader>zfthe function you are in,ztthe type,zothe object/table literal,zbthe block (again = the one around it),zmthe methods of the current type,zF/zT/zOevery function/type/object in the file,zaoutline only,zeexpand all,zsfocus here (all closed except this),z1-z3fold everything deeper than that level (all after<leader>);<leader>Cfolds long comments (blocks or consecutive//lines ofcomments.min_linesor more, 4 by default):Ccfolds them all and remembers the preference (files you open afterwards, and future sessions, open with them already folded; it is saved instdpath("state")/comments.json),Ceunfolds them and stops folding them automatically,Cttoggles only the one under the cursor without touching the preference;comments.auto_collapseonly gives the initial value;:Lazy syncinstalls/updates plugins (the config auto-reload does not).
Light mode
On machines with ≤ 2 GB of RAM, LSP, go.nvim and golangci-lint are turned off (go vet remains). Treesitter, conform and nvim-lint keep running. Force it with NVIM_LIGHT=1|0. Per-machine settings in lua/local.lua (ignored by git).
Adding things
- Language:
lua/languages/<name>.lua+ add it toNAMESinlanguages/init.lua+ its page here. - Plugin: one file in
lua/plugins/<group>/. - New keymap: give it a
descof 2+ words starting with its section ("Harpoon add file"); the generator and NvCheatsheet group by the first word and drop single-word ones. - Go snippet:
nvim/snippets/go.json(VSCode format). - After changing keys or menus: nothing to edit by hand;
scripts/gen-cheatsheet.shregeneratesCHEATSHEET.md(thepre-commithook does it on its own).
Start · CHEATSHEET.md
Every key
Generated from the config's real keymaps (nvim_get_keymap,menus/,servers/on_attach.lua). Regenerated automatically on every commit (.githooks/pre-commit).<leader>=Space. This is the ONLY source of keys. What can't be derived from code (what each thing does, workflows, tools):docs/cheatsheets/.
Leader menus
Each menu has its own letter under <leader> and only exists where it applies: when pressing <leader> which-key only shows what belongs to the current buffer.
| Letter | Menu | Where it exists |
|---|---|---|
<leader>a | Agent | global |
<leader>aA | Agent Advanced | global |
<leader>aD | Agent DDD | global |
<leader>aX | Agent Doctor | global |
<leader>aL | Agent Lenses | global |
<leader>aP | Agent Repository | global |
<leader>aU | Agent Service | global |
<leader>aS | Agent Setup | global |
<leader>aT | Agent Tests | global |
<leader>s | CSS | css, scss, less |
<leader>C | Comments | global |
<leader>d | Dart / Flutter | dart |
<leader>F | File | global |
<leader>z | Fold | global |
<leader>G | Git | files inside a git repo |
<leader>g | Go | go, gomod, gowork |
<leader>W | Herdr | global |
<leader>u | History | global |
<leader>j | Java | java |
<leader>k | Kotlin | kotlin |
<leader>L | LSP | global |
<leader>m | Markdown | markdown |
<leader>p | Python | python |
<leader>R | Run | global |
<leader>r | Rust | rust |
<leader>Q | SQL | sql |
<leader>P | Session | global |
<leader>S | Settings | global |
<leader>i | Swift | swift |
<leader>t | TypeScript / JavaScript | typescript, typescriptreact, javascript, javascriptreact |
Agent — <leader>a
_Where: global_
| Key | Action |
|---|---|
<leader>ai | implement |
<leader>ar | review |
<leader>ae | explain |
<leader>aa | ask |
<leader>af | fix inline |
<leader>aF | fix block |
<leader>ad | docs |
<leader>at | convert |
<leader>an | next edit |
<leader>aR | refactor |
<leader>ag | hand off |
<leader>am | model |
<leader>ah | history |
<leader>ac | clear |
<leader>aC | clear file |
<leader>au | undo run |
<leader>ax | stop |
<leader>al | last answer |
Agent Advanced — <leader>aA
_Where: global_
| Key | Action |
|---|---|
<leader>aAa | fix all |
<leader>aAo | fix from output |
<leader>aAO | fix from a command |
<leader>aAp | plan across files |
<leader>aA] | next proposal |
<leader>aA[ | previous proposal |
<leader>aAy | accept all |
<leader>aAl | proposals list |
<leader>aAk | cache |
Agent DDD — <leader>aD
_Where: global_
| Key | Action |
|---|---|
<leader>aDi | ddd implement |
<leader>aDr | ddd review |
<leader>aDf | ddd fix |
<leader>aDm | ddd missing |
<leader>aDa | ddd ask |
<leader>aDd | ddd diagnose |
<leader>aDc | ddd clear |
Agent Doctor — <leader>aX
_Where: global_
| Key | Action |
|---|---|
<leader>aXx | doctor diagnose |
<leader>aXp | doctor paste |
<leader>aXc | doctor command |
<leader>aXd | doctor describe |
<leader>aXl | doctor log |
<leader>aXh | doctor health |
<leader>aXs | doctor selection |
<leader>aXr | doctor reopen |
Agent Lenses — <leader>aL
_Where: global_
| Key | Action |
|---|---|
<leader>aLs | lens security |
<leader>aLp | lens performance |
<leader>aLc | lens concurrency |
<leader>aLe | lens errors |
<leader>aLa | lens api |
Agent Repository — <leader>aP
_Where: global_
| Key | Action |
|---|---|
<leader>aPp | repo port |
<leader>aPi | repo implement |
<leader>aPr | repo review |
<leader>aPc | repo check |
Agent Service — <leader>aU
_Where: global_
| Key | Action |
|---|---|
<leader>aUi | service implement |
<leader>aUr | service review |
<leader>aUd | service deps |
Agent Setup — <leader>aS
_Where: global_
| Key | Action |
|---|---|
<leader>aSbb | setup build all |
<leader>aSbt | setup build foundation |
<leader>aSbs | setup build domain |
<leader>aSbd | setup build design |
<leader>aSbf | setup build feature |
<leader>aSbu | setup build undo |
<leader>aSr | setup recipe |
<leader>aSv | setup coverage |
<leader>aSl | setup leader data |
<leader>aSP | setup progress |
<leader>aSB | setup recheck |
<leader>aS/ | setup find |
<leader>aSo | setup overview |
<leader>aSa | setup agents |
<leader>aSA | setup agents template |
<leader>aSc | setup claude link |
<leader>aSt | setup stack |
<leader>aST | setup stack write |
<leader>aSk | setup stack addons |
<leader>aSp | setup principles |
<leader>aSd | setup design |
<leader>aSD | setup design write |
<leader>aSK | setup design addons |
<leader>aSw | setup screens |
<leader>aSs | setup spec |
<leader>aSm | setup spec modules |
Agent Tests — <leader>aT
_Where: global_
| Key | Action |
|---|---|
<leader>aTw | test write |
<leader>aTm | test mock |
<leader>aTf | test fix |
<leader>aTg | test gaps |
CSS — <leader>s
_Where: css, scss, less_
| Key | Action |
|---|---|
<leader>sf | format |
<leader>sc | toggle color preview |
<leader>so | outline |
Comments — <leader>C
_Where: global_
| Key | Action |
|---|---|
<leader>Cc | collapse long comments |
<leader>Ce | expand comments |
<leader>Ct | toggle comment under cursor |
<leader>Cd | doc comment template for this function/type |
<leader>Cm | public functions missing a doc comment (quickfix) |
Dart / Flutter — <leader>d
_Where: dart_
| Key | Action |
|---|---|
<leader>dr | run |
<leader>dd | debug |
<leader>ds | run profile |
<leader>dD | run on device |
<leader>dH | hide panels (toggle) |
<leader>dh | hot reload |
<leader>dR | hot restart |
<leader>dq | quit app |
<leader>de | emulators |
<leader>do | widget outline |
<leader>dl | log toggle |
<leader>dt | devtools |
<leader>dp | pub get |
<leader>dU | pub outdated |
<leader>dP | pub add |
<leader>dm | test nearest |
<leader>dM | test file |
<leader>dA | test all |
<leader>d. | test again |
<leader>dT | test file toggle |
<leader>dw | test output |
<leader>dv | analyze |
<leader>dx | dart fix --apply |
<leader>df | format |
<leader>dg | codegen watch (toggle) |
<leader>dG | codegen build once |
<leader>dL | codegen output |
<leader>db | breakpoint toggle |
<leader>dB | breakpoint condition |
<leader>dc | continue |
<leader>dn | step over |
<leader>di | step into |
<leader>dO | step out |
<leader>du | debug panel |
<leader>dzf | fold function here |
<leader>dzt | fold type here (struct, class, interface) |
<leader>dzo | fold object here (table, map, array) |
<leader>dzb | fold block here (again: the one around it) |
<leader>dzm | fold methods of this type |
<leader>dzF | fold all functions |
<leader>dzT | fold all types |
<leader>dzO | fold all objects |
<leader>dza | fold all (outline only) |
<leader>dze | expand all |
<leader>dzs | fold all but this (focus) |
<leader>dzu | unfold here and inside |
<leader>dzz | toggle fold here |
<leader>dz1 | fold level 1 |
<leader>dz2 | fold level 2 |
<leader>dz3 | fold level 3 |
File — <leader>F
_Where: global_
| Key | Action |
|---|---|
<leader>Fn | new file |
<leader>FN | new folder |
<leader>Fr | rename |
<leader>Fm | move |
<leader>Fc | duplicate |
<leader>Fx | delete |
<leader>Fu | undo delete |
<leader>Fy | copy path |
<leader>FY | copy full path |
<leader>Ft | show in tree |
<leader>Fo | open file |
Fold — <leader>z
_Where: global_
| Key | Action |
|---|---|
<leader>zf | function here |
<leader>zt | type here (struct, class, interface) |
<leader>zo | object here (table, map, array) |
<leader>zb | block here (again: the one around it) |
<leader>zm | methods of this type |
<leader>zF | all functions |
<leader>zT | all types |
<leader>zO | all objects |
<leader>za | all, outline only |
<leader>ze | expand all |
<leader>zs | all but this (focus) |
<leader>zu | unfold here and inside |
<leader>zz | toggle here |
<leader>z1 | level 1 (top-level folds open) |
<leader>z2 | level 2 |
<leader>z3 | level 3 |
Git — <leader>G
_Where: files inside a git repo_
| Key | Action |
|---|---|
<leader>Gc | commit |
<leader>Gm | commit + AI draft |
<leader>GM | commit auto |
<leader>Ga | amend |
<leader>Gv | review staged |
<leader>Gi | ignore rules |
<leader>GRc | release changelog |
<leader>GRp | release PR description |
<leader>GRl | release checklist |
<leader>Gt | status |
<leader>Gb | blame |
<leader>Gd | diff |
<leader>Gf | file history |
<leader>Gr | repo history |
<leader>Gq | close diff |
<leader>Go | conflict ours |
<leader>GT | conflict theirs |
<leader>GB | conflict both |
<leader>GN | conflict none |
<leader>Gn | conflict next |
<leader>Gp | conflict previous |
<leader>Gl | conflict list |
<leader>Gxt | conflict trivial |
<leader>Gxr | conflict agent |
<leader>GxR | conflict agent all |
<leader>Gxf | conflict files |
<leader>Gxs | conflict resolved |
Go — <leader>g
_Where: go, gomod, gowork_
| Key | Action |
|---|---|
<leader>ga | struct: add json tags (snake_case) |
<leader>gA | struct: add json tags + omitempty |
<leader>gD | struct: remove json tags |
<leader>gs | struct: fill literal |
<leader>ge | if err != nil for the call under the cursor |
<leader>gT | add test for the function under the cursor |
<leader>gr | run |
<leader>gb | build |
<leader>gv | lint (golangci-lint, or go vet if missing) |
<leader>gf | format |
<leader>gt | go mod tidy |
<leader>gx | golangci-lint --fix |
<leader>gm | go test ./... |
<leader>gR | go test -race ./... |
<leader>gc | go test -cover ./... |
<leader>gp | go fix -diff (modernizers preview) |
<leader>gi | go fix (apply modernizers) |
<leader>gg | go generate ./... |
<leader>gu | go get -u + go mod tidy |
<leader>gSl | setup golangci-lint config |
<leader>gSa | setup Air live reload (.air.toml) |
<leader>gSd | setup Dockerfile |
<leader>gSi | setup .dockerignore |
<leader>gSc | setup GitHub Actions CI |
<leader>gSh | setup git hooks (lefthook) |
<leader>gSe | setup .editorconfig |
<leader>gSp | setup several files |
<leader>gSm | setup Makefile |
<leader>gSg | setup gitignore |
Herdr — <leader>W
_Where: global_
| Key | Action |
|---|---|
<leader>Wa | agents (what the workspace is doing) |
<leader>Ws | setup (install and configure) |
History — <leader>u
_Where: global_
| Key | Action |
|---|---|
<leader>uu | undo tree |
<leader>uy | yank list |
<leader>u[ | previous save |
<leader>u] | next save |
<leader>ux | forget yanks |
Java — <leader>j
_Where: java_
| Key | Action |
|---|---|
<leader>jr | run |
<leader>jb | build |
<leader>jm | test |
<leader>jv | check / verify |
<leader>jc | clean |
Kotlin — <leader>k
_Where: kotlin_
| Key | Action |
|---|---|
<leader>kr | run |
<leader>kb | build |
<leader>km | test |
<leader>kv | check / verify |
<leader>kc | clean |
LSP — <leader>L
_Where: global_
| Key | Action |
|---|---|
<leader>Ls | stop (this buffer) |
<leader>LS | stop all (every project) |
<leader>Lr | restart (this buffer) |
<leader>Lp | pause or resume everything |
<leader>Li | info (what is loading) |
Markdown — <leader>m
_Where: markdown_
| Key | Action |
|---|---|
<leader>mr | toggle render |
<leader>md | toggle linter diagnostics (this buffer only) |
<leader>mz | reading mode (centered, distraction-free) |
<leader>ms | spell check (es, en) |
<leader>mw | word count and reading time |
<leader>mx | toggle checkbox |
<leader>mo | outline |
<leader>mf | format |
Python — <leader>p
_Where: python_
| Key | Action |
|---|---|
<leader>pr | run file |
<leader>pi | REPL with the file |
<leader>pv | ruff check |
<leader>px | ruff fix |
<leader>pf | format |
<leader>po | organize imports |
<leader>pa | uv add package |
<leader>pu | uv sync |
<leader>pU | uv lock upgrade |
<leader>pR | uv remove package |
<leader>pc | coverage |
<leader>pe | interpreter in use |
<leader>pm | test under cursor |
<leader>pM | test file |
<leader>pA | test all |
<leader>p. | test again |
<leader>pT | test file toggle |
<leader>pO | test output |
<leader>pDd | debug program |
<leader>pDt | debug test under cursor |
<leader>pDb | debug breakpoint toggle |
<leader>pDB | debug breakpoint condition |
<leader>pDc | debug continue |
<leader>pDn | debug step over |
<leader>pDi | debug step into |
<leader>pDo | debug step out |
<leader>pDu | debug panel |
<leader>pSr | setup ruff config |
<leader>pSt | setup pytest config |
<leader>pSv | setup .python-version |
<leader>pSd | setup Dockerfile (uv) |
<leader>pSi | setup .dockerignore |
<leader>pSc | setup GitHub Actions CI |
<leader>pSh | setup pre-commit hooks |
<leader>pSe | setup .editorconfig |
<leader>pSp | setup several files |
<leader>pSm | setup Makefile |
<leader>pSg | setup gitignore |
Run — <leader>R
_Where: global_
| Key | Action |
|---|---|
<leader>Rr | pick task |
<leader>Rl | last task |
<leader>Rd | default task |
<leader>Ra | last with args |
<leader>Rp | dry run |
<leader>Ro | output terminal |
<leader>Rq | output to quickfix |
<leader>Rs | stop task |
<leader>Rc | create Makefile |
Rust — <leader>r
_Where: rust_
| Key | Action |
|---|---|
<leader>rr | cargo run |
<leader>rb | cargo build |
<leader>rc | cargo check |
<leader>rv | cargo clippy |
<leader>rx | cargo clippy --fix |
<leader>rf | format |
<leader>ra | cargo add crates |
<leader>rR | cargo remove crates |
<leader>ru | cargo update |
<leader>ro | cargo doc --open |
<leader>re | expand macro under cursor |
<leader>rp | open Cargo.toml |
<leader>rE | explain error code |
<leader>rm | test under cursor |
<leader>rM | test file |
<leader>rA | test all |
<leader>r. | test again |
<leader>rT | test file toggle |
<leader>rO | test output |
<leader>rDd | debug program |
<leader>rDt | debug test under cursor |
<leader>rDb | debug breakpoint toggle |
<leader>rDB | debug breakpoint condition |
<leader>rDc | debug continue |
<leader>rDn | debug step over |
<leader>rDi | debug step into |
<leader>rDo | debug step out |
<leader>rDu | debug panel |
<leader>rSf | setup rustfmt config |
<leader>rSt | setup rust-toolchain |
<leader>rSl | setup clippy config |
<leader>rSy | setup cargo-deny (licenses, advisories) |
<leader>rSd | setup Dockerfile |
<leader>rSi | setup .dockerignore |
<leader>rSc | setup GitHub Actions CI |
<leader>rSe | setup .editorconfig |
<leader>rSp | setup several files |
<leader>rSm | setup Makefile |
<leader>rSg | setup gitignore |
SQL — <leader>Q
_Where: sql_
| Key | Action |
|---|---|
<leader>Qr | run statement or selection |
<leader>Qa | run file |
<leader>Qe | explain |
<leader>QE | explain analyze |
<leader>Qt | tables |
<leader>Qd | describe table under cursor |
<leader>Qc | connection |
<leader>Ql | last run again |
<leader>Qf | format |
Session — <leader>P
_Where: global_
| Key | Action |
|---|---|
<leader>Pr | restore session (this folder) |
<leader>Ps | select session (any project) |
<leader>Pl | restore last session |
<leader>Pw | save session now |
<leader>Px | don't save on exit |
Settings — <leader>S
_Where: global_
| Key | Action |
|---|---|
<leader>Sp | panel |
<leader>St | toggle theme |
<leader>Sf | format on save |
<leader>Sl | plugins |
<leader>SM | tools |
<leader>Si | info |
<leader>Su | updates |
Swift — <leader>i
_Where: swift_
| Key | Action |
|---|---|
<leader>ir | run |
<leader>ib | build |
<leader>iB | build release |
<leader>if | format |
<leader>iF | format the package |
<leader>iv | swiftlint |
<leader>iu | package update |
<leader>ip | package resolve |
<leader>ic | package clean |
<leader>io | open Package.swift |
<leader>im | test under cursor |
<leader>iM | test file |
<leader>iA | test all |
<leader>i. | test again |
<leader>iT | test file toggle |
<leader>iO | test output |
<leader>iDd | debug program |
<leader>iDb | debug breakpoint toggle |
<leader>iDB | debug breakpoint condition |
<leader>iDc | debug continue |
<leader>iDn | debug step over |
<leader>iDi | debug step into |
<leader>iDo | debug step out |
<leader>iDu | debug panel |
<leader>iSf | setup swift-format config |
<leader>iSl | setup SwiftLint config |
<leader>iSd | setup Dockerfile |
<leader>iSi | setup .dockerignore |
<leader>iSc | setup GitHub Actions CI |
<leader>iSe | setup .editorconfig |
<leader>iSp | setup several files |
<leader>iSm | setup Makefile |
<leader>iSg | setup gitignore |
TypeScript / JavaScript — <leader>t
_Where: typescript, typescriptreact, javascript, javascriptreact_
| Key | Action |
|---|---|
<leader>tr | run a package.json script (picker) |
<leader>td | dev (script dev) |
<leader>tb | build (script build) |
<leader>tv | lint (script lint, else eslint .) |
<leader>tc | typecheck to the quickfix list |
<leader>tx | eslint --fix (current file) |
<leader>tf | format |
<leader>to | organize imports |
<leader>tI | add missing imports |
<leader>tF | fix all (TypeScript's quick fixes) |
<leader>tg | go to source definition |
<leader>ti | install dependencies |
<leader>ta | add packages |
<leader>tR | remove packages |
<leader>tu | outdated packages |
<leader>tm | test under cursor |
<leader>tM | test file |
<leader>tA | test all |
<leader>t. | test again |
<leader>tT | test file toggle |
<leader>tO | test output |
<leader>tSt | setup tsconfig |
<leader>tSb | setup Biome (lint + format) |
<leader>tSl | setup ESLint (typescript-eslint) |
<leader>tSf | setup Prettier |
<leader>tSv | setup Vitest config |
<leader>tSd | setup Dockerfile |
<leader>tSi | setup .dockerignore |
<leader>tSc | setup GitHub Actions CI |
<leader>tSn | setup .nvmrc |
<leader>tSe | setup .editorconfig |
<leader>tSp | setup several files |
<leader>tSm | setup Makefile |
<leader>tSg | setup gitignore |
LSP (when a server attaches)
Buffer-local keymaps from servers/on_attach.lua.
| Key | Action |
|---|---|
<leader>ca | code action |
gd | go to DEFINITION |
grr | see WHERE IT'S USED (list) |
Autocompletion (blink.cmp, insert mode)
Preset enter plus the overrides from plugins/editing/blink.lua. → = tried in order until one applies.
| Key | Action |
|---|---|
<C-b> | scroll documentation up |
<C-e> | close menu |
<C-f> | scroll documentation down |
<C-k> | show signature help → hide signature help |
<C-n> | next item |
<C-p> | previous item |
<C-space> | open menu → show documentation → hide documentation |
<CR> | accept Go ErrX ghost text + new line → accept item |
<Esc> | close menu |
<S-Tab> | previous item → jump to previous snippet placeholder (when no menu is open) |
<Tab> | accept Go ErrX ghost text → next item → jump to next snippet placeholder (when no menu is open) |
nvim-tree (inside the tree)
Ctrl+n opens/closes it and <leader>e focuses it (see Global shortcuts). g? inside the tree lists these same keys.
| Key | Action |
|---|---|
- | Up |
. | Run Command |
<2-LeftMouse> | Open |
<2-RightMouse> | CD |
<BS> | Close Directory |
<C-]> | CD |
<C-e> | Open: In Place |
<C-k> | Info |
<C-r> | Rename: Omit Filename |
<C-t> | Open: New Tab |
<C-v> | Open: Vertical Split |
<C-x> | Open: Horizontal Split |
<CR> | Open file and close tree |
<Del> | Delete |
<Tab> | Open Preview |
<lt> | Previous Sibling |
> | Next Sibling |
B | Toggle Filter: No Buffer |
C | Toggle Filter: Git Clean |
D | Trash |
E | Expand All |
F | Live Filter: Clear |
H | Toggle Filter: Dotfiles |
I | Toggle Filter: Git Ignored |
J | Last Sibling |
K | First Sibling |
L | Toggle Group Empty |
M | Toggle Filter: No Bookmark |
O | Open: No Window Picker |
P | Parent Directory |
R | Refresh |
S | Search |
U | Toggle Filter: Custom |
W | Collapse All |
Y | Copy Relative Path |
[c | Prev Git |
[e | Prev Diagnostic |
]c | Next Git |
]e | Next Diagnostic |
a | New file or folder |
bd | Delete Bookmarked |
bmv | Move Bookmarked |
bt | Trash Bookmarked |
c | Copy |
d | Delete |
e | Rename: Basename |
f | Live Filter: Start |
g? | Help |
ge | Copy Basename |
gp | Move |
gy | Copy Absolute Path |
m | Toggle Bookmark |
o | Open |
p | Paste |
q | Close |
r | Rename |
s | Run System |
u | Rename: Full Path |
x | Cut |
y | Copy Name |
Global shortcuts
Sections = first word of the desc (same as the dashboard's Mappings button).
Agent
| Key | Mode | Action |
|---|---|---|
<leader>aAO | n | Fix from a command |
<leader>aA[ | n | Previous proposal |
<leader>aA] | n | Next proposal |
<leader>aAa | n | Fix all |
<leader>aAk | n | Cache |
<leader>aAl | n | Proposals list |
<leader>aAo | n | Fix from output |
<leader>aAp | n | Plan across files |
<leader>aAy | n | Accept all |
<leader>aC | n | Clear file |
<leader>aDa | n,x | Ddd ask |
<leader>aDc | n | Ddd clear |
<leader>aDd | n | Ddd diagnose |
<leader>aDf | n,x | Ddd fix |
<leader>aDi | n,x | Ddd implement |
<leader>aDm | n,x | Ddd missing |
<leader>aDr | n,x | Ddd review |
<leader>aF | n,x | Fix block |
<leader>aLa | n,x | Lens api |
<leader>aLc | n,x | Lens concurrency |
<leader>aLe | n,x | Lens errors |
<leader>aLp | n,x | Lens performance |
<leader>aLs | n,x | Lens security |
<leader>aPc | n | Repo check |
<leader>aPi | n,x | Repo implement |
<leader>aPp | n,x | Repo port |
<leader>aPr | n,x | Repo review |
<leader>aR | n,x | Refactor |
<leader>aS/ | n | Setup find |
<leader>aSA | n | Setup agents template |
<leader>aSB | n | Setup recheck |
<leader>aSD | n | Setup design write |
<leader>aSK | n | Setup design addons |
<leader>aSP | n | Setup progress |
<leader>aST | n | Setup stack write |
<leader>aSa | n | Setup agents |
<leader>aSbb | n | Setup build all |
<leader>aSbd | n | Setup build design |
<leader>aSbf | n | Setup build feature |
<leader>aSbs | n | Setup build domain |
<leader>aSbt | n | Setup build foundation |
<leader>aSbu | n | Setup build undo |
<leader>aSc | n | Setup claude link |
<leader>aSd | n | Setup design |
<leader>aSk | n | Setup stack addons |
<leader>aSl | n | Setup leader data |
<leader>aSm | n | Setup spec modules |
<leader>aSo | n | Setup overview |
<leader>aSp | n | Setup principles |
<leader>aSr | n | Setup recipe |
<leader>aSs | n | Setup spec |
<leader>aSt | n | Setup stack |
<leader>aSv | n | Setup coverage |
<leader>aSw | n | Setup screens |
<leader>aTf | n,x | Test fix |
<leader>aTg | n | Test gaps |
<leader>aTm | n,x | Test mock |
<leader>aTw | n,x | Test write |
<leader>aUd | n | Service deps |
<leader>aUi | n,x | Service implement |
<leader>aUr | n,x | Service review |
<leader>aXc | n | Doctor command |
<leader>aXd | n | Doctor describe |
<leader>aXh | n | Doctor health |
<leader>aXl | n | Doctor log |
<leader>aXp | n | Doctor paste |
<leader>aXr | n | Doctor reopen |
<leader>aXs | x | Doctor selection |
<leader>aXx | n | Doctor diagnose |
<leader>aa | n,x | Ask |
<leader>ac | n,x | Clear |
<leader>ad | n,x | Docs |
<leader>ae | n,x | Explain |
<leader>af | n,x | Fix inline |
<leader>ag | n,x | Hand off |
<leader>ah | n | History |
<leader>ai | n,x | Implement |
<leader>al | n | Last answer |
<leader>am | n | Model |
<leader>an | n,x | Next edit |
<leader>ar | n,x | Review |
<leader>at | n,x | Convert |
<leader>au | n | Undo run |
<leader>ax | n | Stop |
Clipboard
| Key | Mode | Action |
|---|---|---|
C | n,x | C without copying |
X | n,x | X without copying |
c | n,x | C without copying |
p | x | Paste over keeps it |
x | n,x | X without copying |
Comment
| Key | Mode | Action |
|---|---|---|
<leader>/ | n,x | Comment |
Comments
| Key | Mode | Action |
|---|---|---|
<leader>Cc | n | Collapse long comments |
<leader>Cd | n | Doc comment template for this function/type |
<leader>Ce | n | Expand comments |
<leader>Cm | n | Public functions missing a doc comment (quickfix) |
<leader>Ct | n | Toggle comment under cursor |
Diagnostics
| Key | Mode | Action |
|---|---|---|
<leader>xa | n | Blocks inline (toggle) |
<leader>xc | n | Blocks collapse / expand |
<leader>xd | n | Line popup (press again to enter) |
<leader>xe | n | Copy all |
<leader>xf | n | Copy file |
<leader>xl | n | Copy line |
<leader>xn | n | Next block |
<leader>xp | n | Previous block |
<leader>xx | n | Project list |
File
| Key | Mode | Action |
|---|---|---|
<leader>FN | n | New folder |
<leader>FY | n | Copy full path |
<leader>Fc | n | Duplicate |
<leader>Fm | n | Move |
<leader>Fn | n | New file |
<leader>Fo | n | Open file |
<leader>Fr | n | Rename |
<leader>Ft | n | Show in tree |
<leader>Fu | n | Undo delete |
<leader>Fx | n | Delete |
<leader>Fy | n | Copy path |
Find
| Key | Mode | Action |
|---|---|---|
<leader>H | n | File history (session) |
<leader>fo | n | Recent files of this project |
<leader>fr | n | Replace in project |
<leader>fs | n | Project symbols |
Flash
| Key | Mode | Action |
|---|---|---|
S | n,x,o | Treesitter |
s | n,x,o | Jump |
Fold
| Key | Mode | Action |
|---|---|---|
<leader>z1 | n | Level 1 (top-level folds open) |
<leader>z2 | n | Level 2 |
<leader>z3 | n | Level 3 |
<leader>zF | n | All functions |
<leader>zO | n | All objects |
<leader>zT | n | All types |
<leader>za | n | All, outline only |
<leader>zb | n | Block here (again: the one around it) |
<leader>ze | n | Expand all |
<leader>zf | n | Function here |
<leader>zm | n | Methods of this type |
<leader>zo | n | Object here (table, map, array) |
<leader>zs | n | All but this (focus) |
<leader>zt | n | Type here (struct, class, interface) |
<leader>zu | n | Unfold here and inside |
<leader>zz | n | Toggle here |
General
| Key | Mode | Action |
|---|---|---|
; | n | Command mode (; instead of :) |
<C-c> | n | Copy whole file |
<C-s> | n | Save file |
<Esc> | n | Close extra window / go back / clear highlight |
<leader>d | n,x | Delete without copying |
<leader>fm | n,x | Format file |
<leader>q | n | Close buffer / quit |
<leader>w | n | Save |
jk | i | Leave insert (closes the completion menu first) |
Harpoon
| Key | Mode | Action |
|---|---|---|
<leader>h1 | n | Jump to file 1 |
<leader>h2 | n | Jump to file 2 |
<leader>h3 | n | Jump to file 3 |
<leader>h4 | n | Jump to file 4 |
<leader>ha | n | Add file |
<leader>hm | n | Menu |
Herdr
| Key | Mode | Action |
|---|---|---|
<leader>Wa | n | Agents (what the workspace is doing) |
<leader>Ws | n | Setup (install and configure) |
History
| Key | Mode | Action |
|---|---|---|
<C-r> | n | Redo (flashes what changed) |
<C-y> | n | Redo (Ctrl+Y) |
<C-z> | i | Undo in insert mode |
<C-z> | n | Undo (Ctrl+Z, never suspends nvim) |
<leader>u[ | n | Previous save |
<leader>u] | n | Next save |
<leader>uu | n | Undo tree |
<leader>ux | n | Forget yanks |
<leader>uy | n | Yank list |
P | n | Paste before ([y / ]y swap it) |
U | n | Redo (U) |
[y | n | Older yank in place of the paste |
]y | n | Newer yank in place of the paste |
p | n | Paste after ([y / ]y swap it) |
u | n | Undo (flashes what changed) |
LSP
| Key | Mode | Action |
|---|---|---|
<C-LeftMouse> | n | Go to DEFINITION (Ctrl+click) |
<leader>LS | n | Stop all (every project) |
<leader>Li | n | Info (what is loading) |
<leader>Lp | n | Pause or resume everything |
<leader>Lr | n | Restart (this buffer) |
<leader>Ls | n | Stop (this buffer) |
Move
| Key | Mode | Action |
|---|---|---|
<A-h> | x | Left |
<A-j> | x | Down |
<A-j> | n | Line down |
<A-k> | x | Up |
<A-k> | n | Line up |
<A-l> | x | Right |
<A-l> | n | Line right |
<C-b> | i | Beginning of line |
<C-e> | i | End of line |
<C-h> | i | Left |
<C-j> | i | Down |
<C-k> | i | Up |
<C-l> | i | Right |
Multicursor
| Key | Mode | Action |
|---|---|---|
<A-LeftMouse> | n | Toggle cursor at mouse |
<A-N> | n,x | Skip next match |
<A-P> | n,x | Skip previous match |
<A-a> | n,x | Add cursor on every match |
<A-n> | n,x | Add next match |
<A-p> | n,x | Add previous match |
<C-Down> | n,x | Add cursor below |
<C-Up> | n,x | Add cursor above |
Nvim-tree
| Key | Mode | Action |
|---|---|---|
<C-n> | n | Toggle window |
<leader>e | n | Focus window |
Run
| Key | Mode | Action |
|---|---|---|
<leader>Ra | n | Last with args |
<leader>Rc | n | Create Makefile |
<leader>Rd | n | Default task |
<leader>Rl | n | Last task |
<leader>Ro | n | Output terminal |
<leader>Rp | n | Dry run |
<leader>Rq | n | Output to quickfix |
<leader>Rr | n | Pick task |
<leader>Rs | n | Stop task |
Session
| Key | Mode | Action |
|---|---|---|
<leader>Pl | n | Restore last session |
<leader>Pr | n | Restore session (this folder) |
<leader>Ps | n | Select session (any project) |
<leader>Pw | n | Save session now |
<leader>Px | n | Don't save on exit |
Settings
| Key | Mode | Action |
|---|---|---|
<leader>SM | n | Tools |
<leader>Sf | n | Format on save |
<leader>Si | n | Info |
<leader>Sl | n | Plugins |
<leader>Sp | n | Panel |
<leader>St | n | Toggle theme |
<leader>Su | n | Updates |
Surround
| Key | Mode | Action |
|---|---|---|
sa | n,x | Add |
sd | n | Delete |
sr | n | Replace |
Telescope
| Key | Mode | Action |
|---|---|---|
<leader>fb | n | Find buffers |
<leader>ff | n | Find files |
<leader>fw | n | Live grep |
Terminal
| Key | Mode | Action |
|---|---|---|
<A-h> | n,t | Toggleable horizontal term |
<A-i> | n,t | Toggle floating term |
<A-v> | n,t | Toggleable vertical term |
<C-x> | t | Escape terminal mode |
Textobject
| Key | Mode | Action |
|---|---|---|
<End> | n,x | Block end (End) |
<Home> | n,x | Block start (Home) |
<PageDown> | n,x | Next block (PageDown) |
<PageUp> | n,x | Previous block (PageUp) |
[f | n,x,o | Previous function |
]f | n,x,o | Next function |
ac | x,o | Outer class |
af | x,o | Outer function |
ic | x,o | Inner class |
if | x,o | Inner function |
Window
| Key | Mode | Action |
|---|---|---|
<C-h> | n | Window left |
<C-j> | n | Window down |
<C-k> | n | Window up |
<C-l> | n | Window right |
Native Neovim 0.11 (not from this config)
| Key | Action |
|---|---|
grn | rename symbol |
gra | code action |
gri | go to implementation |
grt | go to type definition |
gO | document symbols |
K | hover (documentation) |
gx | open the URL or path under the cursor |
gc / gcc | comment (operator / line) |
Start · CHANGELOG.md
Changelog
All notable changes, newest first. Generated from the Conventional Commits since each tag (<leader>GRc).
[1.14.2] - 2026-10-21
Highlights
- An empty file can be a right answer: a build used to report a failure for every file the model left empty, including the ones that are correct that way (a Python
__init__.py, a.gitkeep,py.typed, the requirements file of a project with no dependencies). It now does for those what it should: nothing.
Fixes
- build: an empty __init__.py is not a failed answer (5ecfbb0)
[1.14.1] - 2026-10-21
Highlights
- Checked against a real build: a build of a small project with Claude Haiku (13 files, $0.03, under 3 minutes) did what the 1.14.0 notes promise: it named its riskiest requirement, deferred the one SPEC.md left open and said why, ended
⚠ built · 3/4 requirements have a testinstead of✓, and left a.leader/folder with nothing of the project in it. Two details came out of it and are fixed here: the plan review no longer calls a test that uses a fake or a temporary directory a "stand-in", and the builder's own wiring check is recorded under its name.
Fixes
- build: tests are not stand-ins, name the wiring check (1ff9203)
[1.14.0] - 2026-10-21
Highlights
- A build no longer calls itself done when it only compiles: the closing line now says what is proven, e.g.
◐ built · 23 files · checks pass · 5/17 requirements have a test · core without one: FR-16 · 2 placeholders, and it is✓only when every requirement has a test and nothing is open. PROGRESS.md carries the same proof and what is open. A layer build (foundation, design, domain) reports the numbers but keeps✓;kit.build.strictmakes an unproven build a failure. - A repair cannot delete your code quietly: before a replan trashes files, the review lists what goes (lines, public types, tests, who still uses them) and asks, and the closing line counts the public declarations a build removed or the tests it made unproven.
- Layers build on the code that is there: a whole-project build used to plan from the documents and the ADRs' text only, so it wrote the code earlier builds had left a second time under other names (two ports, two controllers, hundreds of analyzer errors). It now sees the project's files and their API. A free check fails when a public type is declared in two files, Flutter projects run
flutter testand Rust onescargo test, and plans name their risks and what they defer, with a warning for stand-ins of core requirements. .leader/in every project the builder touches: how Leader worked there (how builds end, which checks fail, what a model costs, whether you accept a proposal) as counts, sizes, durations, costs and outcomes, for measuring and improving Leader from real use. Offline: nothing is ever sent. It is never your work: a schema is the only gate, so unknown fields are dropped, file names are salted hashes, and there is no code, prompt, answer, name or text. The folder documents itself, ignores itself in git,<leader>aSlshows the report,LEADER_NO_DATA=1turns it off, and deleting it clears everything.
Features
- build: honest builds and a local .leader folder (1cb1c6e)
Fixes
- build: skip examples in the duplicate check, hide custom models (bebfb1d)
Documentation
- build: proof, loss gate, duplicates and the .leader folder (94a536e)
Chores
- deps: update the plugin lockfile (3ab99c6)
[1.13.0] - 2026-10-21
Highlights
- The agent is measured now, and a modest model gets help:
make evalruns 31 cases (Python, Go, JavaScript, Lua; fix, docs, refactor, review) through the real pipeline against any model, judged by code (does it parse, does a script that imports it still pass, did anything outside the target move, did it find the planted bug), never by another model. On the committed baselines Claude Haiku passes 100% of the single tries on the cases it was tuned on at roughly a tenth of Sonnet's cost per usable answer. Every model gets a capability profile (small, medium, strong): a smaller target limit, a second try with the reason when an answer does not parse, server-shaped output for Ollama and OpenAI, a chain of single steps for an instruction that is several things, and an optional cascade to the main model. - You are told what is going to happen, and can take it back: the spinner carries the estimate, every proposal says whether anything checks it (
checked on accept (LSP)orunverified), the statusline shows what the session has spent,<leader>autakes a whole run back across files (never what you edited since) and<leader>aSbutakes the last build back. - Less on the menu: DDD, the repository/service/lens menus, the doctor, building a whole project and multi-file plans are experimental and stay hidden until
agent.experimental(Settings → Agent) is on. Nothing was removed. - Review stops crying wolf: it no longer reports a finding that says nothing is wrong, nor hypothetical misuse; on the eval, Haiku went from 82% to 100% on review without losing the real findings.
Features
- agent: evals, capability profiles, trust layer, experimental flag (3777b62)
Documentation
- agent: evals, profiles, trust layer and stability (4cef712)
[1.12.2] - 2026-10-21
Highlights
- Your nvim now speaks Herdr: Herdr is the terminal workspace that runs the coding agents, and Leader talks to its CLI.
<leader>Waopens the breakdown of the workspace: one row per agent grouped by workspace (its state, the agent and where it is), the transcript of the one under the cursor and why Herdr thinks it is in that state (the rule that matched). It refreshes on its own, so what is on screen is the state now.<leader>Wsis the setup: it detects the binary, the version, the server and the agents' integrations, and installs what is missing (nothing without the button) — the installer does the same, and the start screen gets an Agents button when Herdr is there. - And Herdr sees what your nvim is doing: nvim registers itself as an agent of the workspace and says what it is doing (
workingwhile the inline agent runs,blockedwhile a proposal waits for you,idleotherwise), so the Agents panel of Herdr shows it. It never takes the pane from a real agent that already owns it, and a headless nvim (a script, a test) does not register at all.
Features
- herdr: the workspace of agents under <leader>W (135c337)
[1.12.1] - 2026-10-21
Highlights
- The code search index builds ~3x sooner and no longer freezes the editor: on a 4000-file TypeScript monorepo the first
<leader>fstook 19-35 s with 12-22 s of blocked UI (stalls up to 1 s); it now takes 6-10 s with the UI blocked ~0.2 s. The files are read by up to 4 headless Neovims in parallel (settings.search.workers); the result is identical to reading them in the editor, which is still what happens for a few changed files or if a worker fails. - Typing is smoother:
nvim-highlight-colorsrepainted the window on every key while the completion menu was open; it now waits 60 ms. In a TSX file the per-key cost went from p95 8.8-9.8 ms to 6.1-7.5 ms (the frame budget is 16.6 ms). - Less memory in big projects: Tailwind's server only starts where the project uses Tailwind (~210 MB less in the API of a monorepo), TypeScript is linted by the eslint server alone (
eslint_dstays for light mode), and a server that outgrowssettings.lsp.memory_limit_mb(vtsls: 2200 MB; it reached 2.9 GB in a long session) is restarted when you are idle.scripts/bench-project.luameasures all of this on any project.
Performance
- lsp: fewer resident servers, and tsserver cannot grow forever (44a781a)
- ui: highlight-colors repaints behind a 60 ms debounce (d1d39ba)
- search: the code index is built by parallel workers (08ddc7e)
Other changes
- chore(bench): a probe for a real project (5aa2719)
- chore(packaging): point the PKGBUILDs at v1.12.0 (3995f15)
[1.12.0] - 2026-10-20
Highlights
- Lua answers across files ~2.5x sooner: Lua now uses
emmylua_ls(a Rust server) when it is installed, andlua_lsotherwise. On this repo's 670 files cross-file go-to-definition worked at ~0.7 s against ~3.8 s on the bare workspace, and ~2.5x sooner with the full config, with the same hover, completion and diagnostics.lua_lskeeps nothing between sessions and itsmaxPreloadhas no effect, so the way to speed it up was a faster server. Mason installs it with the rest;settings.lsp.lua_serverforces either one. <leader>drruns and stops a Flutter app: pressed again while the app runs, it quits it and closes the debugger and every panel that opened, instead of only saying it is already running.- Your sessions are safe from tests: Neovim saves a session on every exit, and a headless test that opened temp files in a repo replaced that folder's real session with files that no longer existed, so restoring the project opened an empty buffer. Persistence now stays off without a UI, every test script runs against a throwaway state directory, and the whole suite leaves sessions, recent files, agent history and logs byte-identical.
Features
- dart: leader dr toggles run/stop (fffd15c)
Fixes
- sessions: tests and headless runs never write your state (db55542)
Performance
- lsp: Lua gets emmylua_ls when it is installed (~2.5x faster) (ccc1a54)
[1.11.0] - 2026-10-20
Highlights
- Big files stay fast: past 1.5 MB (or a minified bundle) a file opens as plain text, with no treesitter, LSP, folds, lint or formatter. A 4 MB Lua file opens in ~135 ms instead of ~1.9 s, and its first frame in the window matches a one-line file's.
- Saving never freezes: a buffer over 3000 lines is written at once and formatted in the background. A 15k-line save goes from ~1.2 s to ~20 ms. Linting is debounced, so a burst of Esc and saves lints once.
- Stop a language server that takes too long:
<leader>Lstops the servers of a buffer (also in the middle of "Loading workspace", in ~13 ms), stops all of them, restarts them, pauses everything, or says what is loading. A server that has not answered yet is found too. The editor cannot cache a server's workspace load, so this is the way out. - Code search remembers: the index is kept on disk, so only the very first search parses every file (~5 s here); every later session loads it in ~120 ms. The first
<leader>ffof a session is ~2x faster too. - A cleaner, faster start: the window opens on the themed editor, never on a black frame, and the real editor reaches the screen ~45 ms sooner (graphics driver and Neovim start in parallel with the window, the first frame is warmed).
leader --reuse fileopens files in the window that is already running in ~6 ms, without a new window or Neovim. Opt-in; a plainleaderis unchanged.- Measure it yourself:
make bench(startup, first frame, big files, picker, search index) andmake bench-edit(big paste, undo, save, diagnostics, typing latency), with the findings and limits inAGENTS.md.
Features
- lsp: stop, restart and pause language servers (<leader>L) (ecaf792)
- gui: a full-screen theme picker (0770236)
Performance
- app: a faster, cleaner start and a window you can reuse (21d5427)
- editor: big files, saves, lint, search index, picker preload (c185df4)
Other changes
- chore(bench): make bench and make bench-edit, and what they found (859c6b9)
- chore(dev): a Makefile and a real-time loop for the app (09ebf01)
- chore(packaging): point the PKGBUILDs at v1.10.0 (5b5cf42)
[1.10.0] - 2026-10-20
Highlights
- Install it the way your system expects: a
.debfor Debian, Ubuntu, Mint and Pop!_OS, PKGBUILDs for Arch and CachyOS (prebuilt and from source), the one-command installer for any Linux and WSL, and./install.sh --appfrom a clone. They add the menu entry, the icons, the manual (man leader) andleader-setup, and uninstalling removes exactly what was installed. - Signed releases: every file has a SHA-256 and a detached signature by the Leader Releases key (fingerprint in
packaging/README.md). The installer requires a signature made by exactly that key whengpgis installed, and refuses a forged or missing one. - The logo, clear: the N is the window icon, the menu icon at every size (a solid N when small, the dotted one when large) and the art of the About dialog. The app id is
io.github.sazardev.Leader, so launchers pair the window with its icon and menu entry. - First launch that explains itself: no Neovim says so in a notification; no Neovim config runs
leader-setupin the window's own terminal. AppStream metadata, a New Window action, a license (MIT) and a site with Open Graph cards, structured data, a sitemap and real screenshots.
Features
- site: SEO, screenshots, install options for every system (eab5dbc)
- packaging: signed releases, a .deb and Arch PKGBUILDs (3ebec37)
- app: the logo everywhere, an app id, first-launch setup (035cbea)
[1.9.0] - 2026-10-20
Highlights
- Projects first, like an IDE: Leader asks which project to open when it starts (or reopens the last one, or shows the start screen: your choice),
leader <folder>opens that folder, and starting inside a repo opens that repo. The picker keeps its facts and the list it found on disk, reads git in the background one folder at a time, pins projects (p), and a Yazi-like browser (b:hup,lin,Enteropens) walks your folders. - Updates, connected: a
↑ 1.9.0badge in the bar and a line at the top of the logo's menu when a newer version exists; a page shows what changed version by version and updates on request (the config withgit pull --ff-only, the app through its installer). One tiny request, never more often than every 12 hours, never updates by itself. - Recent files that belong to the project you are in, not everything you ever opened (
Ctrl+Awidens it). - Esc closes whatever is extra: help, quickfix, panels, floats, pickers (from insert too), lone tabs; never the last window. The hidden cursor has one owner list now, so it can no longer stay invisible after pages overlap.
- Install progress in numbers: every language tool with its state, time, bytes and speed, a bar, and a warning when data stops arriving. The window remembers its size, place and maximized state; the frame is yours to pick.
- Speed: hovering never scrolls a list, hovers are coalesced, Telescope no longer leaks a buffer per open, streaming answers redraw at most every 40 ms, and a list that fails to open leaves nothing behind.
Features
- gui: projects first, a folder browser, install progress, Esc (e9ccc65)
- app: keep the window's size, place and maximized state (fd876ef)
- ui: one owner list for the hidden cursor, Esc closes extra windows (0157e10)
- update: detect new versions, show the changelog, update on request (3eac257)
- recent: recent files that belong to the project you are in (9ac6d49)
[1.8.0] - 2026-10-20
Highlights
- Guided setup in the desktop app: seven steps on one screen on the first launch (and with
Ctrl+Shift+W). It shows what this machine is, lets you pick a theme previewed live, your languages (the ones you already use checked), a few features and an AI agent with its model, then lists what is missing with the exact command for your Linux family (pacman, apt, dnf, zypper, apk). Nothing runs until you press Install; sudo asks in a terminal and each item shows its status. A review with the install checks closes it. - A toolbar you decide: a right click on the bar turns each tool and chip on or off (project, home, recent files, palette, file tree, terminal, problems, agent, theme, settings, setup). The theme and the agent are right there: a theme menu previewed as you hover, and every installed agent with its models. Every tool shows a tooltip with its key.
Features
- gui: guided setup and a toolbar you decide (aef5599)
- app: a customizable toolbar with tooltips (a5b7db1)
Other changes
- docs(gui): document the guided setup and the toolbar (48502be)
[1.7.0] - 2026-10-19
Highlights
- Leader for the desktop (Linux and WSL):
leader, a GPU window for the editor written in Rust. The same config, drawn in one GPU draw call per frame, nothing running between keystrokes, ~170 ms to the first frame. Its own bar in the theme's colors: Zed-like menus (File, Edit, Selection, View, Go, Run, Git, Agent, Window, Help), a command palette (Ctrl+Shift+P), the projects you work in (switched with their sessions) and a full-screen project picker (Ctrl+Shift+O), the current file as a clickable path, live activity (LSP indexing, installs, saves). Full mouse: menus, pickers, modals, Telescope, the tree and the start screen follow the pointer and answer a click. A welcome screen checks the install and fixes what is missing. Install it in one command (curl -fsSL https://github.com/sazardev/leader/releases/latest/download/leader-installer.sh | bash: the app, plus Neovim and the config when they are missing; Linux x86_64 with glibc 2.31+, WSL with WSLg) or from a clone with./install.sh --app. - Doctor (
<leader>aX): a troubleshooting agent for builds that fail, apps that will not start or crash. Free checks first (dependencies, lockfile,.env, toolchain versions, a merge in progress), then one confirmed request. - Merge conflicts (
<leader>Gx): trivial ones resolved for free, the rest one proposal per conflict. - Language packs: Python, TypeScript, Rust, SQL, Swift, JVM and more as packs, with tests in the editor, project setup presets, debugging and a SQL runner. The installer asks once which packs you want.
- A full-screen marketplace for SPEC.md modules and STACK.md / DESIGN.md add-ons, and a bigger library of each.
- Leader is the new name (repo
sazardev/leader), with a brand kit, wallpapers and a scripted product video.
Features
- app: release package with a guided installer (9cb78d4)
- gui: the config side of the desktop app (8c320ce)
- app: Leader desktop app, a GPU front-end for Neovim (f0ee3b0)
- site: wallpapers page, fed by a release of the brand assets (196b936)
- brand: logo, image catalog and wallpaper collection (602c5a7)
- video: scripted product video with keys, effects and score (9ec8b75)
- agent: doctor, a troubleshooting agent (4b23e52)
- git: merge conflict assistant (818657c)
- kit: full-screen marketplace and a bigger library (af08bb9)
- agent: statusline badge for one-shot requests (a2b18f9)
- languages: packs, tests in the editor, setup and SQL runner (4c6f2a1)
- agent: SPEC.md modules for what every app repeats (50e3da4)
Fixes
- kit: drop a stray local path from the design presets (9ced0ca)
Other changes
- docs(app): install and document the desktop app (fb4775e)
- chore(rename): rename my-nvim-setup to Leader (0a5425c)
- docs(agent): document the doctor, conflicts and marketplace (3a3e74a)
- docs(docs): list Go and Dart / Flutter under Languages (4270f1f)
[1.6.0] - 2026-10-19
Highlights
- Build by layer (
<leader>aSbthent,sord): the foundation from STACK.md, the domain from SPEC.md or the screens from DESIGN.md, each built on top of the files the project already has.bbstill builds everything at once. - Build one feature (
<leader>aSbf): pick a requirement of SPEC.md, or describe a new one in your words; it is added to SPEC.md as the next FR-n (after a confirmation) and built end to end, following the patterns the code already uses. - Recipes (
<leader>aSr): twelve prefab tasks for your stack (a Flutter screen with its state, a data feature or a form; a Go endpoint, entity with its repository, CLI command or Bubble Tea screen; web components and pages; TypeScript, Python and Rust endpoints). Each is a plain Markdown file you can copy and adapt. - Coverage (
<leader>aSv, 0 tokens): which requirements have a test, what STACK.md names that the manifest lacks, DESIGN.md tokens no code uses, folders no document mentions. - Decisions and progress (0 tokens): every accepted plan records its new decisions as ADRs in
docs/adr/, which later builds follow (reversing one is explicit:Supersedes ADR NNNN);PROGRESS.mdsays what was done, what is open and what is next.<leader>aSPlists them. - Release kit (
<leader>GR): this changelog and the next version from the Conventional Commits (c), a pull request description (p, one light request, copied to the clipboard) and a release checklist from STACK.md (l). It never tags, pushes or posts. - Flutter: run on a chosen device, profile mode, quieter runs. opencode 2.x CLI supported.
Features
- agent: layer and feature builds, recipes, ADRs, release kit (b2644fd)
- agent: support the opencode 2.x CLI (790cf02)
- dart: run on a chosen device, profile mode, quiet runs (b109513)
Fixes
- release: write other changes as the commit is written (8e93451)
- commit: force a/ b/ prefixes in the staged diff (e67396c)
Other changes
- chore(deps): update neoconf.nvim and nvim-treesitter (5abb9eb)
- test(agent): keep the backend detection test hermetic (573dec0)
[1.5.0] - 2026-10-18
Features
- agent: build look step, design screens and toolkits, live plan (cb319cc)
- agent: project kit (<leader>aS) and build with agent (43fe81a)
[1.4.0] - 2026-10-15
Features
- history: undo word by word, redo keys, clipboard and yank list (8b7c617)
- go: getters, Reconstitute, repository port and package ranking (8df42c8)
- dart: tests in the editor, entry points, pub and translations (3b93295)
- dart: debugging, build_runner watch and stack completion (1e51b98)
- agent: convert selection, smaller context, Dart support (1c441fc)
- setup: .gitignore from the project and Go config presets (eb0b24a)
Fixes
- docs: polish the docs site on phones and desktop (49838b4)
- theme: follow the terminal theme faithfully and live in every nvim (6b26a31)
Performance
- theme: terminal theme starts as fast as the default one (7a060db)
Other changes
- chore(deps): update schemastore.nvim (43b5e4d)
- docs: web documentation at /docs, generated from the Markdown (362660e)
[1.3.0] - 2026-10-15
Features
- tasks: run the targets of the project's Makefile from <leader>R (19d630d)
- agent: answer cache, next edit, diff-only docs and code search (47d3945)
- diagnostics: copy the line, the file or the project as text (2b6d1fd)
- files: create, rename, move, duplicate and delete from <leader>F (f6f0ecb)
- settings: a settings panel on <leader>S (217be6a)
- folds: fold code by what it is from the <leader>z menu (18863ae)
- terminal: Esc and Shift+Esc in NvChad terminals (7df03f4)
Fixes
- ui: modal buttons, filled rows and floats without a dark hole (77549d5)
Other changes
- chore(deps): update neoconf.nvim and schemastore.nvim (e131374)
- docs: settings panel, file operations, copy diagnostics and the tree (533f1df)
[1.2.0] - 2026-10-14
Features
- agent: agentic layer on top of the inline pipeline (68b0012)
Performance
- agent: warm opencode server, no MCP or thinking, cheap model list (42e435e)
Other changes
- docs(site): demo scenarios for the agentic layer (a841bfa)
[1.1.0] - 2026-10-14
Features
- agent: history in markdown with filters and back navigation (7391daf)
- ui: every view drawn like the landing page's demo (ee4615c)
- ui: which-key labels without the section the menu already says (377cd38)
- ui: commit panel bars, key hints as words, which-key grid (8dc4010)
- ui: start screen identical to the landing page's, compact popups (420d023)
- ui: optional key display like the demo's keycaps (:FlatKeys) (8b89b30)
- ui: flat design for the whole editor, same as the demo (1e4a33b)
- install: one-command install with bootstrap, no prompts (93b8bc4)
- install: ajustes por máquina y agente de IA desde el instalador (c3ca0f4)
- comments: plegado de comentarios con preferencia que se recuerda (3306c7a)
- diagnostics: colores unificados y número de línea con píldora (5c23cab)
- go: if err, mapeadores, cableado, enums, port y HTTP sin LLM (6a1eff6)
- go: completar los métodos que faltan para cumplir una interfaz (e5fe0d0)
- agent: pruebas (aT) y lentes (aL) con gasto mínimo de tokens (10dbc7d)
- commit: borrador de IA rápido y en inglés, y commit automático GM (ce388d1)
- agent: fix en dos niveles, en línea (af) y de bloque (aF) (e572dce)
- agent: limpiar hallazgos de uno en uno, por archivo o todos (f29532c)
- commit: panel único para commitear con validación e IA clara (e849ae6)
Fixes
- lsp: servers never attach to diffview:// and other virtual buffers (5fd444f)
- ui: telescope preview hit line and text chips without patches (f92969b)
- ui: files opened from the start screen lost their line numbers (0c6b3d2)
- install: run the agent setup without loading the user config (26676ba)
Other changes
- chore(plugins): update nvim-treesitter and schemastore (31da4dc)
- docs(site): agent scenarios drawn as Neovim draws them (204b017)
- docs(site): terminal-derived theme picker, menus from real keymaps (eaf2c98)
- docs(site): 41 demo scenarios in tabs: search, tree, replace, git, more (90d8974)
- docs(site): flat inspect floats, no border and no divider (b44dba3)
- docs(site): hover, signature help, diagnostics popup and completion docs (7147a7c)
- docs(site): faithful editor demo with multicursor, DDD and commit panel (9b8e9da)
- docs(site): scripted real-looking editor for the Go and agent demos (c1dd9db)
- docs(site): redesign the landing page flat, square and minimal (9a6e050)
- docs(site): single-file landing page in the config's own design (f56c5ea)
- chore: translate scripts, installers, hooks and tests to English (6aea6b9)
- refactor: translate code comments and UI text to English (1776927)
- docs: translate README, AGENTS, CLAUDE and cheatsheets to English (db1990f)
- docs(readme): README con todo lo nuevo y los principios de diseño (bb3faee)
- test(agent): más margen para gopls en frío en test-fix (ca8392d)
- refactor(go): gohttp separa lo de cada router y quita código muerto (e98ff23)
[1.0.0] - 2026-10-13
Features
- install: instalador de un paso para Linux, WSL, macOS y Windows (7d47d1f)
- agent: agente inline con DDD, niveles y modelos locales (9fa27fd)
- editing: commits convencionales, bloques de diagnóstico (3159950)
- session: autoguardado periódico y reapertura del pane en Herdr (7adaa22)
- dependencies: new (8a8ef31)
- ui: diagnósticos de la barra como etiquetas E/W de color y sin LSP (e74c296)
- go: atajos de archivos de modelo (functable, funcdomain, functodomain, funcfromdomain) (b969d7f)
- go: tags gorm con variantes por campo/tipo y TableName en *_model.go (d2aaefc)
- docs: cheatsheet único generado desde los keymaps reales (a5ea1a6)
- docs: new marketing slogan (4380500)
- docs: new marketing slogan (cb73724)
- dashboard: 66 arts braille nuevos (mascotas, anime, retro, comida) (3198ee1)
- docs: new marketing slogan (88e6e19)
- native search/autoread options, lazydev and git-conflict (7f14f7b)
- git: diffview (<leader>Gd/Gf/Gr/Gq) and treesitter-context (0ef25e4)
- session: persistence.nvim with dashboard Restore Session button and <leader>P menu (b16163d)
- options: jumpoptions=view, diagnostics severity_sort, restore last cursor position (63f7d5c)
- markdown: toggle diagnostics, reading mode, spell, word count, checkbox (d2665a9)
- docs: new marketing slogan (adf7b62)
- design: new ascii art for neovim! (1d899be)
- dashboard: add herdr braille art (da3d974)
- dashboard: braille art catalog discovered from lua/ui/arts, picker with "Change Art" (8b4657a)
- keymaps: native multicursor with Ctrl+Down/Up (visual block) (c854a30)
- nav: <Esc> goes back after gd/grr, <leader>H file history; fix <leader>q (8334e31)
- lsp: clear definition/references navigation (gd, grr, Ctrl+click) (413d00a)
- terminal: responsive integrated terminal panel for language menu actions (8efa087)
- theme: inherit the terminal (Alacritty) theme with live follow and <leader>S settings (c3e6412)
- keymaps: <leader>q closes buffer or quits nvim (a9d3dd1)
- funcs: new funcs (f063361)
- light mode, blink.cmp tab-complete, color previews; drop rest.nvim/lazygit (3e3622c)
- blink.cmp, harpoon, mini.move, drop sessions (98d62f5)
Fixes
- plugins: go.nvim y flutter-tools respetan languages.disabled (30c4a40)
- session: registro de nvim como agente de Herdr apagado por defecto (e9d8327)
- completion: Tab vuelve a elegir sugerencia con el menú abierto; Esc + Tab para saltar (a983fb5)
- completion: Tab salta de hueco en snippets y Esc/jk solo cierran el menú (beb23dd)
- dashboard: usa layout horizontal en el picker de arte (7e16d09)
- dashboard: re-lock nvdash buffer after resize (NvChad leaves it modifiable) (b10b2cc)
- telescope: defer require until display closure (1d1b556)
Other changes
- docs(readme): README nuevo con instalación, filosofía y estructura (c36b153)
- refactor(i18n): pasa a inglés los textos de UI (keymaps, menús, notificaciones) (8136b19)
- chore(lock): add persistence, diffview, treesitter-context and refresh plugin versions (fb79e18)
- docs: document sessions, diffview and treesitter-context in AGENTS.md (b49eae4)
- new art! (607c7ba)
- refactor: modular architecture, contextual leader menus, and config cleanup (f3f2dd1)
- refactor: trim unused plugins and keymaps, add global replace (73fb3d6)
- unify visual: single borders, zero icons, fix diagnostics (f1ebbad)
- docs for go! (e3719b4)
- Remove Copilot plugin and dependencies (5ceb0fa)
- Add Go tooling keymaps, fix treesitter highlighting, brighten gruvbox syntax colors (a8e5bb2)
- update config (c4f50b2)
- update config (81c1348)
- update config (2e53d5b)
- update config (b454716)
- update config (d9a5192)
- update config (1bd0135)
- update config (0227935)
- update config (dc6de83)
- update (33b096d)
- update chadrc (61586bd)
- update (53069e6)
- add AGENTS.md (f9a64be)
- update config files and README (7c45f1e)
- update config files (b4dba20)
- Initial commit (68e950e)
Features · docs/cheatsheets/agent.md
AI agent
One-shot actions on what you are looking at: a function, a class or a selection. No chat, no sidebar, no conversation. Every action has a closed prompt, a bounded context and an output with a fixed format, so it is predictable and cheap. Keys: CHEATSHEET.md (Agent menu).
Flow
- Put the cursor inside a function (or select lines in visual mode) and press
<leader>a+ the action. - The code being sent is highlighted and a spinner turns on the right:
⠋ implement · claude/haiku · 1.2s. Requests with no code to paint (the project documents of<leader>aS, including the screens of<leader>aSw, the commit draft, the staged review, plans) show a badge in the statusline from any buffer:⠹ drawing screens, then✓ drawing screens(or a red✗) for a few seconds after it ends.<leader>axcancels them too. - The result shows up in the code, depending on the action:
| Action | Input | Result |
|---|---|---|
i implement | function (its name, signature, doc or TODO are the spec) + optional instruction | inline diff: deleted lines in red, added lines in green below. ⏎ accepts, ⎋ rejects |
r review | function or selection | real findings as diagnostics on their line (◆); <leader>xd shows the detail |
e explain | function or selection | window next to the code, streaming; closes by itself when the cursor moves, or with Esc from the code (while it is still streaming, Esc also stops the call) |
a ask | your question (text field) | window, same as explain |
f fix inline | the diagnostic (LSP, linter or agent finding) on the cursor line + optional hint | diff with the minimal change on only those lines; the surrounding code and the objects it points to (the variable's type, the signature of what is called) go along as read-only context; with no diagnostic on the line, it does nothing |
F fix block | all the diagnostics of the function (or selection) + optional hint | diff with the minimal change that leaves the block coherent (several diagnostics with the same cause are fixed together) |
d docs | function + optional focus | diff that only adds/updates the documentation comment; the model returns only the comment (agent/docplace.lua puts it above the function, or inside it for Python), so the code cannot change and the output is a few lines |
t convert | selection (or the paragraph under the cursor) holding a JSON sample, a SQL CREATE TABLE or a curl command, in a code file + optional root type name | diff that replaces it with ready code in the file's language: JSON → Go structs with json tags and nested types (TS interfaces, JSDoc typedefs in JavaScript, Python dataclasses, Rust serde structs, Dart classes with fromJson/toJson…), a table → a struct with db tags and pointers for nullable columns, curl → a function that sends the request with the standard client (secrets become parameters). What each source becomes is fixed per language (agent/convert.lua), so the prompt is explicit; light model, only rules as context. A one-word instruction names the root type (Invoice); in a non-code file it refuses (paste the JSON where the types should go). In Dart it follows pubspec.yaml: JSON → freezed classes (or @JsonSerializable()), a table → a drift Table; the import and part lines are added on accept (convert_header.lua on proposal.accepted, rules in utils/dartheader.lua) and a snake_case key left without @JsonKey(name: …) gets it by rule (Dart / Flutter) |
n next edit | the 30 lines around the cursor + what you just changed in this file | diff with the ONE edit that follows from your recent changes (the rename you are following through, the field added to one constructor and not the others); with nothing changed in the file it asks nothing (Next edit) |
R refactor | your instruction (text field) | diff that applies exactly that instruction |
g handoff | your instruction | sends file:range + instruction to an agent already running in Herdr |
aA + a fix all | none (the file's findings and diagnostics) | one proposal per function that holds any, asked once with the total estimate; they wait in a queue (Several proposals at once) |
aA + o / O fix from output | none / a command | the errors a build printed (last terminal, quickfix, or a command you run), one proposal per function they point into |
aA + p plan across files | your instruction (text field) on a type or function | an editable plan of steps, then one proposal per step (Multi-file plans) |
The instruction field is inline (like the inline chat in Zed or VS Code): it opens right ABOVE the target code and pushes the code down instead of covering it, with an accent bar, a header (✦ implement · claude/sonnet) and a placeholder. It grows as you type (up to 6 rows), ⏎ sends, ⎋ cancels (also if you switch windows), ⌃J / Alt+Enter insert a line break and ↑/↓ (or ⌃P/⌃N) walk through the previous instructions of that action, which are remembered across sessions. In narrow windows the key hint shortens itself. There is no autocompletion or auto-closing of quotes inside.
Instruction: i, d, t and f open the field and Enter with an empty field runs without an instruction; if you type something, it is appended to the prompt and takes precedence over the default behaviour. Useful for an empty func main() {} ("read a CSV and sum column 3"), for an unclear function ("this validates orders before charging; the coupon case is missing") or to steer a fix. a ask and R refactor require the text; r review and e explain run instantly.
Short menu: i implement, d docs, t convert, n next edit, r review, e explain, a ask, f fix inline, F fix block, R refactor, g hand off (only inside Herdr), h history, m model (main, light and effort in a single selector), c clear (discards the finding on the line, or those in the selection in visual mode), C clear file (those in this file), x stop (cancels what is running and discards the pending proposals; it does not touch findings) and l last answer (reopens the last answer or rejected proposal). Everything that works on many pieces at once lives in its own Advanced menu, <leader>aA, so the everyday one stays short: a fix all, o / O fix from output / from a command, p plan across files, and for the proposals they leave waiting ] / [ next / previous, y accept all, l list. Accepting and rejecting a proposal have no menu entry: they are ⏎ and ⎋ on the proposal (and Tab refines it).
Accepting is a single change: u undoes it entirely. If you edit the code while the agent is working, the proposal is discarded instead of overwriting your change. Several actions can run at once (not on the same block).
Which target it takes
Priority: visual selection (always whole lines) > function under the cursor (with the contiguous comments above it, which are the spec) > class > paragraph of lines around the cursor (useful for a half-written signature: type func ParseConfig(path string) (*Config, error) and press <leader>ai). "Function/class" is the definition of af/ac (treesitter textobjects), for any language with a parser. More than context.max_target_lines (400) is rejected: trimming it would break the replacement.
Context (and why it is cheap)
Each action asks for providers; together they are capped at context.max_chars (8000 characters), the target is counted separately:
| Provider | What it contributes | Cost |
|---|---|---|
rules | settings.agent.rules + .agent.md at the project root (your conventions) | text |
types | the definition of the types the target uses (fields of a struct, members of a class/interface/enum/trait), so the model does not guess Note.Done | local + LSP |
contracts | if the target is an adapter (PostgresOrderRepository): the port it must satisfy (with path), which methods already exist and which are missing, the entity the port names and, when implementing, another implementation in the project as a style example (constructor, errors, mapping) | rg + treesitter |
schema | for a persistence adapter: the entity's real table (SQL migrations with their later ALTERs/indexes, or schema.prisma) and the database in use: dialect, driver, parameter style ($1, ?, @p1) | rg + manifests |
diagnostics | LSP/linter diagnostics inside the target, each one with its code line (the target is not numbered) | local |
callees | REAL signature of what the target calls, requested from the LSP (hover); whatever does not arrive within 700 ms is omitted | LSP |
outline | imports + first line of each function (without repeating the types already included in types) | local |
types is language-independent (it uses the same treesitter queries as af/ac): Go (struct, interface, alias), TypeScript (interface, class, type, enum), Python, Rust (a method of impl Note brings struct Note, not the impl), Java... A type in the same file is read instantly; one in another file or package is located with the LSP (textDocument/definition) and extracted from the destination file (project only, not dependencies). Field types are followed one level (Tags []Tag), and if the target is a class method, the class is included without the method itself. Cap: context.types (4) definitions of context.type_lines (30) lines. Each definition is compacted first (compact in providers/types.lua): full-line comments and blank lines go (context.type_comments = true keeps comments), and runs of bare enum values are joined, at most 24 shown and the rest counted (… (188 more)). Measured on a Flutter widget whose build uses two enums with a doc comment per value: the <types> section went from 3,243 to 871 characters. Definitions are never taken from generated files (x.freezed.dart repeats the class ~5x longer; utils/generated.lua).
Each action asks for what it needs, in priority order (if the budget gets tight, the last ones are trimmed):
| Action | Context |
|---|---|
| implement | rules, contracts, schema, types, callees, outline, diagnostics |
| port | rules, contracts, schema, types, diagnostics |
| review | rules, diagnostics, contracts, schema, types, callees |
| fix (inline) | rules, diagnostics, findings, contracts, around (surrounding code), types, callees |
| fix block | rules, diagnostics, findings, contracts, types, callees |
| refactor | rules, types, callees, outline |
| ask | rules, types, callees, outline, diagnostics |
| explain | rules, types, callees |
| docs | rules, types (documents the real fields and parameters, without paying for outline or callees) |
| convert | rules (the selection is the whole input) |
Transparency (note:): after the code block the model may add note: ... (max. ~15 words) when it assumed or added something that was not in the context, and needs-import: .... Both show up in the proposal hint (note: Note has no Done field; assumed one) and are kept in the history. Measured in a test with SetDone without types: it guessed Done and Updated and said so; with types: the same code, with no warning, -70 % output tokens and -25 % cost.
Measured with Claude: a normal claude -p call drags ~46 000 input tokens (your plugins, skills, hooks and memory). With --setting-sources "", --tools "" and its own --system-prompt it drops to ~600 tokens (+ the context), about 2 s and fractions of a cent per action (haiku). The CLIs run in a neutral directory (stdpath("cache")/agent), so they do not load the AGENTS.md/CLAUDE.md of your repo either. That is why --bare is not used: it would break subscription login.
Cost audit (measured): every claude -p call was billed as a 1-hour cache write (up to 2× the input price) and never hit the cache, because the message changes on every call. With DISABLE_PROMPT_CACHING=1 (setting agent.claude.prompt_cache = false, the default) a Sonnet action went from $0.0065 to $0.0037 and a Haiku one from $0.0005 to $0.0003, with no change in latency. The system prompts were also shortened ~35 % (implement 1462 → 943 characters) and now state what is true (types, callees, diagnostics) and that whatever was assumed must be flagged with note:. In total, the same action costs ~50 % less than before the audit. About ~560 fixed input tokens from the CLI itself remain, which no safe flag trims (CLAUDE_CODE_SIMPLE and --bare break subscription login). agent.claude.max_cost_usd (0.25) cuts off a runaway call.
Order of the prompt (stable first, volatile last)
The sections reach the prompt arranged by each provider's stability: 1 the same on every call of the project (rules, testing), 2 changes only when the code does (types, outline, contracts, schema, ports, ddd, testfile, callees), 3 changes with the cursor or the editor (diagnostics, findings, around, callers, gitlog, changes, subject). The declared order still decides what survives when the budget is tight; only what is SENT is rearranged. What this buys depends on the backend:
- Ollama / llama.cpp / vLLM keep the KV cache of the last prompt and reuse what is identical from the start: a second action on the same code re-reads only the tail. For that, Ollama's
num_ctxis now sticky per model (a small action after a big one keeps the big window, otherwise Ollama reloads the model and drops that cache). claude -pgets almost nothing from it: the CLI sends the whole message as ONE block and the fixed part is under the cache's minimum size. That is whyprompt_cachestays off (measured: -40% per call); if you turn it on,<leader>ahshowscache_read/cache_writeper run so you can check it pays. The answer cache is what makes repeated questions free there.- A new provider declares
stability = 1|2|3next to itscap(default 2).
Models per tier (cheap where it is enough)
Each action runs on the main model or the light one. docs, explain and convert are light (Claude: haiku); the rest use the main one (sonnet by default). Measured with Claude, a single-function action: ~$0.005 on sonnet vs ~$0.0003 on haiku, about 17 times less, and the spinner shows the model that is running. Light actions do not inherit the effort either: they are the cheap ones and must not become expensive because of a setting meant for others.
<leader>amopens a selector with the main model (and the backend), the light model and the effort, with their current value. Everything is remembered.- Change which actions are light:
agent = { tiers = { review = "light", docs = "main" } }inlocal.lua. agent.claude.light_model/agent.opencode.light_modelset the default light model. In OpenCode both default toopencode-go/deepseek-v4.1-flash(ids depend on your provider: pick another in<leader>am).
Backend and model
<leader>am → Main model opens ONE fuzzy list with the models of all installed backends (Claude: haiku/sonnet/opus; OpenCode: the cheap, fast shortlist in agent.opencode.models, or whatever opencode models returns if that is nil). Choosing one also switches the backend and is remembered across sessions (stdpath("state")/agent.json). Effort (Claude only; --effort) is in the same selector. With backend = "auto" the first available one in agent.order is used.
- Claude Code (
claude -p): no tools, no session on disk, text streaming. - OpenCode (
opencode run): each action travels as an ephemeral agent with no tools (OPENCODE_CONFIG_CONTENT). Your global MCP servers are turned off for these runs (agent.opencode.mcp = truekeeps them): their tool schemas were ~20k input tokens and ~2 s per action. Withagent.opencode.serve(default) a privateopencode serve(free loopback port, random password, no MCP, stopped on exit, off in light mode) boots in the background and actions talk to its HTTP API with curl on a session created ahead: ~2 s per action instead of ~8 s. The first action (while it boots) is cold. The CLI line is detected (opencode --version): 2.x changes the inline schema (agents/system/permissions), takes the variant in-m provider/model#level, needsrun --standalone(without it the background service, which never seesOPENCODE_CONFIG_CONTENT, answers and the requested agent is "not found") and has no--pureequivalent. Its warm path is off untilopencode serve's v2 HTTP API is driven, so every action pays the private-server boot (~3 s measured on 2.0.31) and the run's JSON stream carries no usage events in our tests (the report shows tokens as —). In 1.x, models matchingagent.opencode.no_thinking(defaultdeepseek,haiku,minimax-m3) run with thinking off: deepseek-v4.1-flash spent 300-1400 reasoning tokens (4-15 s) on small functions; it costs some quality,{}turns it back on. It is per model because glm/cat-jam reject the parameter. (2.x has no such option: reasoning is chosen with the model's variants,effortin<leader>am.) The shortlist (deepseek-v4.1-flash, minimax-m3, claude-haiku-5-5, glm-5.3-flash, gpt-6-luna) was picked by benchmarking all opencode-go models on four graded tasks (twoimplementchecked withgo test, areviewwith a seeded bug, areviewof clean code that must answerOK): 4/4, 1.4-3 s warm, ~$0.0002 per action. Haiku keeps requests 30 days; Gemini and Muse Spark train on prompts (left out). DeepSeek costs double at Peak (01-04 and 06-10 UTC, Mon-Fri). Only one run per task, so differences under ~0.5 s are noise.agent.opencode.attachpoints at a server of your own instead (--serveron 2.x), butopencode run --attachprinted no text in v1.19.agent.opencode.agentuses an agent of your own. If opencode does not find the requested agent it warns and falls back to the default one, which DOES have tools: here that is treated as an error. - Each opencode session stays in its list: titled
nvim <action>in the cold path,nvim agenton the private server.
Local backends and models (automatic setup)
Besides Claude Code and OpenCode, the agent talks to local models (Ollama on CPU or CUDA, LM Studio, llama.cpp, vLLM...) and to any other agent CLI (Hermes Agent, Gemini CLI, Codex, Aider). A setup detects and connects them.
Quick start
<leader>am→ Setup (orscripts/agent-setup.shfrom the terminal). It detects in parallel, without touching anything: the installed CLIs, Ollama and OpenAI-compatible servers (also the Windows one if you are on WSL: it tries127.0.0.1, the nameserver and the gateway), their models, whether what is loaded runs on GPU or CPU, the VRAM/RAM, and it remembers the server it found.- Use detected models applies the recommendation with what you already have. If there are no models, it offers to Download a couple according to your memory (VRAM, or half the RAM without a GPU):
qwen2.5-coder32b/14b/7b/3b/1.5b. They are suggestions, you decide. - If Ollama is installed but stopped: Start Ollama server (
ollama servein the background). Starting and downloading are always requested explicitly: nothing is installed or downloaded by itself.
Terminal (scripts/agent-setup.sh; with no flags it only reports and exits with 0 if a backend is ready):
| Flag | Does |
|---|---|
--start | starts ollama serve if it does not respond |
--pull MODEL | downloads a model (repeatable) |
--auto | applies the recommendation with the models already installed |
--use backend/model[@light] | sets backend and model (main, or light with @light) |
--json | the full snapshot, for other tools |
Example: scripts/agent-setup.sh --start --pull qwen2.5-coder:3b --auto.
The best of each tier: the main one stays on Claude and local models cover the light tier (docs, explain): light_backend (in <leader>am → Light model, or agent.light_backend) keeps those actions on a free local model without moving implement/fix/review. With no cloud CLI at all, the local one becomes the main one too.
Backends (all use curl, no dependencies):
ollama:agent.ollama = { host, model, light_model, models, num_ctx, max_ctx, keep_alive, temperature, think }. The host comes fromhost>OLLAMA_HOST> what the setup detected >127.0.0.1:11434.num_ctxis automatic (power of two depending on the prompt, capped bymax_ctx): Ollama SILENTLY truncates whatever does not fit in its window and would lose precisely the rules at the beginning.keep_aliveleaves the model loaded (the first action is slow, the following ones are not);temperatureis low (0.1) because the required format is respected better. Cost is always 0. The<think>…</think>blocks of qwen3/deepseek-r1 are discarded.openai:agent.openai = { base_url, api_key_env, model, light_model, models, temperature }(LM Studio:1234, llama.cpp:8080, vLLM:8000, LiteLLM, OpenRouter...). The key is read from theapi_key_envvariable and travels through the process environment, not in the arguments (it does not show up inps).custom(Hermes Agent, Gemini CLI...): a declarative entry inagent.custom, without writing code:
-- local.lua
return { agent = { custom = {
hermes = {
label = "Hermes Agent",
cmd = { "hermes", "chat", "-q", "{prompt}" }, -- {prompt} {system} {model}
system_in_prompt = true,
model_flag = { "-m", "{model}" },
models = { "hermes3" },
},
} } }
The flags in the Hermes Agent example are not verified (the CLI was not installed when this was written): check them with hermes --help. Hermes models do work today through Ollama (ollama pull hermes3).
Pitfalls
- A small local model fails more often with the structured format (code block,
L<n>|sev|msg): use it fordocs/explainand, if you want more, move up to a 14b+ code model.Escrejects malformed output as usual. - Without a GPU the model runs on CPU: fine for the light tier; the report says whether what is loaded is on GPU, GPU+CPU or CPU.
- WSL2: to reach the Windows Ollama, set
OLLAMA_HOST=0.0.0.0there. In Docker with NVIDIA:docker run -d --gpus all -p 11434:11434 -v ollama:/root/.ollama ollama/ollama. - The setup (and the terminal report) stores the servers it finds in
agent.json(endpoints).
Handoff to Herdr
<leader>ag (only inside Herdr): you type the instruction, pick an agent from herdr agent list (those in this folder first) and file:range + diagnostics are sent with herdr agent prompt. With unsaved changes it attaches the current code. Use it when the task no longer fits in a single shot.
What the other agents of the workspace are doing is in the Herdr panel (<leader>Wa), and this nvim can say what it is doing to Herdr (see herdr.md).
Hexagonal architecture levels (one menu per level)
<leader>a keeps the generic ones (implement, review, explain, ask, fix inline, fix block, docs, convert, refactor, hand off, model, history, clear, stop, last). Each architecture level has its own folder (lua/agent/<level>/), its own action registry and its own menu, with its context and, in each one, a local 0-token analysis. The actions of each level run through the same pipeline (inline diff, findings, history and cost), and in the history they appear under their own name (repo_port, service_review, ddd_fix...).
| Menu | Level | LLM actions | Local analysis (0 tokens) | Own context |
|---|---|---|---|---|
<leader>aD | Domain / DDD (agent/ddd/) | implement i, review r, fix f, missing m, ask a | diagnose d (layers, violations, aggregates), clear c | ddd (layers, naming, related pieces) |
<leader>aP | Repository (agent/repository/) | port p, implement i, review r | check c (port vs methods, entity vs table) | contracts, schema |
<leader>aU | Service (use cases) (agent/service/) | implement i, review r | deps d (ports it uses, nonexistent calls, implementations, wiring) | ports |
<leader>aT | Tests (agent/tests/) | write w, mock m, fix f | gaps g (public functions in the folder that no test names) | testing, subject, testfile |
<leader>aL | Lenses (agent/lenses/) | security s, performance p, concurrency c, errors e, api a | — (each lens first discards what is not its concern) | changes |
The providers of each level (contracts, schema, ports, ddd) are silent if the code does not belong to that level, so implement/review/fix of the generic menu also receive them when applicable, at no cost when not. The shared code (searching with ripgrep, reading a type definition) lives in agent/discovery.lua, so levels do not depend on each other.
Repository (<leader>aP): persistence adapters
Implementing a repository (SQL, Postgres, MySQL, in-memory...) is long but mechanical, and the model needs what is not in the file you are looking at. The agent finds it by itself, without you mentioning files, and each piece arrives with its path:
- Port: the interface it must satisfy. It is deduced from the name (
PostgresOrderRepository,pgOrderRepository,OrderRepositoryImpl,CachedOrderRepository→OrderRepository) and from what is declared explicitly (implements X,class T(X),impl X for T,var _ X = (*T)(nil)); it is located with ripgrep in any folder structure and read with treesitter. - What is missing: it compares the port's methods with those the type already has (in Go, the functions with that receiver in the package) and also warns about the constructor. Only the missing ones are requested.
- Entity and types the port names (
Order, with its fields and constants) even if the empty struct does not mention them: without this the model invents the fields. - Sibling example: another implementation in the project (the one in the same folder is preferred) with the constructor and the equivalent method: this way it copies your structure, your error handling (
fmt.Errorf("…: %w"),errors.Is(err, sql.ErrNoRows)) and your mapping. - Schema: the entity's real table with its later changes and the dialect (from the schema syntax and from
go.mod/package.json/pyproject/Cargo.toml/pom.xml), the access library and the parameter style. It is agnostic: with the same prompt, a Postgres migration produces$1andON CONFLICT, and a MySQL one,?andON DUPLICATE KEY UPDATE.
<leader>aPp (repo port) uses the type under the cursor (or that of its receiver, if you are inside one of its methods) and writes the type, the constructor and ALL the missing methods, in the port's order. <leader>aPi (repo implement) writes a single method with that context; implement (i/aDi) receives it too if the code is an adapter, and <leader>aPr (repo review) reviews what is specific to persistence (columns that do not match, dialect parameters, concatenated SQL, unclosed rows, unwrapped errors, N+1) and <leader>aPc (repo check, 0 tokens) compares the port's methods with those of the type and the entity's fields with the columns of its table. Silent if it does not apply: a service, a handler or any other function does not pay even for the search (measured: 0 characters of extra context).
Tested with Claude on an empty PostgresOrderRepository with a 4-method port: it returned the constructor and the 4 methods with the real columns from the migration, $1…, the error style of the sibling implementation and no invented fields (what it assumed, such as order.ErrNotFound, it states in note:), for ~$0.0013 with Haiku. Limits: the port is found by name or explicit declaration (not by type inference); the sibling example is the first reasonable one it finds; the schema is only read from *.sql and *.prisma (not from ORM models or the live database).
Service (<leader>aU): application services / use cases
A service can only call the methods that its ports declare. The ports provider finds the interfaces the service receives (fields, constructor parameters, method parameters, in any language) and reads them with their real signatures, each with its path and the instruction to call only those methods; with examples it adds another service of the project as a style example.
<leader>aUiservice implement: orchestrates the domain and calls ONLY the ports in<ports>with their exact names and signatures; business rules are called on the entity (not reimplemented); it propagates the context and translates the ports' errors.<leader>aUrservice review: calls to port methods that do not exist or with wrong arguments, business rules in the service, leaking infrastructure details, swallowed or untranslated errors, non-atomic steps or steps ordered in a risky way (effects before saving), N+1.<leader>aUdservice deps (0 tokens): for each port, which methods it declares, which ones this file calls and which ones the port does NOT declare (it would not compile), which implementations it has and whether any is wired (used outside its file); it lists what needs attention.
Tested with Claude: on a service that called orders.FindByID (the port declares ByID), deps detected it without an LLM and service review flagged it against the real port; service implement used only ByID, Save and Mailer.Send and called the entity's o.Confirm() instead of repeating the rule (what it assumed, such as a method the entity does not have, it stated in note:).
Findings that stay (and how to fix them)
The findings of review and ddd review, and the violations of ddd diagnose, are diagnostics pending attention: they are saved (stdpath("state")/agent-findings.json) and come back when you open the file in another session, even if you do not deal with them today.
- Each one is saved with the text of the flagged line. If the code has moved since then (you edited above, another branch...), it is relocated by that text (up to ±40 lines); if the line no longer exists, it is dropped with a warning (
Agent: 1 saved finding no longer matches the code and was dropped). Saving the file updates the positions. - Only the agent's are saved: LSP ones are not touched. If there is no findings file, opening a file costs nothing.
- Clearing them (from memory and from disk; review and ddd alike):
<leader>acclear discards the one on the cursor line (in visual mode, those in the selection) → one at a time;<leader>aCclear file, all those in this file;:AgentClear [file|all](with a range::'<,'>AgentClear),all= all files.<leader>aDcddd clear, the DDD diagnostic of the whole project.<leader>axstop does not delete them: it only cancels what is running and discards pending proposals. - Fixing them:
<leader>af(fix inline, the one on the line),<leader>aF(fix block, the whole function) and<leader>aDf(ddd fix) read the findings that fall in the function or selection, together with the LSP diagnostics, and propose the minimal change (inddd fix, respecting the layers in<ddd>; if the fix needs a port, type or file that does not exist, it says so innote:and does not invent its content). When you accept the proposal, the findings in that range are considered resolved and also disappear from what is saved.reviewandddd reviewdo not read earlier findings: a new review must not start from what the previous one said. - Flow:
<leader>aDr(review) → tomorrow, another session: the diagnostics are still there →<leader>aDfon the function →⏎.
DDD (<leader>aD)
A feature separate from the normal agent, with its menu, its action registry (lua/agent/ddd/) and its context. Same pipeline and same experience (inline diff, findings as diagnostics, window, history and cost, where they are distinguished as ddd_implement, ddd_review...), but the actions follow the layers and naming of YOUR project.
| Key | Action | Costs |
|---|---|---|
<leader>aDd | diagnose: static analysis of the whole project | 0 tokens |
<leader>aDi | implement: writes the code in the layer where it belongs, with your names and the language's idiom | LLM |
<leader>aDr | review: DDD and SOLID violations as diagnostics (without repeating those the local analysis already detected) | LLM |
<leader>aDf | fix: fixes what ddd review and the diagnostic left pending (minimal change, respecting the layers) | LLM |
<leader>aDm | missing: what is missing around what you see (ports, implementations, use cases, DTOs, wiring, errors, tests), with file names following your convention | LLM |
<leader>aDa | ask: architecture question with the real layers and pieces | LLM |
<leader>aDc | clear: discards the violations of the whole-project diagnostic (also from what is saved) | 0 tokens |
What the local analysis does (free, no LLM): it walks the project once (asynchronously, in batches; it only reads files inside layer directories and that are not tests; cached by mtime) and deduces:
- Layers by directory name (
domain,application,infrastructure,interfaceand synonyms:usecases,adapters,persistence,api,handlers...; adjustable insettings.agent.ddd.layers). The dependency rule points inwards: domain ← application ← infrastructure / interface. Composition roots (main,cmd/,container,di...) may wire everything. - Violations (
◈diagnostics in the code + quickfix list + panel): outward dependencies (with the exact import), frameworks in domain/application (database/sql,net/http,gorm,@nestjs,sqlalchemy,org.springframework... per language: Go, TS/JS, Python, Java/Kotlin/Scala, Rust, C#, PHP, Dart) and badly named files (PaymentServiceinpricing.go). Only the project's own imports are assigned to layers (go.modmodule, relative ones in TS/Python,crate::in Rust):database/sqlis a framework, not "the database layer". - Naming: file style (
snake_case,kebab-case,order.entity.ts,PascalCase) and most used roles (repository,service,handler...), deduced from what already exists; suggestions for missing files respect it. - Aggregates, gaps and connections: for each domain entity (a type with an
IDfield) it looks for its repository port, its implementation, whether it is wired (its name is used in some other file, viarg) and its use cases; it lists what is missing.
What the model receives (<ddd> section, max. 1800 characters, only what is relevant to that target): its layer and what it may depend on, project layers, naming convention with examples, neighbouring files, the pieces surrounding the entities it touches (Order: port OrderRepository (...) · impl PostgresOrderRepository (...) wired ✓ · use cases PlaceOrder), the violations in that file and a line with the idiom of the current language (constructors, errors, injection). Plus the DDD rules (~600 characters).
Measured on a Go project with one complete entity (port + implementation + use case) and another without a port: implement of Order.Confirm() used Pending/Confirmed (the constants of the Status type travel with it) and respected the layer; review found the *sql.DB and the concrete repository stuffed into the entity, plus primitives without invariant; missing listed the vertical slice of Customer with paths in the project's style. Each action, under $0.001 with Haiku. On a repo with 7500 code files, the first analysis takes ~0.9 s (252 layer files) and the next one 35 ms.
Limits: layers are recognised by directory name (if yours are named differently, adjust layers); "entity" is a domain type with an ID field; "wired" means "its name appears in another file", not a dependency-graph analysis; imports are read with regex and types with treesitter, so it is not a compiler. Languages without a treesitter parser only contribute imports.
History and spending (<leader>ah)
Every action is stored in stdpath("state")/agent-history.jsonl (append-only): when, action, model, target, your instruction, the system prompt, the exact message sent, the response, tokens (with cache read/write), cost, time and what you did with the result (accepted, rejected, discarded, N findings...). This way you can see transparently what was asked and what was paid.
<leader>ahopens a selector with one row per run (date, outcome, action, model, cost, target); the title adds up the total spent. The preview shows the full detail rendered as markdown, including the code sent (the target and the code around it, highlighted in its language);⏎opens it large and⌫(BS) comes back to the list with the same filters, search text and run selected.⌃Ffilters the history by agent (claude, opencode, ollama…), provider (who serves the model: anthropic, openai, local…), model, action, outcome or project: pick the dimension, then the value (each with its runs and cost). Filters stack and show in the title with the runs and spend that match;⌃Xclears them.⌃Sopens the spending report (⌫comes back to the list): by period (today, 7 days, total), by action, by model, by day, cost per applied change, what was spent on proposals you did not apply, errors, cache hits, where the prompt goes (system, context per provider, target) and the 5 most expensive runs. It is the basis for deciding what to cut.⌃Ycopies EVERYTHING about the run as markdown (the same text you see in the detail): outcome and event line (accepted at 09:38:27...), target, time, tokens, cost, your instruction, the response, the system prompt and the exact message sent.⌃Pcopies only the prompt (system + message) to reproduce or tweak it. In the detail panel and in the report panel,Ycopies the whole panel. Everything goes to the clipboard (+) and the unnamed register.- Privacy: the history includes your code (it goes inside the prompt).
agent.history.store_text = falsestores only metrics;enabled = falseturns it off. It compacts itself to themax_entries(1000) most recent.
Refine: change a proposal without starting over
A proposal is on screen and almost right? Tab (settings.agent.keys.refine) opens the instruction field again ("how should it change?") and replaces the proposal with a new one that starts from the previous code: the model sees the original target, the proposal and only what you ask to change. The old proposal never touched your buffer; the history links the new run to it. Cost: about one ordinary run per round (your instruction is ~30 tokens, the previous proposal 100-300), instead of rejecting and describing everything again.
The same key on an answer (explain / ask window) asks the next question about the same code with the earlier questions and answers as the conversation (the oldest turns are dropped past ~4k characters). The answer window never takes focus, so while it is on screen the key is also bound on the code buffer you are in (it goes away with the window; a proposal waiting in that buffer gets the key back). Pressing it needs no <leader>al first.
After accepting: verification
Accepting a proposal is no longer the end. agent/verify listens to the replace presenter's events and, for free:
- remembers the buffer's LSP and linter diagnostics before the code is written (the agent's own findings do not count: accepting resolves those);
- waits until the language server settles (
verify.settle_ms, never more thanverify.max_wait_ms) and compares: a diagnostic is identified by what it says (source, code, message), not by its line, so the same old warning one line lower is not new; - nothing new → one quiet line
✓ verified · no new problems; something new → a questionfix regression 1/2: it names the problems, the attempt, the model and the estimate. Yes launches afix regressionproposal over the function that holds them (same experience as any proposal, and verified again if you accept it);verify.max_retries(2) bounds the chain,verify.auto_retry = trueskips the question (still bounded and still behind the spend guard); - a build check (
verify.command = "auto":go build ./...,cargo check,py_compile,dart analyzeof the saved file in machine format (only its errors count, never warnings); or your own per language) runs when you save the accepted code, because a compiler reads the disk. Only the errors on or next to that code are blamed on it; the rest of the project's errors are counted, not guessed at.
Tokens: 0 unless you answer yes; then about 1.8 runs per attempt (the original prompt plus the errors). docs is excluded (verify = false: only comments change).
Several proposals at once
fix all, fix from output and multi-file plans share one engine (agent/batch):
- One confirmation with the total (
3 proposals · claude/sonnet · ~2.3k tokens · ~$0.0012, the files and how many problems each holds). The budget guard is not asked again per piece, but a hard cap still stops the rest. - The pieces run one by one; the statusline says
⠋ agent fix all 2/3. If you cancel (<leader>ax) or the budget blocks one, the rest does not go on. - Each piece is anchored (an extmark), so accepting something above it while it runs does not misplace it.
- The proposals wait in a queue:
<leader>aA]/<leader>aA[jump between them across files,<leader>aAllists them in the quickfix,<leader>aAyaccepts them all after one question (each is its ownu). Each proposal's hint ends with2/4 · <leader>aA] next.
Fix all (<leader>aAa) groups the file's diagnostics by the function (else the type) that contains them: three findings in one function are one request. Sources and severity: settings.agent.fixall (agent findings, DDD violations, LSP/linters; warn and up by default). Tokens: about one fix block run per affected function (5 findings in 3 functions ≈ 3 runs).
Fix from output (<leader>aAo): the errors come from the last terminal buffer, else the quickfix list; <leader>aAO asks for a command (the project's build by default), runs it and uses what it prints. They are parsed by Neovim's own errorformat (go, tsc, cargo, python, javac, flutter analyze, dart analyze (human and --format=machine) and the generic file:line:col: message), 0 tokens; each piece sends only the errors of its function (<build_errors>), not the whole output. At most settings.agent.errors.max_errors (20) errors are read.
Multi-file plans
<leader>aAp on a type or function, then what should change ("add a Done flag to notes, through every layer"):
- Free: where it reaches. Language-server references (completed with
git grepwhen the server never leaves the starting file), collapsed to the declarations that hold the symbol (function or type, with its first line as the signature), capped bysettings.agent.plan(max_files,max_sites). - One request: the instruction and only those signatures (never bodies; ~40 tokens per site) go to the model, which answers ordered steps
n|path|symbol|what. Steps that name a file or symbol that is not in the list are dropped (the model inventing them). - You: the plan panel (
☑ 1. path · Symbol — what).Spaceunchecks a step,aall/none,Enterruns the checked ones,qcancels. Its first line keeps the estimate up to date as you toggle. - One request per step, in order: each is a proposal for its declaration, with the overall plan (this step marked) and what the steps before it proposed as context, so step 3 uses the field that step 1 adds. They land in the queue above and are verified as you accept.
A snapshot (git stash create, nothing in your work tree changes) is taken first: git diff <ref> shows everything the plan changed and git checkout <ref> -- <file> restores a file. Renames are better done with LSP rename (deterministic, 0 tokens); the plan is for semantic changes. Tokens: plan ≈ 1-2.5k in, 300 out; each step ≈ 1 run; a 5-step plan is about 6 runs in total.
Spend guard
Before every request (actions, batches, the staged review, plans) the budget guard (agent/guards/budget.lua, pure core in agent/budget.lua) checks the size of what is about to be sent (characters ÷ 3.5 ≈ tokens) against settings.agent.budget and what the history says you already spent. The price of a token is learned from your own history with that model (no price table). Above confirm_tokens / confirm_usd, or near a cap, it asks (modal with the reasons); a used-up day_usd / session_usd blocks. Every one-shot call (commit draft, staged review, plan) is recorded in the history too, so the spend is complete. The usage report (<leader>ah → ⌃S) gains a Budget line and Suggestions: actions you accept rarely (and what they cost), the share of the spend that went to proposals nobody applied, and how often the verifier had to retry.
Review before committing
<leader>Gv (or ⌃R in the commit panel) reviews what is staged:
- Free checks on the added lines: secrets (AWS/GitHub/Slack/OpenAI-style keys, private keys,
password = "…"literals), merge-conflict markers, debugger/print leftovers per language (not in tests) and sensitive files (settings.git.commit.sensitive). - If there is real code to read (not only lockfiles, generated files, deletions or binaries) one request goes out with the staged diff numbered by its lines in the new file (
+L13added,L12context), withingit.commit.review_max_chars. A finding must point at one of those lines or it is dropped as invented. - Findings land as diagnostics on their lines (open files at once; the others when you open them: they persist like any finding) with source
staged review, and as a quickfix list. The panel's agent line saysreview: 2 findings (quickfix).
Tokens: a typical commit (40-200 changed lines) is ~1.5-6k in and 100-600 out; a docs-only or lockfile-only commit costs nothing.
Context for refactors and explanations
Two providers that are opt-in per action (they cost tokens): callers (refactor, the api lens): who calls the target, from the LSP references, one short line each (path:line code), at most context.callers (6) and two per file, so a refactor sees how the function is really used before it changes a signature (~60 tokens each). gitlog (explain, review, refactor): the last context.gitlog (3) commits that touched those lines (git log -L), one line each (~40 tokens): the "why is it like this" and "this was fixed before".
Project conventions: .agent.md
An .agent.md at the root (or further up) is added to ALL prompts: style, preferred libraries, what to avoid. Max. 1500 useful characters. Example: Idiomatic Go; errors with %w; no logs in pure functions; tests with testify.
Next edit (on demand)
Not autocomplete and not ghost text: one key press (<leader>an) asks the light model (a small local one through light_backend is enough) for the edit you are about to make. What it sees: the lines around the cursor (settings.agent.next_edit.window, 30 each side) and <edits>, your latest changes in this file as -/+ hunks, oldest first.
- The changes are tracked by
agent/edits.lua: a baseline snapshot per file buffer, compared with the buffer when an insert ends or a normal-mode change settles (onevim.diff; no model, no LSP, files overnext_edit.max_linesare not followed). It keeps the lastkeephunks and sends at mostmax_charsof them (the newest win). - With nothing changed in the file it asks nothing (0 tokens, and says why). If nothing in the window clearly follows from your changes the model returns the window unchanged and you get "no changes".
- It is a normal
replaceproposal: inline diff,⏎/⎋, verification after accepting, history. Not cached (a proposal on live code). - Why on demand: a streaming ghost-text subscription spends tokens on every pause; this spends them only when you ask.
Answer cache (repeated questions are free)
A request is identified by everything the model would receive: system prompt, file, context sections, target, task, your instruction, plus backend, model and effort (agent/cache/, pure logic in cache/core.lua). If the same request was answered before, the stored answer comes back with 0 tokens, before the budget guard (nothing to ask about), and goes through the same presenter (the status line says cached · 0 tok · saved $…). A hit is not a run: it never appears in the history (<leader>ah) or the spend report, since no model was called. Any change to the function, to a type or diagnostic it reads, or to your instruction changes the hash, so a stale answer cannot come back.
- Kept by default:
textandfindingsoutputs (explain, ask, review, lenses). Areplaceproposal is never cached: asking again on live code should give a fresh one. An action can opt in or out withcache = true|false. - Stored in
stdpath("state")/agent-cache.json; the least recently used go pastmax_entries, and answers expire afterttl_days. <leader>aAkshows what the cache holds and has saved you (answers kept, times it answered, tokens and money saved: counters the cache keeps itself) and has a Clear button.settings.agent.cache.enabled = falseturns it off.- Tests must never share the real cache (a hit hides the call they count):
K.fake()points it at a temp file.
Project documents (<leader>aS): AGENTS.md, STACK.md, DESIGN.md, SPEC.md
The documents every coding agent (Claude Code, Codex, Cursor, opencode, this one) reads before it changes code, written at the repository's root in the same sections in every project, so an agent always knows where to look. Lowercase keys use presets or the project's own facts; uppercase ones take what you describe in your words and the model writes the document.
| Key | Document | How |
|---|---|---|
bb | the project | build with agent, everything at once: reads the four documents (says which one is missing and its key) and builds the project in its own tab: one plan request, then one small request per file (only the documents it needs, the plan, the API of what is already written and the files it uses), then the manifest's own checks (Go: tidy, gofmt, build, vet, test) and up to 2 fix rounds for the files the errors name. One question before it starts (files, estimated tokens and cost, what it replaces, what the previous build left that this plan drops) |
bt | the foundation | build one layer from STACK.md: manifest, layout, tooling, a minimal entry point that runs (below) |
bs | the domain | build one layer from SPEC.md: entities, rules, use cases and their tests, no interface (below) |
bd | the design | build one layer from DESIGN.md: theme, styles, components, screens, wired to the domain the project has (below) |
m | SPEC.md | modules: what every app repeats (accounts, roles, lists, workspaces, notifications, audit log, uploads, import/export, offline, payments, subscriptions) merged into your SPEC.md, with edge cases, and questions where the answer is yours (below) |
bf | one feature | build one requirement end to end: pick an FR of SPEC.md (○ no test yet, ● tested) or describe a new one, which becomes the next FR-n in SPEC.md (below) |
r | a recipe | prefab task for your stack (a screen with its state, an endpoint, an entity with its repository, a form…): asks its values, then builds it on the project (below) |
v | coverage | 0 tokens: which FR-n have a test, what STACK.md names that the manifest lacks, DESIGN.md tokens no code uses, folders no document mentions (below) |
P | records | PROGRESS.md and the decisions (ADRs) the builds wrote, with a preview |
B | the project | check again: the checks and fix rounds on the files the last build wrote, as they are now (after you edit them, or after a restart; its list is kept per project in the state dir) |
/ | the library | every preset in one search (stacks, designs, their add-ons, principles) with its preview: ⏎ uses it (a stack or design opens its customize step; an add-on is merged into the STACK.md/DESIGN.md there; a principle is added), ^e embeds its text under your cursor, ^y copies it, ^o opens the preset file (to read it or make your own from it) |
o | all four | which are there (●) and which are missing; Enter opens one or starts it |
a | AGENTS.md | the model reads the project (file tree, languages, manifests, Makefile targets, package.json scripts, README, the other documents) and writes how to work in it: real commands, layout, conventions, workflow, boundaries, definition of done. With one there it updates it, keeping what is still true |
A | AGENTS.md | the same sections without a model (0 tokens): commands detected from the Makefile and manifests, top folders, general rules, TODO: lines for what only you know |
c | CLAUDE.md | Claude Code reads CLAUDE.md, not AGENTS.md: creates one that imports it (@AGENTS.md), or adds that line to yours |
t | STACK.md | preset: kind of project → stack (with a preview) → customize → principles. Kinds: backend (Go stdlib / chi / Fiber / Huma / gRPC / workers, Bun + Hono / Elysia, Hono, NestJS, FastAPI, Axum, tonic, Spring, Ktor), web (Next.js, React + Vite, Vite vanilla, Solid, Preact, Alpine.js, Lit, SvelteKit, Astro, Nuxt), full stack (Go + templ + htmx, Bun full stack, Leptos, Next.js + Supabase, T3, Django + htmx, Laravel), mobile (Flutter with Riverpod, Bloc, Supabase, Firebase, offline-first, desktop or no package; Expo, SwiftUI, Compose), self-hosted (Go + SQLite + Litestream, PocketBase, Docker Compose stack, Bun + SQLite), monorepo, CLI / desktop / TUI (Bubble Tea, Wish over SSH, Ratatui, cobra, clap, Tauri) |
T | STACK.md | described ("Go API for invoices, Postgres, self-hosted"), then customize (the add-ons that fit the words you used) and principles; the model writes it knowing the add-ons, and they are merged after |
k | STACK.md | customize the one there: add deploy, CI, observability… (those already in it come checked) |
p | STACK.md | change the principles it carries: the section is rebuilt from the presets you check |
d | DESIGN.md | kind → design (with a preview) → customize. Design systems (Material 3, M3 Expressive, Cupertino, Ant Design, Fluent 2, Carbon, Primer, shadcn/ui, Notion, Linear), styles (neo-brutalism, Swiss, retro pixel, glass, mono minimal) and terminal / TUI (ASCII art, Braille, brutal, classic, retro CRT, modern Charm-style) |
D | DESIGN.md | described ("like Linear but warmer", "an ASCII art TUI with amber phosphor"), then customize; a design you name is sent as the base, a terminal description gets the terminal add-ons |
w | DESIGN.md | screens: one request draws a wireframe of every main screen SPEC.md names (and the states that change the layout: empty, error, too small) in DESIGN.md's own style, character by character at the minimum size, with what each region uses and how it adapts; written as its ## Screens section (replaced when there is one, the old file to the trash). Needs SPEC.md and DESIGN.md. Models copy a picture far more faithfully than prose, so the build lays screens out as drawn |
s | SPEC.md | write what you are building in any shape; it comes back as a spec: requirements FR-n with Given/When/Then acceptance criteria, non-functional NFR-n, data, interfaces, edge cases, milestones, and every gap as an open question (never invented). With a SPEC.md there, the page starts from it: edit freely and it comes back tidy, ids kept |
Customize is one list in groups, merged into STACK.md without a model (rows replace the row with the same first word, rules and commands are added once, so applying twice changes nothing). It opens with the best picks for that stack already checked (an add-on's <!-- default: … -->: in a one-of group the most specific match wins, the stack itself over its language over its category), so Enter alone gives a complete setup; change or uncheck what you want. Round marks (●/○) are one-of groups, squares (■/□) take any number. Only the add-ons that fit the stack are listed (its main language is read from the Stack table, so a Go API is never offered TypeScript tools).
The customize step is a full-screen marketplace too (the same keys): the choices by group on the left (●/○ one-of groups, ■/□ any number), the add-on under the cursor on the right (what it is and what it writes: its Stack rows, conventions, commands, rules), and under both what the choice sets (sets the rows Database, Auth…). p shows exactly the text the document would gain, Space picks (in a one-of group the others go off), ⏎ goes on, Esc goes back, / filters, Tab folds a group, ? lists the keys. <leader>aSk opens it on an existing STACK.md and <leader>aSK on an existing DESIGN.md.
Esc goes back one step (Principles → Customize → stack → kind of project; in T, Customize → the page with what you wrote), keeping what you chose; Enter moves on. Only the first step's Esc closes.
| Group | Choices |
|---|---|
| Package manager (one of) | Bun or npm instead of pnpm: every command, the lockfile and the Stack row are rewritten |
| TypeScript | TypeScript 7 (the native tsc, ~10x faster type checks) |
| Lint and format (one of) | Biome, Oxlint, strict ESLint + typescript-eslint |
| Styling (one of) | Tailwind CSS 4, CSS Modules + tokens, UnoCSS |
| Database (one of) | Postgres, SQLite, MySQL, MongoDB, Turso |
| Data access (one of) | Drizzle, Prisma, sqlc, sqlx, SQLAlchemy |
| Auth (one of) | Better Auth, Auth.js, Clerk, Supabase Auth, OIDC (Keycloak/Auth0/Entra), own session cookies |
| Payments (one of) | Stripe, Lemon Squeezy, RevenueCat |
| Email (one of) | Resend, Postmark, SES, SMTP |
| File storage (one of) | S3-compatible (S3, R2, MinIO), Cloudinary, local disk |
| Search (one of) | Meilisearch, Typesense, Postgres full-text |
| Background jobs (one of) | BullMQ, River, Celery, Inngest, cron only |
| Realtime (one of) | WebSockets, Server-Sent Events, Ably |
| Internationalization (one of) | i18next, Flutter gen-l10n, gettext, go-i18n |
| Analytics and flags | PostHog, Plausible, OpenFeature |
| Unit tests (one of) | Vitest, Jest, pytest, Go + testify, cargo-nextest |
| End-to-end tests (one of) | Playwright, Cypress, Maestro |
| Deploy (one of) | Docker self-hosted + Caddy, Coolify, Fly.io, single binary + systemd, Kubernetes, Cloudflare, Vercel, stores with fastlane, GoReleaser, cargo-dist |
| CI (one of) | GitHub Actions, GitLab CI, Forgejo / Gitea Actions |
| Observability | OpenTelemetry, Prometheus + Grafana, Sentry |
| Quality | lefthook, Renovate, Dev Container, mise, Nix dev shell |
DESIGN.md customize works the same way (best picks checked, Esc back), with its own groups:
| Group | Choices |
|---|---|
| Platforms | web responsive, mobile app, desktop app; TUI only, narrow terminals (Termux, Blink) |
| Theme (one of) | dark + light, dark, light, high contrast; inherit the terminal (ANSI roles, NO_COLOR) |
| Palette (one of) | brand (tints from one color), Gruvbox, Catppuccin, Nord, Tokyo Night, monochrome, phosphor green / amber |
| Motion (one of) | none, subtle, expressive; terminal animations (spinners, typewriter, frame budget) |
| Effects | ASCII gradients, CRT, grain, glass blur, dithering, neon glow; hero art (ASCII art, Braille, synthwave, retro: the art is the app's identity, so the main screen carries it, framed and centered, its value also in plain text) |
| Glyphs (one of) | Unicode box + Braille, Nerd Font, ASCII-safe |
| Density (one of) | compact, comfortable, spacious |
| Accessibility | reduced motion, color-blind safe, screen reader, WCAG AAA |
| Toolkit (one of) | how the design is written in code, as a ## Toolkit table (token → code): Charm (Lip Gloss: one styles file, Place to center, Join to compose, Harmonica), Ratatui, Textual, Ink; Tailwind CSS v4, CSS variables, Flutter theme. Checked from the project's STACK.md: Go → Charm, Rust → Ratatui, Python → Textual, a Tailwind stack → Tailwind |
A terminal design is a real DESIGN.md for a TUI: colors as ANSI 16 / 256 / truecolor roles with fallbacks, typography as monospace cells and glyph sets, spacing in cells, borders as box drawing, motion as frames.
The build (b…) is an orchestrator you can watch, steer, leave and come back to. It builds the whole project (bb) or one layer from the document that describes it, with the same letter that document has in this menu:
| Key | Layer | Reads (required first) | Builds | Skips |
|---|---|---|---|---|
bt | foundation | STACK.md, AGENTS.md | manifest, folder layout, formatter/linter/test config, Makefile or scripts, a minimal entry point that builds and runs, one smoke test, README (15 files at most) | domain logic, screens; the wiring check and the use phase |
bs | domain | STACK.md, SPEC.md, AGENTS.md | entities, value objects, rules, errors, use cases and ports where STACK.md's layout puts them, each with tests that prove its FR-n; in-memory adapters only when the tests need them | the interface and the entry point; the wiring check (nothing calls the domain yet, by design) and the use phase |
bd | design | STACK.md, DESIGN.md, SPEC.md (when there), AGENTS.md | theme or tokens, styles, components and screens (as ## Screens draws them), wired into the entry point; it calls the domain the project has, sample data when there is none | nothing: the wiring check, the use phase (with SPEC.md) and the look all run |
A layer is built on top of what the project already has: the plan request gets the existing files as <project> (their paths, the manifests whole, the API the code declares), every file request sees that API and the existing files it uses, and an existing file the plan has to change (the domain adds a dependency to go.mod, the design makes main.go open the screens) is sent as it is and comes back whole. The review shows those as replaced. Usual order: bt → bs → bd, each reviewed before anything is written; any of them alone works too (a domain on an empty folder plans its own manifest). Each layer keeps its own saved run and its own list of the files it created: a replan of the domain only drops the domain's old files, X sends only the files that layer created to the trash (the ones it only changed stay), and the foundation's run is never touched by the domain's. The tab's top line and the statusline name the layer (domain · from SPEC.md, ⠹ build domain 3/9).
One feature at a time (bf): after the first build, most work is a new feature. The picker lists the requirements of SPEC.md with whether a test proves them yet (the preview opens SPEC.md at that requirement), and + new first:
- An existing one opens the feature build with that requirement's text: its line to the next requirement, plus every other line of SPEC.md that names it (its edge cases, its open questions).
- A new one: you describe it on the draft page in your words; one request (main tier, SPEC.md as context) turns it into a requirement (title, "The system must…", 2-6 Given/When/Then criteria, edge cases, the questions only you can answer). A confirmation shows it; Add and build writes it into SPEC.md in the document's own style (heading
### FR-n:or bullet- **FR-n**, the next free id), its edge cases under## Edge casesand its questions under## Open questions(numbered on from the lastQn); the old SPEC.md goes to the trash.
The feature build plans the smallest set of files that makes the requirement work for the user: the domain it needs, the interface that reaches it (when there is one), the tests of its acceptance criteria, following the patterns the code already uses (same folders, naming, state management, error handling), on top of the project as it is (<project>, like the layers). Existing files it changes come back whole and keep what the rest uses. The wiring check and the use phase run, with scenarios from that requirement alone. Features are additive: one feature's replan or clean never touches another's files. One feature run is kept at a time; picking another requirement starts afresh.
SPEC.md modules (m, 0 tokens): your SPEC.md stays yours (you write the core of your business); the modules add the well-known parts you would otherwise forget. It opens as a small marketplace on the whole screen: the modules by group on the left (□ not picked, ■ picked, + comes with what you picked, ✓ already in SPEC.md), the one under the cursor on the right (what it is, its requirements with their criteria, edge cases, and the questions that are yours), and under both what SPEC.md would get from your choice (the modules, the FR range, the counts, what came in because another needs it). Nothing is written until you add, and the confirmation sums it up first (the old SPEC.md goes to the trash; cancelling it brings you back to the marketplace as you left it).
- List:
j/kmove,Spacepicks,⏎reads and customizes,pshows what SPEC.md gets,/filters by name, description, key or requirement title,{/}jump between groups,Tabfolds a group (on a folded title it unfolds it),hhides what SPEC.md already has,t/dopen the STACK.md add-ons / DESIGN.md patterns it goes with (below),Aadds,?lists every key,qcloses.★marks a module your SPEC.md already talks about without having it (a word of its name or one of its requirement titles appears in the text: 0 tokens), and its pane says which word. - Read and customize (
⏎):j/kscroll,Spacekeeps or leaves out the requirement under the cursor (its edge cases and questions go with it; leaving every one out drops the module),Esc/hback to the list. - What SPEC.md gets (
p): exactly the text that would be added, as it would read, with what you left out missing;Escgoes back.
| Group | Modules |
|---|---|
| Every app | accounts and sign-in (sign up, sign in, reset, verification, lockout) · roles and permissions (ownership, changing roles) · lists (pagination, empty states, search and filters, delete with undo and a trash) · profile and preferences · first run and onboarding (empty states) · loading, errors and feedback (waiting, failures, undo) · privacy and personal data (consent, export, deletion) · languages, time zones and currencies · reviews and ratings · favorites and wishlists · multi-step flows · bulk actions |
| Teams | workspaces and invitations (data kept apart, members, leaving) · notifications (in-app, email/push, preferences, unsubscribe) · comments and mentions · sharing and links (people, links, expiry) · activity feed · chat and direct messages · tasks and boards · calendar and booking · contacts and CRM · support tickets · newsletter |
| Data | audit log · file uploads · import and export · offline and sync (queued changes, conflicts) · version history (compare, restore) · dashboards and reports · tags and categories · background and scheduled jobs (retries, never twice) · public API and webhooks (keys, signed events, limits) · forms and surveys · content publishing · media gallery · maps and location |
| Money and commerce | payments (checkout, receipts, refunds, idempotency, webhooks) · subscriptions (plans, trials, renewals, grace period, upgrades, cancelling) · cart and checkout (orders, stock) · invoices and receipts (numbering, credit notes) · coupons and promotions · product catalog · inventory · seats and billing |
| Security | two-step sign-in and sessions (second factor, devices, recovery) · limits and abuse protection (rate limits, guessing, reports, blocking) |
| Platform | back office (finding records, acting with care) · feature flags and settings (gradual release) · single sign-on |
| Quality | accessibility · performance · reliability and backups · observability |
How a module goes in:
- Each requirement gets the next FR-n in SPEC.md's own style (heading or bullet) with its Given/When/Then criteria; its edge cases go under
## Edge casesand its questions under## Open questions(numbered on from your lastQn), tagged with its id. - Questions, not invented rules: lockout limits, roles, taxes, grace periods… come as questions; the build's plan decides them explicitly until you answer.
- A requirement you already wrote (same title, any case or punctuation) is kept as you wrote it, and the module's edge cases and questions for it are tagged with your id.
needs:pulls what a module builds on (subscriptions → payments → accounts, needs first) and the confirmation says so.<!-- module: accounts -->marks where a module went in: applying it again adds nothing. Nothing is ever removed.- Then each new requirement is a
○inbf(build it) and in the coverage (v).
Modules are plain files in agent/kit/presets/spec/<group>/ (## Requirements with ### Title, "The system must…" and criteria; - [Title] … edge cases and questions; <!-- needs: … -->). They are also in the library (/), where the search looks at their requirement titles too (refund finds payments).
Starter kits open the list of the marketplace: a product type as a set of modules (SaaS for teams, marketplace, online store, internal tool, blog, booking, community, support desk, API product, consumer mobile app, project tool, forms and surveys). Space on a kit picks all its modules (◧ = some picked), ⏎ goes to the first one, and its pane says who it is for, what it adds and the decisions to take first. They are files too: agent/kit/presets/spec-kits/<id>.md with <!-- modules: key key … -->.
Narrow terminals (under 100 columns): the list and the pane take turns on the whole screen: ⏎/l opens the pane, Esc/h gives the list back.
What a module goes with: every module (and kit) names the STACK.md add-on axes and the DESIGN.md screen patterns it pairs with (<!-- tech: auth email -->, <!-- design: forms auth-screens -->), shown in the pane under "Goes with": accounts → Auth, Database, Email · Forms, Auth screens. The names are the library's own (<leader>aSt / <leader>aSd customize steps), and a test checks every name exists. In the marketplace t opens the STACK.md add-ons over it (the groups that go with what you picked marked ★, the cursor on the first; with nothing picked it uses the module or kit under the cursor) and d the DESIGN.md add-ons with those screen patterns already checked; closing them puts you back in the marketplace with your picks. They need a STACK.md / DESIGN.md in the project. DESIGN.md has a new group, Screen patterns (forms, data tables, empty states, onboarding, settings, navigation shell, toasts, dialogs, command palette, search and filters, dashboards, auth screens, pricing and checkout, comments, file upload, notification center).
Recipes (r): prefab tasks, plain Markdown files in agent/kit/presets/recipes/<group>/ (add your own: the header of the presets plus one <!-- input: key | question --> line per value, and a body with ## Make, ## Rules, ## Tests using {{key}}, {{key_snake}}, {{key_camel}}, {{key_pascal}}, {{key_kebab}}). Only the recipes whose for: fits STACK.md are listed (go+server = both, go+!server = a Go project that is not a server). Picking one asks its values, fills it and opens the recipe build: the recipe says what to make, STACK.md and the existing code say where and how (its paths are only a hint when the project already does it differently). Recipes skip the wiring check and the use phase. They are also in the library (/).
| Group | Recipes |
|---|---|
| flutter | screen with its state (Riverpod Notifier or Bloc/Cubit), route and widget test · data feature (model, repository interface + implementation, provider) · form with validation · list and detail |
| go | HTTP endpoint (handler, service, route, httptest) · entity with its repository (port, in-memory + SQL adapter, contract tests) · CLI subcommand · Bubble Tea screen (teatest golden) |
| web | UI component (states, tokens, accessibility) · page with its route and data · data table · validated form · settings page |
| general (typical features) | CRUD resource · auth flow · search endpoint · background job · webhook receiver · file upload · transactional email · feature flag |
| typescript, python, rust | API route / HTTP endpoint with validation, service and tests |
| general | module with its tests (any stack) |
Coverage (v, 0 tokens): a report in a split, read from the documents and the code:
| Section | How it is checked |
|---|---|
| Requirements | every FR-n of SPEC.md's Requirements; proved when a test file names it (FR-3, not FR-30) or a build plan's verifies does and that file exists. ⏎ on an untested one opens its feature build |
| STACK.md names the manifest lacks | package.json: the tools the build knows; pubspec.yaml, go.mod, Cargo.toml, pyproject.toml: the package names the Stack table writes in backticks |
| DESIGN.md tokens no code uses | the first column of the Color tables and every --css-variable, searched in every spelling (text-muted, text_muted, textMuted, TextMuted) |
| Folders no document mentions | top-level folders and those one level under lib/, src/, internal/, cmd/, app/, pkg/…: the layout drifted from STACK.md |
⏎ opens the place a line is about, r reads again, q closes. Ask tests to name the requirement they prove (// FR-3: …): the builds' tests do.
Records (0 tokens, settings.agent.kit.build.adr / .progress):
- Decisions (
docs/adr/NNNN-title.md): when a review is accepted, the plan's decisions that no earlier ADR records are written as one ADR (Status, Context, Decision, Consequences: the files created and changed) and indexed indocs/adr/README.md. A build that decides nothing new writes nothing. Every later plan follows them: the decisions still in force go to the plan request as<decisions>(oldest first; the newest kept past ~4000 characters), and the plan lists only new choices. When a build truly must reverse one, the plan says so as a decision of its own (Supersedes ADR 0002: Bloc for state because …): its ADR's Status sayssupersedes ADR 0002, the index marks 0002superseded by 0005, and 0002 is no longer sent to plans. An ADR is never edited. The feed's plan line says how many it followed (follows 3 ADRs). - PROGRESS.md at the root, rewritten whenever a run settles:
Now(the last build, how it ended, how to resume it),Done(a log, one line per finished build, kept across rewrites),Open(files not written, failing checks, issues the use phase found, SPEC.md's open questions),Next(the requirements no test proves yet). Any agent reads it to go on where the last one stopped.
Both are linked from AGENTS.md's "Read first" the first time, and neither is ever read as code by a layer build.
What a build may claim, and what it may throw away (agent/kit/build/proof.lua, loss.lua, kit/decls.lua; all free, no model). Checks are compilers and unit tests: they say the files agree, not that SPEC.md is delivered. So the closing line says what is proven. A build of the whole project, a feature or a recipe ends ✓ built only when every requirement has a test that names it and nothing is open; otherwise ◐ built · 23 files · checks pass · 5/17 requirements have a test · core without one: FR-16 · 2 placeholders. A requirement is core when SPEC.md's Goals cite it (FR-1 to FR-15 ranges included) or its line says (core). A placeholder is code that says it is not real yet (TODO, UnimplementedError, todo!(), "placeholder", "coming soon"; an input's own placeholder= attribute, tests and generated files are not). A layer build (foundation, design, domain) reports the same numbers but keeps ✓, because one layer cannot meet most requirements. Also in the line: a requirement that had a test before the build and has none now, and the public declarations of the files the build rewrote or deleted that are gone. PROGRESS.md gets a Proof line under Now and every item under Open. settings.agent.kit.build.strict = true makes such a build end ✗ incomplete instead.
- Deleting code asks. The files a replan drops are inventoried before anything goes to the trash (lines, public types, tests and which requirements they name, who still uses them). The review screen says so (
⚠ deletes 2,591 lines and 9 public types), and when what goes holds a real declaration or 60+ lines,⏎opens a confirmation with the list first.<leader>Fustill brings them back. - A whole-project build sees the code that is there.
<leader>aSbbover a project that earlier builds left code in used to plan from the documents and the ADRs' text only, and wrote the code a second time under other names (twoProjectRepositoryports, twotimeline_view.dart: 364 analyzer issues). Now the plan request carries the project's files and the API they declare, and is told to use exactly those names and to merge duplicates instead of adding a third. - A type is declared once. Every project gets a free check,
no type is declared twice: a public type (class, interface, struct, trait, enum) in two source files of the same language fails it and goes to a diagnosis (Go: per package; tests, generated code and tool scaffolds do not count). Flutter/Dart projects also runflutter test(a project with atest/folder), and Rust projects withtests/runcargo test: "it analyzes" was all that was checked. - Plans name the risk and defer honestly. The plan lists
risks(the hardest requirements, riskiest first; the real path through the first is built right after the manifest, before the layers that only present or store) anddeferred(what this build cannot deliver, and exactly what is missing). The review shows⚠for a requirement the plan neither builds, wires nor defers, for a file that stands in for a core requirement, and for a plan that names no risk. These are warnings for you to read, not a reason to ask for a second plan. (Building the risky path first as a verified skeleton before the rest is not enforced: it is a rule in the plan prompt that the warnings check.)
| Phase | What happens | Your keys |
|---|---|---|
| ready | pre-flight: the four documents and their size, the model of each phase (plan · files · fixes · use, each on the main or light tier of <leader>am), fix rounds, what the plan request will cost | ⏎ plan · 1 2 3 4 tier of each phase · + - fix rounds |
| plan | one request writes the build's contract: how it works, the architecture and dependency direction, the key decisions (and the SPEC questions it had to assume), then every file in order with what it does, what it exports and promises (NewKeypad() — the highlight starts on =), what a test verifies (FR-n), the documents it needs and the files it uses, and the wiring: one row per requirement from what the user does to what runs to what they see and in which file (FR-2: presses + → ui.Update → ChooseOperator → view.go shows "12 +"), so a feature never stops at the domain. The contract goes to every file request (cached), so files written apart agree. Shown as plan.md in the code pane while it is made: first what the model read and its thinking steps (the CLI streams the thinking without its text on subscription accounts), then each section and file as it arrives. Long lines wrap: nothing scrolls sideways | x stop |
| review | the plan with an estimate per file and in total (with and without the cache), what it replaces, leftovers of the last build. This is the confirmation: nothing is written before ⏎ (an Enter that arrives as the review appears, typed ahead or pressed twice, is ignored) | ⏎ build · x skip a file · 2 3 tiers · n new plan · p the plan request |
| write | one request per file; the code pane types each file out, the feed says what went in and what it cost | x stop · q hide (it goes on) · p the request of the file under the cursor |
| check | the manifest's own tools plus free consistency checks (below), then the interface uses what the plan exports: a function the plan promised to other files that only its own file or a test calls (Calculator.Expression() implemented and tested, never drawn) fails the check and goes to a diagnosis like any error (0 tokens to find; values nobody uses, a style or a color token, are only context for the use phase) | |
| use | when every check passes, the app is used like its user would. One request (the use model) writes scenarios from SPEC.md (flows, Given/When/Then, edge cases, the state the user needs mid-flow) and DESIGN.md (its smallest size): steps and what each must show, the expectations from the documents, never from the code. A driver performs them: terminal UIs in a private tmux at the scenario's size, keys one at a time like a person types; command lines run once per step (output, errors, exit code); web apps (dev server, static page, the project's own server) and Electron in Playwright, read as a screen reader would plus the layout problems measured in the page (sideways scroll, controls out of the window, overlaps, targets under 24 px, cut text) and the JavaScript errors; Android over adb on the device or emulator you started (uiautomator hierarchy + screenshot). Every step is checked for free (texts that must and must not show, exit codes, crashes, actions with no visible effect); one verdict request reads the scenarios as the user lived them and names what to fix (blocker, major, minor). Fixes go like a diagnosis' files, the golden snapshots are written again, and the same scenarios are replayed: use_rounds fixes at most (2), the last play only reports. The frames and screenshots stay in the state dir (the feed says where). An app that cannot be driven here (no tmux, no Playwright, no device) skips to the look at 0 tokens; off with settings.agent.kit.build.use = false | |
| look | then the screens as they render (Go teatest golden files: exactly what the terminal shows, colors stripped) go to the model with DESIGN.md (its Screens wireframes first when it has them; never invented) in one request (cached system prompt, ~$0.006). What differs (not centered, cut off, a missing frame) is fixed in the code like a diagnosis' files, the golden files go to the trash, the checks write them again and they are looked at again: look_rounds looks at most (2: one fix, verified). r stays at 0 tokens; off with settings.agent.kit.build.look = false | |
| fix | rounds go on while they make progress (the failure changes), up to 8; they stop after max_fixes rounds in a row that change nothing (the ± keys), and a round that changed nothing escalates the next one to the main tier with more effort. Each round: first a diagnosis: one request sees the tool's output, the API (with each declaration's doc comment) and the files involved, and names the root cause and every file to change, including files the errors do not name (a test fails, the bug is in the code it tests). Then each of those files is fixed with the diagnosis and its own instruction, the code before the tests. A fix answers with edit blocks (<<<<<<< SEARCH the exact current lines ======= the new ones >>>>>>> REPLACE, applied here; UNCHANGED when the file is right): a few hundred output tokens instead of the whole file. A block that does not match the file, an answer much shorter than the file (a fragment taken for it), leftover block markers, prose or tool-call markup, or "unchanged" for a file the diagnosis said how to change are caught before anything is written and asked again once (after blocks that do not apply: for the whole file); still wrong, the file stays as it was. A fix that weakens a test (fewer assertions or waits, a new skip) is refused and the test stays as it was; a check whose tool does not run sends the manifest. A file the plan missed (an asset go:embed reads) can be created by the diagnosis ("new": true, a safe path, 2 at most). Every repair (the checks, each use or look fix) has its own rounds; after a use fix the diagnosis gets what the user met (<user>): a test that expects the opposite locks the bug in and its expectation changes, not the code back | |
| done | ✓ or ✗, totals, every file with its lines and cost | ⏎ opens a file · r checks again · n new plan |
- The tab: on the left the phases as chips (done ones green, the current one with a spinner, the next ones faint), the progress bar, then the files (the one being written on a full-width band, like the agent's inline target) and the checks; models and documents only before it writes. The code (
cshows/hides it) and the feed (f) on the right; with both hidden only the list advancing is left (settings.agent.kit.build.viewsets which panes it opens with). The statusline of the tab is its own (a lualine extension): the phase as the mode chip in the color of how it stands (red failed, yellow stopped, green done), the keys of the moment (as many as fit, most useful first), then progress · money (and cache · time when the screen is wide). - Reading and moving:
zputs the code pane alone on the screen with the cursor in it (the plan, a file, a request) andzagain brings the panes back;lhides or shows the files,cthe code,fthe feed;Tab/Shift-Tabmove the cursor between the panes;Pshows the plan (plan.md, rendered) at any moment. - Start over:
nplans again right away (the files of the old plan stay; the new review says which it replaces and which leftovers go to the trash).Xcleans the build after a confirmation that lists them: every file it wrote goes to the trash (<leader>Fubrings them back), empty folders it made are removed, the saved run is forgotten; the four documents are never touched. - Nothing is lost: the run is saved after every step. Closing Neovim with a plan in review and running
<leader>aSbb(or the layer's key) again brings the review back with the plan on screen; a run stopped mid-write continues from the first file not written. - Leave it running:
qcloses the tab, the build goes on; the statusline shows⠹ build 7/25 · $0.031from any buffer, a notice says how it ended, its key (<leader>aSbb, or the layer's) brings the view back exactly where it is. - Resilient: the run is saved after every step (state dir, per project). Closing Neovim mid-build and opening it again offers to continue from the first file not written; the spend so far is kept.
- Transparent: money, tokens in/out, the share read from the cache and the time, live; per file its lines and cost;
pshows the exact request (system prompt and message) a file was written from; every request is also in<leader>ah. - Clean answers: a model that wraps the file (a
fence with a sentence around it, the prompt's<file>tags echoed, tool-call markup leaking from the CLI, "I'll make the file…" before the code) is cleaned before anything is written; when it is still not a file (Go withoutpackage`, JSON that does not parse, markup left) the request is asked again once with the reason. A broken fix never replaces a file that was a file. Each of these used to cost a full fix round. - Cheap: every file and fix request shares ONE system prompt (all four documents + the plan), so the backend caches it once and every later request reads it at a tenth of the price (measured with claude/haiku: ~20k of ~25k tokens per request from the cache); the message carries only the file's focus, the API of what exists and the files it uses.
Checks (never commands written by the model): Go go mod tidy · gofmt (fixes itself) · build · vet · test; Node/Bun package.json has STACK.md's tools (Vite, Vitest, TypeScript, Biome… named in the Stack table) · install (the package manager STACK.md names when there is no lockfile yet) · every id the code looks up exists (HTML vs getElementById, querySelector('#…'), *_ID constants) · every control the page declares is handled (a data-action/data-key/data-op name such as equals that no script handles: the button does nothing while every unit test passes) · every stylesheet and module is used · tsc --noEmit · the lint (Biome/Prettier fix format themselves), build and test scripts; Python uv sync · compileall · ruff · pytest when declared; Rust, Dart and Flutter their build/analyze. A Go module version the model invented (invalid version: unknown revision) is dropped with go mod edit -droprequire and go mod tidy writes the real one (0 tokens; go.mod is asked without a require block, and the plan never lists files tools generate: goldens, lockfiles, build output). Formatters (gofmt, Biome, Prettier) run again after every fix round. Missing golden files (snapshot tests run for the first time) are created by running those packages' tests with -update (no tokens; review testdata/*.golden), then the checks run again. A build is done only when every check passes; otherwise it ends failed with the fix rounds it tried.
Measured fix (claude/haiku): ascii-calc's keypad opened on "7" so Enter typed a 7 and three UI tests failed; the diagnosis named keypad.go (not in the errors), one fix there, tests untouched, every check green: 2 requests, $0.008.
Measured on two projects (claude/haiku): a 25-file Go TUI and a 23-file Vite + TS 7 + Bun web app each cost about $0.08–0.10 in total, documents, plan, files, checks and fix rounds included, in 7–8 minutes. Known limits: the use phase reads screens as text (terminal cells, the accessibility tree and measured layout, the Android hierarchy): colors and pixel-level looks still need your eye (the screenshots are kept for that); native desktop toolkits (GTK, Qt, Flutter desktop) and iOS have no driver; library APIs the model knows badly can be invented (a fix round usually repairs them).
Principles are presets too, appended to STACK.md as ## Engineering principles (one ### each): SOLID, Clean Code, one file one responsibility, hexagonal, testing, error handling and security are checked by default; Clean Architecture, DDD, Twelve-Factor, observability, API design, performance, functional core, accessibility, git workflow and documentation are one Space away. The model writing a STACK.md only gets their names (to keep the layout consistent); their text is the preset's.
- Never in silence: a document that is there is replaced only after a yes, and the old one goes to the trash (
<leader>Fubrings it back). A file with unsaved changes in a buffer is never written under. - The page (
s,D,T) is a Markdown float::wor Ctrl-s sends it,qcloses it; what you wrote is kept for the session, so closing it or a failed request loses nothing. - Linked: creating STACK.md, DESIGN.md or SPEC.md adds its line to AGENTS.md's "Read first".
- Language: the documents are written in
settings.agent.kit.language(English by default) whatever language you write the description in: the model translates it and keeps names and domain terms. Measured with Haiku: Spanish drafts came back fully in English (they came back mixed before the rule was moved to the end of the request). - Facts vs choices: AGENTS.md and SPEC.md never invent (a gap is a
TODO:or an open question); STACK.md and DESIGN.md are choices, so the model makes them, marked(proposed)with the reason, and keepsTODO:for what only you know. - Commands come from what the project declares: the Makefile's targets first, then the lockfile's manager (npm/pnpm/yarn/bun, uv/poetry/pdm/pip, Go, Flutter/Dart, Cargo, Maven/Gradle wrappers, Swift, Deno, Composer, Bundler) and only the tools a manifest names (no pytest or ruff guessed).
- Cost: the model ones run once on the main tier (
kit_agents,kit_stack,kit_design,kit_spec, reassignable insettings.agent.tiers), through the spend guard and into the history (<leader>ah); presets andA/c/pcost 0 tokens. - Your own presets: drop a Markdown file in
lua/agent/kit/presets/design/,stack/<category>/(a new folder is a new category),addons/<group>/(<!-- for: ts web -->says which stacks it fits: tags from the stack's words,presets.tags) orprinciples/, starting with<!-- name: … -->and<!-- desc: … -->(<!-- core: true -->checks a principle by default).scripts/test-kit.shchecks every preset has its document's sections.
Merge conflicts (<leader>Gx…, inside a repository)
git-conflict.nvim already chooses ours / theirs / both and jumps between conflicts (<leader>Go T B N n p l). The agent adds what it cannot do:
| Key | What it does |
|---|---|
xt | trivial conflicts for free (0 tokens): both sides made the same change, or (with merge.conflictStyle = diff3) only one side changed anything; trailing whitespace does not count |
xr | the conflict under the cursor (or the next one) to the agent |
xR | every conflict of the file (the trivial ones first, for free): ONE confirmation with the estimate, one proposal per conflict, reviewed in the proposal queue (], [, Y) |
xf | the files git lists as unmerged, with how many conflicts each holds |
xs | git add of the current file once no marker is left (asks first) |
Each conflict is sent with the story of the merge, read from git and never changed: which operation is in progress (merge, rebase, cherry-pick, revert) and which branches, what each side did (commit subjects touching that file), and what each side changed in the file against the common ancestor (stages 1, 2 and 3 of the index). The result has no markers; where both sides changed the same thing differently it combines them and says how in the note, and it never drops a side's change silently. Nothing is written until you accept the inline diff. Knobs: settings.agent.conflict (conflicts per batch, characters of each diff, commit subjects).
Doctor (<leader>aX): the app does not build, does not start, or dies
A troubleshooting agent for failures that are not one more squiggle in a function: a build that breaks, a server that will not start, a crash at runtime, a project that is off before you compile. Hand it the failure the easy way:
| Key | Evidence |
|---|---|
xx | the best at hand: the last terminal if it holds errors, else the quickfix list, else the clipboard, else it asks you to describe it |
xp | the clipboard (copy the error, press this) |
xc | a command it runs for you, captured: the project's own build/test/run commands, docker compose logs, or one you type (a command that passes says so and stops) |
xd | in your own words (a page to write on); the last terminal rides along when it looks like errors |
xl | the end of a log file of the project (or any path) |
xs | (visual) the selected lines |
xh | before compiling: what the language servers already flag in the project plus every free check below |
xr | reopens the last case of this project |
How it works, in this order, and what each step costs:
- Evidence, free. Colors off, repeated lines folded into a count, secrets blacked out (passwords, tokens, API keys, bearer headers,
user:pass@host, AWS/GitHub/Slack keys, private keys), the long middle cut so the lines around the first errors and the end of the output survive, the stack frames that point into the project turned into code excerpts (Go, Node, Python, Rust, Java…file:line; a Python traceback puts the culprit first;node_modules,vendor, venvs and the standard library are skipped), the toolchain versions the project uses (Node, Go, Python, Rust, Java, Dart, Docker…) and what git says changed lately (status, last commits, the size of the uncommitted diff: "it worked before" starts there). - Known causes, free. A table of the usual suspects matched on the output (a missing module, a port in use, the Docker daemon down,
ECONNREFUSEDto a well-known database port, a wrong Node/Go version, a missinggo.sumentry, no C compiler…) plus checks of the project itself: dependencies not installed, a lockfile older thanpackage.json,.envmissing keys its example has, the Node/Go version the project pins against the one installed, Docker not running, a merge or rebase in progress. They are shown before any model with the command that fixes them. - The model, once.
⏎on “Ask the model” shows what is sent (sizes, the model, about how many tokens) and asks. One request reads the evidence, finds the root cause (the first error, not the cascade) and writes ordered steps. If it cannot conclude it may ask to read a file, search for a string, list a folder or run one read-only command, at most twice more; that is all it can do: no shell (a command is split into words and checked against an allowlist of diagnostics: versions, package lists,git status/log/diff,docker ps/logs,lsof -i :PORT…), no pipes or redirects, and secret files (.env, keys,.ssh,.aws,.npmrc…) are never readable; every refusal goes back to the model as the result.pshows the exact text that would be sent. - Steps. A step is a command (
⏎runs it captured, in the project folder: its output and exit code come back into the case;truns it in the integrated terminal for interactive things and servers), an edit (⏎: one proposal with the usual inline diff, accept/reject, the queue; the screen gives way to the code and the step is done when you accept) or a manual thing (Spacemarks it). Nothing runs without a yes that shows the exact command; a command that destroys or rewrites something (rm -rf,git push --force,git reset --hard,DROP DATABASE,curl | sh…) is marked!and asked twice. - Check again.
rruns the failing command again (or reads the terminal) and says whether it still fails; if it does, a new case starts carrying what was tried, so the model never suggests it twice.aasks a follow-up (what you saw after a step).
The screen is the same full-screen layout as the other markets (list, reading pane, strip; ? lists the keys; narrow terminals show one pane at a time). A case is kept in memory: close it, run something in the terminal, <leader>aXr brings it back. When the model answers after you closed the screen you are told. Knobs: settings.agent.doctor (terminal lines, characters of output sent, stack frames, rounds, command timeout, characters per tool result). Runs are in the history (<leader>ah).
Evals: how good is each model? (make eval)
The claim of the agent is that the pipeline (curated context, strict output, free checks, one bounded retry) lets a modest model deliver work you can accept. evals/ is where that is measured: small projects with a question and a check answered by code (the new code parses, a script asserts its behaviour, nothing outside the target moved, a finding sits near the planted bug), run through the real pipeline against a list of models. It reports pass@1 (one try), pass@k (any of the samples), how often the output was usable, the number of tries and the cost, per action × model. SCAFFOLD=0 measures a model bare, so the table says what the pipeline adds. A free regression test of the harness itself (every case passes its golden answer and fails a do-nothing one) is part of scripts/test-agent.sh. Cases, cassettes and the rules for writing them: evals/README.md. Infrastructure errors (a server that answers 500, a gateway that refuses the account) are counted apart and never as the model failing, a model that errors three times in a row is given up on instead of asked thirty more times, flaky counts the cases whose samples disagree (how predictable the model is), and per pass is what one usable answer costs. The committed baselines (evals/baselines/) become evals/RESULTS.md with make eval-table; make eval BASELINE=… fails when something got worse.
Backend errors are completed with what to do about them (agent/backends/hints.lua: a gateway that blocks the account, an opaque server error, no credit, a rate limit, no login): the message is never replaced, only followed by one line of advice.
Capability profiles: small models get help
Each model belongs to a class (small, medium, strong; agent/profiles.lua), decided by settings.agent.capability.models, then by what make eval PROFILE_OUT=1 measured for it, then by its name (a 7b is small, haiku medium, sonnet strong; an unknown local model is small). What the class changes:
| small | medium | strong | |
|---|---|---|---|
| target limit (declined before sending, with what to do) | 60 lines | 150 | the global limit |
| extra attempts when the answer is unusable | 2 | 1 | 1 |
| extra context budget | 4000 chars | 6000 | context.max_chars |
server-shaped output (Ollama format, OpenAI response_format) | yes | no | no |
| an instruction that is several things runs as a chain of single steps | yes | no | no |
An unusable answer is one the free checks reject: no code block, findings in the wrong format, or code whose syntax errors the file did not have before (a treesitter parse of the file as it would be, agent/syntax.lua, no server needed). The same job asks again with the model's answer and the reason in the prompt; the usage of every attempt is added up, so history and budget see the real bill. With settings.agent.cascade.enabled, a model that is down, still unusable after its repairs, or measured to fail the action hands the job to the main model (or cascade.to) and says so; it is off by default because it spends where you did not choose to. Measured advice per action: below 40% of the cases a model is declined for that action, below 90% it gets two repairs (3 cases or more are needed to say anything). settings.agent.capability = { enabled, measured, models, small, medium, strong } and cascade = { enabled, to }.
Chain of steps (agent/decompose.lua, for refactor and implement): an instruction the user typed as a list, as ;-separated clauses or with "then" is split for free (no model) into at most five steps, and the job asks for one edit at a time over the code the previous step produced, each checked like any answer (repairs included). The last step becomes the proposal, diffed against the ORIGINAL code, so accepting, <leader>au and verification behave as always; the budget guard and the preflight see the whole chain (2 steps). It costs the steps, so only the small class chains by default. Measured on Claude Haiku (a medium model) with make eval FILTER=three-edits DECOMPOSE=1|0 SAMPLES=5: 100% either way, 2.5 calls and about twice the cost with the chain, which is why it is not on for models that already do multi-part edits; whether it pays for a 1.5B-7B local model is what the same command is for.
Trust: what you are told, and taking it back
- Before: the spinner carries the estimate (
~1.8k tok);trust.preflight = "notify"prints the whole line first (claude/haiku · light · reads rules, types · ~1.8k tok · ~$0.0007);trust.preview = trueshows the start of the message and asks before anything leaves. - After: the proposal's hint says whether anything checks it:
checked on accept (LSP),checked on save (build),comment only · code untouched, orunverified · no language server. Findings and answers are labelled advice. When a server is attached but says nothing after you accept, the line isaccepted · unverified, never "no problems". - Spend: the statusline shows what this session cost next to its cap (
$0.031/$2.00, in the warning colour near the cap); it counts in memory, it never reads the history on a redraw. - Undo:
<leader>autakes the last run back. Every accepted proposal is remembered with the lines before and after and where they are now, and the proposals of one batch (fix all, accept all, a plan) are one group across files. A range you edited after the agent wrote it is left alone and reported. The restore is an ordinary edit:uundoes the undo. The build has its own undo,<leader>aSbu: every fileaSbwrites is journaled (path, hash of what was written, and where the file it replaced went in the trash); undo brings the originals back, deletes what it created and the folders it made, and never touches a file you changed since (the hash no longer matches). Only the last build of a project is kept (agent/kit/build/journal.lua).
.leader/: how Leader worked on this project (offline, yours)
A build that ends, an agent action and your decision about its proposal leave one line each in <project>/.leader/events.jsonl, so Leader can be measured and improved from real use: how builds end, which checks fail and how often, what a model costs per file, which actions you accept. Nothing is ever sent anywhere (no module of the folder touches the network or starts a process; a test checks it). It records how Leader works, never what the person is working on:
- Recorded: counts, sizes, durations, costs, models, outcomes; a file is a salted hash plus its extension and a size bucket (
<50,<200,<500,500+); a project's own script in a check iscustom. - Never recorded: code, documents (SPEC/STACK/DESIGN), prompts, model answers, file or folder names, type or function names, error or diagnostic text, instructions you typed, environment, name or machine.
- Enforced, not promised: every event goes through a schema (
agent/leaderdata/schema.lua). A field it does not name is dropped, a value of the wrong type is dropped, a word outside the allowed list becomesother, an id is 8 hex digits. The README the folder generates for itself is built from that same schema, so the list of what is recorded cannot drift from what is. A test builds a project full of marker strings (names, documents, code, answers) and checks none appears anywhere in the folder. - Files:
README.md(what is and is not recorded, how to read, switch off and delete it),events.jsonl,salt(random: the hashes are useless without it and it is never exported: shareevents.jsonlalone),.gitignore(*: never committed by accident). - When: the builder creates the folder; agent actions write to it only if it already exists, so a keypress never gives a project a folder.
- Read it:
<leader>aSlopens the report: builds and outcomes, cost per build, how many needed fix rounds, which checks fail most, files by kind and model, accept rate and cost per agent action. Switch off:settings.agent.leader_data.enabled = falseorLEADER_NO_DATA=1(the folder is then never created or written). Delete: remove the folder; nothing depends on it.max_kb(2048) caps the file: past it the oldest half goes.
Stable and experimental
The core is what is tested on every release and measured by the evals: fix, docs, refactor, review, explain, ask, implement, tests, the commit panel, the project documents. The rest still finds its shape and stays out of the menus until settings.agent.experimental = true (or <leader>Sp → Agent): DDD, repository, service and lens menus, the doctor, building a whole project (<leader>aSb, recipes) and multi-file plans. They keep working and say · experimental when shown. The list and the reason for each is agent/stability.lua; a feature moves by changing that table. scripts/gen-cheatsheet.sh shows everything (NVIM_EXPERIMENTAL=1).
Settings (settings.agent, overridable in local.lua)
backend, order, light_backend, history, cache, experimental, capability, cascade, trust, language (of explanations, answers and findings; English by default), rules, timeout_ms, tiers, context.*, apply.format (formats the range with conform on accept), keys (accept/reject), claude.models/model/light_model/prompt_cache/max_cost_usd, opencode.*, ollama.*, openai.*, custom.
Extending
| I want... | Do |
|---|---|
| another backend (another CLI) | lua/agent/backends/<n>.lua with the contract of backends/init.lua + NAMES; without code: an entry in agent.custom |
| another action | lua/agent/actions/<n>.lua with the contract of actions/init.lua + NAMES; the menu and the cheatsheet come out by themselves |
| an eval case | a folder evals/<action>/<name>/ with case.json, the files and cassettes/<action>/<name>/golden.json (rules: evals/README.md) |
| another kind of output | lua/agent/output/<n>.lua (done and optional stream/release) + NAMES |
| another context provider | lua/agent/context/providers/<n>.lua (cap, collect(target, done)) and use it in the context of an action |
The format the model must return (code block, L<n>|sev|msg) lives in lua/agent/protocol.lua next to its parser: the prompt rules and how they are read always change together.
Pitfalls
- Generated files (
*.g.dart,*.freezed.dart,*.gr.dart,*.mocks.dart, protobuf; list inutils/generated.lua) are rewritten on the next build: actions that edit (replaceoutput) refuse to run there, explain/ask still work. They are also left out oftypes, code search and the architecture levels' searches. - If the model disobeys and sends several code blocks (draft + corrected version, 2 out of 6 times in a test with Rust), the last one that starts like the target is used and the hint warns:
model sent 2 blocks, check the diff. - The accept/reject hint shortens in narrow windows (it drops tokens/cost first) and the spinner switches to a compact version.
- The colors of the diff, the spinner, the hint and the window are derived from the active theme's palette with guaranteed minimum contrast (4.5:1 for text) and are recomputed when the theme changes (
<leader>S); tested with the 97 base46 themes and the terminal-inherited theme. Findings use the diagnostic colors of your theme. - A visual selection is taken as whole lines.
Escrejects the proposal while one is visible in the buffer (only then;settings.agent.keyschanges it).- A model may return an empty block: it means deleting that range (e.g.
fixremoving unused imports). needs-import:after the block is shown in the hint; imports are not added by themselves.scripts/test-agent.shruns the tests for parsers, target, local backends and setup (no network or tokens: the Ollama/OpenAI servers are fake,scripts/fake-llm.py).
Fix: two levels
<leader>affix inline: put the cursor on the line with the diagnostic (the underline/block from LSP, linter or review). The target is only the diagnostic's lines (if it spans more thanagent.context.diagnostic_lines, only what starts there). It carries as context: the full diagnostic, theagent.context.aroundlines before and after within its function (<around>, read-only), the signature of what it calls (<callees>) and the definitions of the objects it points to: for each identifier in the diagnostic (and the object of anx.Fieldaccess) the LSP is asked for itstypeDefinition, so the model seestype Note structeven if the code does not name the type (n := load(); n.Titl). If the real fix lives elsewhere (an import, a missing declaration) it says so inneeds-import:/note:instead of editing outside the lines. In visual mode, the selection is the target.<leader>aFfix block: the target is the whole function (or the selection) and it fixes all its diagnostics at once, with the same type definitions. Use it when several diagnostics share a cause or the fix touches several lines.
Tests (<leader>aT) and lenses (<leader>aL)
Two menus of their own so <leader>a does not fill up. Both run through the same pipeline (instruction field, inline diff, ⏎/⎋, history and cost) and are designed to spend the minimum: whatever can be decided without a model is decided beforehand.
Tests (agent/tests/): they do not replace the function, they append to the end of the test file.
| Key | What it does | How it saves |
|---|---|---|
w write | tests for the function under the cursor (or the selection), only what is missing: main case, boundaries and each error branch, max. 6 cases, in the file's style | the target sent is one line (the end of the test file), the model returns only what is new; from the test file only the header, names, the tests that already cover that function and, if there are none, ONE example travel; a function of <4 lines is rejected before calling |
m mock | a hand-written fake for the interface/class under the cursor (records calls, returns what the test sets) | light model |
f fix | fixes a failing test | runs the test first (go test -run, vitest/jest -t, pytest -k, cargo test) and only if it fails sends its real output; if it passes, 0 tokens. If the code is at fault, it leaves the test as is and says so in note: |
g gaps | quickfix with the public functions in the folder that no test names | local, 0 tokens (approximate map: a short name can give a false "covered") |
- The test file comes from the language's convention (
agent/tests/convention.lua): Gox_test.go; TS/JSx.test.ts(or.spec.if the folder already uses it; vitest/jest/mocha according topackage.json); Pythontest_x.py(intests/if it exists); Luax_spec.lua; Rust, amod x_testsin the same file; Dartlib/→test/; Java/Kotlinsrc/main→src/test. - When done, the test file opens to the side with the proposal (
⏎accepts,⎋rejects); accepting only changes the buffer, you save. A new file is not created (nor its folder) until you save it. fixonly knows how to run Go, TS/JS (vitest/jest), Python and Rust; in another language it warns without spending anything.- Change the model:
agent.tiers.test_write = "light"(ortest_mock,test_fix).
Lenses (agent/lenses/): reviews with a single focus, which come out as diagnostics with their origin ([security]) on the exact line. af/aF fix them, and repeating a lens only replaces the findings of that lens (not those of another one or of review).
| Key | Looks for | Model |
|---|---|---|
s security | untrusted input reaching a sink (SQL/command/template injection, traversal, SSRF, XSS), authentication/authorization, secrets, cryptography, deserialization | main |
p performance | N+1, repeated work, quadratic algorithms, copies and allocations in hot paths, unbounded memory | light |
c concurrency | races and unsynchronized shared state, deadlocks, leaking goroutines/tasks, unawaited promises, double closes | main |
e errors | ignored or context-less errors, swallowed in catch, resources not released on the error path, half-done state | light |
a api | uncommitted changes that break whoever uses it (signatures, return, errors, nil, tags) | light |
- Each lens has a gate: if the code contains nothing of its topic (a function without threads for
c, without I/O fors, without changes fora) it warnsskipped (0 tokens)and does not call the model. - Minimal context per lens (rules and signatures of what it calls;
cadds types;aadds the diff): the output is ≤ 8 lines. - Adding a lens = one file in
agent/lenses/actions/(key,title,focus,gate) and its name inNAMES. - Change the model of one:
agent.tiers.lens_security = "light".
Features · docs/cheatsheets/herdr.md
Herdr
Herdr is the terminal workspace that runs the coding agents: every agent lives in a real pane with its shell, its logs and its process. Leader talks to it through its CLI, so you can see what every agent in the workspace is doing without leaving the editor, and Herdr can see what your nvim is doing.
Keys: CHEATSHEET.md (section Herdr). Nothing here costs tokens: it only reads.
The panel (<leader>Wa)
The breakdown of the workspace:
- One row per agent, grouped by workspace: its state (
● working,○ idle,! blocked,✓ done), the agent (opencode,claude,codex…) and where it is (~/Work/leader). - The pane on the right is the agent under the cursor: its own words (the transcript of its terminal) and why Herdr thinks it is in that state (the detection rule that matched, e.g.
progress_bar_working). - It refreshes on its own every
herdr.observe_ms(2 s), so the state you read is the state now.
| Key | Does |
|---|---|
j k | move |
Enter | read the agent under the cursor |
f | bring its pane to the front in Herdr |
s | send it a prompt (the text you type is submitted to it) |
x | the report of how Herdr detected its state |
r | refresh now |
q | close |
An agent that is working can only be read as it is drawn on screen (its scrollback is only captured when it goes idle), so the panel shows the visible screen then and the transcript once it settles.
Setup (<leader>Ws)
What Herdr is on this machine, and what to do about it:
- The binary: not installed? The setup offers to install it (
curl -fsSL https://herdr.dev/install.sh | sh, the PowerShell one on Windows) in a floating terminal. Nothing is installed without pressing the button. - The server: running or not, its version, its socket.
- This nvim: whether it is inside a Herdr pane, and whether it registers itself as an agent (see below).
- The integrations:
herdr integration install <agent>makes an agent (Claude, OpenCode, Codex…) report its own state to Herdr and come back after a server restart. The setup lists the ones that are missing or need repair.
Settings (<leader>Sp → Herdr)
| Setting | What it does |
|---|---|
| Herdr | auto (the binary decides), on, off |
| Register as an agent | auto (inside Herdr), on, off |
| Setup | the menu above |
In lua/local.lua:
return { herdr = { enabled = "auto", register = "auto", observe_ms = 2000, lines = 150 } }
The old knob still works: session = { herdr = true } (or false) in local.lua decides the registration when herdr.register is left at auto.
nvim as an agent of the workspace
When Leader registers itself in Herdr (register), the pane shows up in Herdr's Agents panel and says what it is doing: working while the inline agent runs (with the action's name), blocked while a proposal waits for you to accept it, and idle when nothing runs. It never takes the pane from a real agent that already occupies it.
With herdr >= 0.9.5 the registration also carries a resume command (nvim +HerdrResume, which restores the folder's session), so Herdr brings the pane back with your session after the server restarts. Older versions (0.9.x) do not know that option: the registration is made without it, so Herdr cannot bring the pane back — the panel says so.
Handing work to another agent
<leader>ag (the agent menu) sends file:range + your instruction to an agent that is already running in a Herdr pane, with the diagnostics of the range and the current code when the file has unsaved changes. It only appears inside Herdr. See agent.md.
Features · docs/cheatsheets/editing.md
Undo and clipboard
Undo and paste behave closer to VS Code without changing how Vim works underneath. Nothing runs in the background, and nothing is loaded until you use it. Keys: CHEATSHEET.md (sections History and Clipboard).
Undo word by word
In plain Vim, u undoes a whole insert at once: type a paragraph, press u, and all of it is gone. Here one insert becomes several undo steps. A new step starts:
- after each space or tab (one
u= one word), - after a new line (Enter, a snippet that adds lines, a paste),
- after a pause in typing (
settings.history.pause_ms, 1 s by default).
. still repeats the whole insert, and a macro's insert is still a single step. To get Vim's behaviour back, turn off Undo word by word in the settings panel (<leader>Sp) or set history.word_undo = false.
| Key | What it does |
|---|---|
u, Ctrl+Z | undo; the lines that changed glow for a moment |
Ctrl+R, U, Ctrl+Y | redo |
Ctrl+Z in insert mode | undo without leaving insert mode |
Ctrl+Z never suspends nvim. U replaces Vim's rarely used "undo line", and Ctrl+Y replaces "scroll one line up". history.redo_keys = false keeps Vim's meanings.
Going back in time
<leader>uuopens the undo tree (Neovim's built-in:Undotree) in a side panel. Moving the cursor in it moves the file through its history, branches included (what you undid and then overwrote is still there).qcloses it.<leader>u[/<leader>u]take the file to how it was at the previous / next save (:earlier 1f/:later 1f). This is VS Code's Timeline, and it is undoable too:<leader>u[thenugets your edits back.
Undo history is saved with the file (undofile), so all of this works after you restart nvim.
The clipboard keeps what you copied
With settings.clipboard.keep (on by default), only y (copy) and d (cut) fill the clipboard:
x,X,c,Cdelete without copying (changing a word does not lose what you copied);- pasting over a selection (
viwthenp) keeps the clipboard, so you can paste the same text again over the next one.
An explicit register still works as usual: "ax puts the character in register a. <leader>d deletes without copying with any motion. Turn it off with x / c keep the clipboard in the settings panel.
Yank history
Every copy and cut is kept, newest first, without duplicates. The list survives restarts (stdpath("state")/yanks.json).
- Right after
p/P,[yswaps what you pasted for the previous copy, and]ygoes back to a newer one. The count andp/Pare kept, and your registers are not touched. <leader>uylists them all with a preview. Enter pastes the copy after the cursor and makes it the clipboard again, so the nextprepeats it.<C-x>forgets one entry, and<leader>uxclears the whole list.
Settings: history.yanks.max (50 entries), max_chars (longer texts are not kept), persist (false = memory only).
Pitfalls
- WSL / SSH: text copied outside nvim. Copying from nvim reaches the system clipboard (OSC 52). Pasting from the system clipboard is Ctrl+Shift+V in the terminal, because the terminal does not let nvim read it (
ppastes nvim's own clipboard). [yonly works right after a paste. Once anything else changes the buffer it tells you to paste first.
Features · docs/cheatsheets/tasks.md
Project tasks
The targets of the project's Makefile in one picker, run in the integrated terminal. It asks make, so it costs 0 tokens and nothing stays running. Keys: CHEATSHEET.md (section Run).
It works with a Makefile and only with a Makefile. Without one, every key says so and does nothing, except <leader>Rc, which creates one.
Which Makefile
The nearest GNUmakefile, makefile or Makefile from the current file's folder up to the git root, never above it (the ~/Makefile of the machine is not your project's). In a monorepo, a Makefile in the subfolder wins over the root's.
What is a task
A target is listed when it is .PHONY or has a description. Descriptions come from the file's text:
.PHONY: build test
build: ## Build everything <- inline description
test: build
go test ./...
# Remove the build output <- a comment right above works too
clean:
rm -rf out
.PHONY targets declared in included files are found too (the list comes from make -qp, which prints make's database and runs nothing). Without GNU make (Windows, BSD) only the file's text is parsed. A Makefile with neither .PHONY nor descriptions lists every plain target.
Picker
| Key | Does |
|---|---|
Enter | run the target |
Ctrl-a | ask for arguments, then run (PKG=./x, -j8) |
Ctrl-p | dry run: make -n, prints what it would do |
Ctrl-r | read the Makefile again (the list is cached until it changes) |
The dot marks the default goal; <leader>Rd runs a bare make.
Create a Makefile (<leader>Rc)
Only where there is none. Pick a language (the one the project looks like comes first: go.mod, Cargo.toml, package.json, pubspec.yaml…) and then a preset, with the file as preview. It is written to the project root (the git root) and opened. It never overwrites a Makefile.
| Language | Presets |
|---|---|
| Go | basic, service (live reload, race, coverage, image), cli (cross-compile), library (bench, coverage report) |
| Python | venv, uv, fastapi, django |
| Rust | basic, full (watch, audit, coverage, bench) |
| TypeScript / Node | npm, pnpm, nextjs |
| Dart / Flutter | flutter, dart |
| Java | maven, gradle |
| Kotlin | gradle (ktlint, detekt) |
| C / C++ | c, cpp (sources in src/, objects in obj/), cmake |
| Docker | compose, image |
| Shell | basic (shellcheck, shfmt, bats) |
Every preset has a help target (make help prints each target and what it does) and descriptions on all of them, so the picker shows them at once.
Add your own: drop presets/<language>/<name>.mk in nvim/lua/tasks/ (a new folder is a new language). The first line is a # description comment (it stays in the created Makefile), every target has ## text, recipes use tabs. scripts/test-tasks.sh runs a real make over every preset.
Editing a Makefile
Makefiles get their own treesitter parser and checkmake (pack infra) for the usual mistakes (a target with no .PHONY, no all/clean/test); Neovim keeps real tabs in them. A target documented with ## text on its line is what the picker shows next to its name.
Good to know
- Targets run in the same terminal as
<leader>aoreads, so a failing build can go straight to the agent's "fix from output".<leader>Rqputs the file:line errors of that output in the quickfix for free. - If a target is still running, the next one asks before it is typed into it (
Stop it and run).<leader>Rssends Ctrl-C. <leader>Rlrepeats the last target of the project (it survives a restart);<leader>Raedits its arguments first.
Features · docs/cheatsheets/gitignore.md
.gitignore
Writes a .gitignore from what the project is made of. 0 tokens, nothing resident. Keys: CHEATSHEET.md (section Git, ignore rules).
What it looks at
Marker files and extensions under the git root (3 levels deep; settings.git.ignore), never inside dependency or build folders (node_modules/, vendor/, target/… count by name only). go.mod or *.go → Go, package.json or *.ts → Node, pubspec.yaml → Dart/Flutter, Dockerfile → Docker, *.tf → Terraform… Nothing is read from file contents.
The checklist opens with what it found checked and says why (found go.mod +14). Secrets (.env*, keys, certificates, credentials, password databases) and Editor and system (OS files, IDE state, logs) are recommended for every project; Secrets goes first in the file, Editor and system last. Space or x toggles, a toggles all, Enter writes.
With a .gitignore already there
It adds only what is missing and never removes or reorders anything: a rule is not written twice (not even across topics: dist/ is in Node, Python and Go but lands once), a block with nothing new disappears with its comment, and a topic the file already covers is listed as already in .gitignore, unchecked. Each row says how many new rules it would add. It refuses to run while .gitignore has unsaved changes in a buffer.
What git already tracks
Ignoring a file does not untrack it. After writing, it asks git which tracked files the rules now match (git ls-files -ci --exclude-standard) and warns, e.g. a .env that was committed long ago: git rm --cached <file> stops tracking it (the secret stays in the history: rotate it).
Topics
Plain files in lua/gitignore/presets/<name>.gitignore; drop one in and it is discovered:
# Go: compiled binaries, test and profiling output <- line 1: `Title: what it ignores`, written as the section header
# detect: go.mod go.work *.go <- line 2: what says it applies (not written)
# Binaries and build output <- rules in blocks with a comment each
/bin/
detect takes file names (go.mod), globs on a whole name (*.go, .env.*), folders with a slash (node_modules/) and always. Rules that people usually commit on purpose are left out or commented (Cargo.lock, pubspec.lock, google-services.json, .vscode/settings.json is kept); the tests check with git check-ignore that none of a long list of files a project needs is ignored by all topics together, and that secrets and junk are.
Tests: scripts/test-gitignore.sh.
Features · docs/cheatsheets/release.md
Release
The steps around a release that are always the same, inside a git repository. Changelog and checklist cost 0 tokens; the PR description is one light-tier request. Nothing is ever tagged, pushed, posted or published from here: those lines are shown for you to run. Keys: CHEATSHEET.md (section Git, release …).
Changelog (c)
Reads the commits since the last tag (all of them before the first tag), in Conventional Commits:
| Commits since the tag | Next version |
|---|---|
a breaking change (feat!:, refactor(api)!:, or BREAKING CHANGE: in the body) | major (minor before 1.0) |
a feat | minor |
| anything else | patch |
The current version is read from VERSION, then package.json, pubspec.yaml, Cargo.toml, pyproject.toml, then the last tag. The confirmation shows the section; Write puts it at the top of CHANGELOG.md (the file is created with a header when missing; a section of the same version is replaced, never duplicated). Groups: Breaking changes, Features, Fixes, Performance, Other changes (each commit once, in the first group it fits); scopes in bold, short hashes at the end. Release commits (chore(release): …) and merges are left out.
Then, by hand: bump the version file, commit, git tag vX.Y.Z, git push --tags.
PR description (p)
The branch against its base (origin/HEAD, else origin/main, origin/master, main, master): its commit subjects, the diff stat and the diff (cut at settings.git.release.pr_max_chars, 12000) go to the light model, which writes Summary · Changes · How to test · Risks from what they show, never inventing tickets or behaviour. The body opens in a split and is copied to the clipboard: paste it in the PR (gh pr create --body "$(…)" or the web form).
Checklist (l)
A buffer of what must be true before releasing, checked where it can be:
- Repository: working tree clean, ahead/behind the upstream.
- Quality gates: the commands in STACK.md's
## Quality gates(or the test/lint/format/build ones of## Commands);⏎runs one in the integrated terminal. A command that leaves the machine (push, deploy, publish, release, upload, tag) is listed "by hand" and never runs from here. - Version and changelog: whether CHANGELOG.md has the next version (
⏎writes it), what the version file says against the next version (⏎opens it), the tag line to run. - Shipping: the CI configuration found (GitHub, GitLab, Forgejo/Gitea, CircleCI) and STACK.md's Deploy / Release / Hosting / Distribution / CI rows.
r reads it again, q closes.
Pitfalls
- Subjects that are not Conventional Commits still appear, under Other changes, and never bump more than a patch: keep using the commit panel (
<leader>Gc), which validates them. - The changelog follows tags: tag every release, or the next changelog includes the previous one's commits.
Languages · docs/cheatsheets/go.md
Go
Tools
| What | With |
|---|---|
| LSP | gopls (gofumpt, staticcheck, placeholders, analyses: unusedparams, shadow, unusedwrite, useany; inlay hints) |
Format (on save and <leader>fm) | goimports then gofumpt |
| Lint | golangci-lint (in light mode: go vet); does not run when leaving insert |
| Extra | go.nvim (GoAddTag, GoFillStruct, GoIfErr, GoAddTest...), treesitter, gomodifytags |
<leader>g menu (only in *.go)
The keys are in CHEATSHEET.md (generated; not duplicated here).
- Struct actions (tags, fill literal,
if err, add test) act on the cursor. The tag ones (ga/gA/gD) save the file when done (that is how go.nvim works). - In light mode go.nvim does not load: those actions warn instead of failing.
runandbuildrun on the file's directory (.if there is ago.mod, or the single file if not). The rest run from the module root.
Project setup (<leader>gS, lua/gosetup/)
The config files a Go project usually wants, one key each (0 tokens, nothing resident): golangci-lint (.golangci.yml: minimal, standard, strict, v2 format), Air (.air.toml: basic, api with graceful restarts), Dockerfile (alpine or distroless, multi-stage, non-root), .dockerignore, GitHub Actions CI, git hooks (lefthook) and .editorconfig. Sp is a checklist of all of them (what is missing and core is checked), Sm creates a Makefile (same as <leader>Rc) and Sg the .gitignore (<leader>Gi).
- Filled in from the project, never asked: the module path (golangci's local import prefix), the Go version of
go.mod(image tags), wheremainreally is (./cmd/apior.) and the binary's name. The module is the nearestgo.modup from the current file. - Never overwrites. A file that is already there (also
.golangci.yaml/.toml/.jsonfor golangci-lint) is opened. A misnamed config such as.golangcli.yml(golangci-lint only reads.golangci.yml) is offered a rename with the buffers and servers following; the content is kept. - With several variants the picker previews the file exactly as it will be written.
- golangci-lint v2 configs (
version: "2"): a v1 binary refuses them, and the action warns when it finds one. - Add a preset: drop
presets/<topic>/<variant>.<ext>inlua/gosetup/. Line 1 is# description(stays in the file), line 2 is# target: path/relative/to/the/module(not written). Placeholders:{{module}}{{go}}{{main}}{{binary}}. A new topic (a folder) also needs its entry inTOPICS(gosetup/init.lua): key, label, default variant. - Tests:
scripts/test-gosetup.shrenders every preset and, where the tools exist, has them check it (golangci-lint v2config verify, air's own keys fromair init,lefthook validate, TOML/YAML parsing).
Struct tags while you type (utils/gotags.lua)
The tag value is derived from the field name: SentAt → sent_at, UserID → user_id, HTTPServer → http_server, Kind → kind.
| You type (in a struct field) | Suggests |
|---|---|
` after the type | json:"kind", json:"kind,omitempty", json:"sentAt" (camelCase if it differs), the gorm:"…" variants for that field and its type (see below), yaml, db, mapstructure, form, toml, xml, json:"-", validate:"required" | |
json: | "kind", "kind,omitempty", camelCase, "-" |
json:" | kind, kind,omitempty, camelCase, - |
json:"kind, | omitempty, omitzero, string (options change depending on the key) |
gorm:" | full values by field and type: primaryKey;type:text, type:text;not null… |
gorm:"type:text; | loose options you have not used yet: not null, index, size:255, column:title, default:…… |
type Envelope struct {
Kind Kind `json:"kind"`
ID string `json:"id,omitempty"`
SentAt time.Time `json:"sent_at"`
}
- Inside a tag the first item comes preselected:
Enteraccepts it. - The cursor ends up inside the quotes: type
,for options, orTabto leave. - Only keys you have not used yet in that tag are offered.
- It does not trigger in raw strings (
query := `SELECT…),return, assignments, etc. (treesitter checks that you are in a struct).
gorm
gorm separates its options with ; (not ,) and some take a value (size:255, column:sent_at). When you open the gorm:" tag, full values are offered, chosen by the field name and type; after a ;, loose options are offered (already used ones are not repeated, and the cursor lands on the value of size:, type:, column:… with Tab to jump).
| Field | Variants offered (in this order) |
|---|---|
ID string | primaryKey;type:text, primaryKey, primaryKey;type:uuid |
integer ID | primaryKey;autoIncrement, primaryKey |
string | type:text;not null, type:text, not null, type:text;not null;index, uniqueIndex, index, size:255 |
CreatedAt / UpdatedAt (time.Time) | autoCreateTime / autoUpdateTime, not null;auto…, not null, index |
other time.Time, gorm.DeletedAt | not null, index / index |
bool | not null;default:false, default:false, index |
| integers and floats | not null, not null;default:0, default:0, index |
[]byte | type:blob, type:bytea |
custom type or relation (Author User) | foreignKey:AuthorID, embedded, many2many:author |
| (all) | column:<name_in_snake>, - |
With a pointer (*string) not null is not offered. gorm tags show up in any .go file.
type NoteModel struct {
ID string `gorm:"primaryKey;type:text"`
Title string `gorm:"type:text;not null;index"`
Content string `gorm:"type:text"`
CreatedAt time.Time `gorm:"not null"`
}
Model files (*_model.go / model.go, utils/gomodel.lua)
Only there do functable, funcdomain, functodomain and funcfromdomain show up, at the top of the list (type funct…, funcd…, functo…, funcf…; also toDomain / fromDomain / TableName). They are generated from the struct fields and only for those that do not have the function yet.
- Domain by name:
NoteModel→note.Note(packagenote, typeNote). Fields are passed in order to the constructor and read with getters (n.Title()). - Real constructor: if the file imports
…/noteand the package is in the module, it looks there forReconstitute,Rehydrate,Restore,Hydrate,FromPersistenceorLoad. If none is found,Reconstitutestays as an editable placeholder (Tab). - Several models in the file:
functodomain NoteModel, andfromDomainbecomesfromNoteDomainto avoid a clash. - The domain package import is added by
goimportson save. Formatting (alignment ofID: n.ID(),) is fixed bygofumpt.
Contextual completion (utils/gosmart.lua)
Reads the treesitter tree to know where you are and offers only what applies. The documentation window shows the code that will be inserted.
When starting a declaration (column 0)
Type func, type, New… or the name of a struct. There, loose buffer words and the generic LSP func/type do not show up.
func — function variants:
| Option | Generates |
|---|---|
func | simple function |
func error | returns error (with return nil) |
func value | returns a T |
func pointer | returns a *T |
func bool | returns bool |
func result error | returns (T, error) |
func pointer error | returns (*T, error) |
func ctx error | takes ctx context.Context, returns error |
func ctx result error | takes ctx, returns (T, error) |
func main / func init | the special functions |
Methods per struct (for each struct in the file, e.g. Envelope):
| Option | Generates |
|---|---|
func (e *Envelope) | method with pointer receiver |
func (e *Envelope) error | method that returns error |
func (e Envelope) | method with value receiver |
func (e Envelope) String | fmt.Stringer |
functable | gorm: func (NoteModel) TableName() string with the derived name (NoteModel → notes, Category → categories). Shows up first: just type funct… (or func / TableName). With several structs: functable NoteModel, functable Category… Only in *_model.go or model.go files, and not for structs that already have it. |
funcdomain | toDomain + fromDomain at once (see below). Shows up with func or by typing funcd… |
functodomain | func (m NoteModel) toDomain() *note.Note { return note.Reconstitute(m.ID, m.Title, …) } |
funcfromdomain | func fromDomain(n *note.Note) NoteModel { return NoteModel{ID: n.ID(), …} } |
Constructor — NewEnvelope shows up for every struct without a NewX and already includes the fields:
func NewEnvelope(kind Kind, id string, sentAt time.Time) *Envelope {
return &Envelope{
Kind: kind,
ID: id,
SentAt: sentAt,
}
}
Parameters are in lowerCamel (UserID → userID, ID → id). Embedded fields are not included.
funcnew — constructor that returns (*X, error). It is optional: it does not show up in the list until you type funcn… (at package level). Tab/Enter insert it:
func New(id, title, content string) (*Note, error) {
| // ← cursor here: validations (max lengths, empties…)
now := time.Now().UTC()
return &Note{
id: id,
title: title,
content: content,
createdAt: now,
updatedAt: now,
}, nil
}
time.Time fields whose name starts with created, updated or modified are not parameters: they come from a single now := time.Now().UTC(). Any other time.Time (dueDate) or *time.Time is still a parameter. Without fields of that type, now is not generated.
Contiguous parameters of the same type are grouped. With a single struct in the file it is called New; with several, NewX (and several entries show up: funcnew Note, funcnew Other…).
Reconstitute — the persistence-layer constructor: it takes every field and sets it, with no validation and no now, because the data was valid when it was saved. Optional like funcnew: it shows up when you type recon… (or funcrecon…), 5 letters at least, at package level, once per struct declared in the current file (also inside type ( … ) blocks) that has no such function yet. Structs of other files of the package and generic structs are not offered:
// Reconstitute rehydrates a Note from persistence without validating it.
func Reconstitute(id, title, content string, createdAt, updatedAt time.Time) *Note {
return &Note{
id: id,
title: title,
content: content,
createdAt: createdAt,
updatedAt: updatedAt,
}
}
Same rules as funcnew for the rest: grouped parameters, lowerCamel names, embedded fields skipped, Reconstitute with one struct in the file and ReconstituteX with several. It is the name toDomain (functodomain, *_model.go) looks for in the domain package.
Getters — the accessors of an aggregate's private fields: no Get prefix, no setters (changes go through methods that mean something). For every unexported field of a struct declared in the current file that has no accessor yet:
func (n *Note) ID() string { return n.id }
func (n *Note) Title() string { return n.title }
func (n *Note) CreatedAt() time.Time { return n.createdAt }
func (n *Note) Tags() []string { return slices.Clone(n.tags) }
Two ways in: at package level type getters (or funcgetters, accessors; 5 letters at least) for all the missing ones of a struct at once, or write func (n *Note) and the menu (it opens by itself) lists one item per missing getter plus all getters. Details: the receiver is the one your other methods use (else the first letter of the struct, pointer); names follow Go's initialisms (id → ID, userId → UserID, url → URL); exported and embedded fields are skipped; a getter you already wrote is not repeated; the bodies come aligned like gofmt writes them. A slice or map is returned as a copy (slices.Clone / maps.Clone, the import comes with goimports on save), so the caller cannot change the aggregate's state through it. settings.go.getters.clone = false returns the field as it is. A pointer field is returned as it is.
package at the top of a file — gopls always offers package main next to package <folder>, and it used to rank first. Now the candidates are judged by where the file is (utils/gopackage.lua):
- the other
.gofiles of the folder decide: they saypackage notes→notesfirst and nomain; they saymain→mainfirst (_testfiles count as their package; files with//go:build ignoreand comments are not read as neighbours); - no neighbours:
mainleads only undercmd/…, in a file calledmain.go, or in the module root; anywhere else it is not offered (typingpackage m…still brings it, below the rest); X_teststays belowX, except in a_test.gofile, where both are real options.
Repository port — the persistence interface of a domain object, which is almost always the same. Only in the domain layer (a domain/, entities/, model/… folder, the names of settings.agent.ddd.layers.domain, read from the path under the module root) type repository (repo is enough) at package level and each aggregate of the package that has no port yet is offered:
// Repository is the persistence port of the Note aggregate; infrastructure implements it.
type Repository interface {
Save(ctx context.Context, note *Note) error
FindByID(ctx context.Context, id string) (*Note, error)
Delete(ctx context.Context, id string) error
List(ctx context.Context) ([]*Note, error)
}
- An aggregate is a struct with an identity: a field
id,IDor<Struct>ID; its type is the one in the signatures (string,uuid.UUID,int64,NoteID). Value objects (no id) and*Model,*DTO,*Request,*Response,*Row,*Entitystructs get nothing. A struct that already has its port is not offered again. - In a package named after the struct (
package note) the port isRepository(note.Repository); in any other,NoteRepository. - Nothing shows up outside the domain layer, nor in
*_model.gofiles: the port belongs to the domain. settings.go.repository.methodssets the methods and their order (Save,FindByID,Delete,List,Exists).- The adapter is the other half: in infrastructure,
func (r *noteRepo)offers the methods it is missing (goimpl) andvar _ note.Repository = (*noteRepo)(nil)comes from goport.
type — type variants:
| Option | Generates |
|---|---|
type struct | empty struct |
type struct constructor | struct + NewX |
type generic struct | Set[T comparable] struct |
type interface | empty interface |
type interface method | interface with one method |
type interface ctx result error | interface with (ctx, …) (T, error) |
type enum string | string type + constants |
type enum iota | int type + iota |
type defined | type Name string |
type func | type Handler func(…) error |
type error | custom error with Error() |
type options | functional options (WithX) |
Strict prefix matching (not fuzzy): only what starts with what you typed shows up.
| You type | Shows up |
|---|---|
f, fu, func, func | only the func variants, the methods and the pending NewX |
t, ty, type, type | only the type variants |
New, NewE, Env | only the NewEnvelope constructor |
func Ne | only NewEnvelope |
pac, package, i, v, c… | none of this (the rest comes from the LSP and friendly-snippets) |
type Foo, func Foo | nothing: you are naming something |
Missing methods to satisfy an interface (utils/goimpl.lua)
If a struct shows signs of implementing an interface and is missing methods, the menu offers exactly those, with the interface's signature and a useful body (0 tokens, no LLM). Two places:
- After typing the receiver:
func (s *noteService)opens the menu by itself (without typing anything) with…all missing (6)and each missing method (Create,Get, ...). Filter as you type (Se→SetDone). The receiver is used as you wrote it (s). - When starting the declaration (
funcat column 0): the same for each struct in the file, with thefunc (s *noteService)header already in place.
How it knows which interface it implements (the first that fits): var _ NoteService = (*noteService)(nil); a constructor that returns it (func NewNoteService(...) NoteService { return ¬eService{...} }); the name (noteService ~ NoteService, postgresNoteRepository ~ domain.NoteRepository, also if the interface is in another package of the module); or what it already implemented. What is already written is not offered again.
Body: if the struct holds a dependency that is an interface (repo domain.NoteRepository) and it has a method with the same shape (parameter and return types; the name does not matter), the body calls it: Get → return s.repo.FindByID(ctx, id), List → s.repo.FindAll(ctx, filter), Delete → s.repo.Delete(ctx, id). If there is no equivalent (Create, Update...) the cursor is left in the body with a return nil, nil (zero values) that compiles. With …all missing each empty body is a tabstop (Tab jumps).
Interfaces from another package of the module are read from disk (the go.mod module) and their types are qualified (Note → domain.Note); missing imports are added by goimports on save. Those from the standard library or from dependencies are not read: for those, gopls already offers <leader>ca → "Declare missing methods of …" on the constructor error (fills everything with panic("unimplemented")).
More helpers without an LLM (0 tokens, they read your code and your go.mod)
They show up in the same func / New… menu, above the rest. They share an index (utils/goindex.lua: interfaces, structs, methods, functions and constants of each file, package and the whole module, cached by date).
if err != nilthat knows what to return (utils/goiferr.lua): aftern, err := s.repo.FindByID(ctx, id)and Enter, the menu opens by itself with the block already built and Enter accepts it (turn off withvim.g.goiferr_preselect = false): the zero values come from the RETURNS of the function containing the cursor (return nil, fmt.Errorf("find by id: %w", err)), and the message from the name of what you called (editable;FindByID→ "find by id"). Second variant:return nil, err. Inmain→log.Fatalf. Inside afuncliteral it uses ITS returns. In a function that does not return an error it offers nothing.fmtis added by goimports on save.- Mappers between layers (
utils/gomap.lua): for each struct in the file ending inResponse/Request/DTO/Model/Entity/Record/Input/Output... it looks for its counterpart in the module (NoteResponse→Note;CreateNoteRequest→Note) and onfuncoffersfunc toNoteResponse(n *domain.Note) NoteResponse,toNoteResponses(list) andfunc (r NoteResponse) toNote() *domain.Note. Fields by name (ignoring case and_); if the source has the field private and a getter (ID()), it uses the getter; types defined over a basic type are converted (string(n.Status)/domain.Status(r.Status)); whatever does not fit or has no source comes out as// TODO(nothing made up).*_model.gofiles keep usingtoDomain/fromDomainfrom gomodel. - Wiring constructors (
utils/gowire.lua): in a function (typicallymain) typeNew…or the variable name (noteSer): it offersnoteService := application.NewNoteService(noteRepo)with the arguments already filled in, for ALL packages of the module (even if you have not imported them yet; goimports adds it). Each argument: a variable of the same type → a variable whose concrete type implements the interface the parameter asks for → a variable named like the parameter →context.Background()→ a hole. Variable types come from parameters,var x T,&T{}and the returns of constructors already called. - Enum helpers (
utils/goenum.lua): withtype Status string+ itsconst (...),funcoffersIsValid,ParseStatus,UnmarshalTextand, on numeric enums (iota),StringandMarshalText; plus "…enum helpers" with all of them. Only what is missing. - Interface from a struct and
var _(utils/goport.lua): insidetype NoteService interface { }it offers the exported methods ofnoteServiceit does not declare yet (with their signature); andvarat column 0 offersvar _ NoteService = (*noteService)(nil)for the structs whose interface is recognized (that line is what makes the compiler complain and what goimpl recognizes with certainty). - HTTP handlers from a service (
utils/gohttp.lua): in a structNoteHandler { svc application.NoteService },func (h *NoteHandler)(orfunc) offers one handler for each service method that is missing,Routesand "all". Framework bygo.mod: net/http (Go ≥ 1.22,PathValue), chi, gin or echo (fiber: nothing). Verb by name (Get/List → GET, Create → POST, Update → PUT, Set*/Patch* → PATCH, Delete → DELETE);id→ route parameter; a struct in a GET (filter ListFilter) → query string field by field (strconv.Atoi/ParseBool); the rest → JSON body (anonymous struct with snake_case tags);(T, error)→ 200 with the JSON (201 on POST), onlyerror→ 204. For net/http and chi, a separate item withwriteJSON/writeErrorif they do not exist.
After type Envelope
Offers struct, interface and func(…). If you already typed struct, nothing shows up.
Inside interface { }
Method signatures: method, method error, method value, method result error, method ctx error, method ctx result error, embed interface.
Struct name in value position → literal already filled
Type the name (x := Env) and Enter inserts the full struct, with no extra steps:
o := Envelope{
Kind: ,
ID: "",
SentAt: time.Time{},
}
Tab jumps from value to value (each value comes with its zero default selected). It applies when what you type is a value:
| Context | Example |
|---|---|
right side of := / = | o := Env, o = &Env |
return | return Env |
| field value | Outer{In: Env |
| call argument | process(Env, process(a, Env |
In type position it does not trigger and normal gopls completion continues: var e Env, parameters func(a Env, new(Env, []Env. Only for structs of the same package; the bare gopls/buffer name is hidden to avoid duplicating the item.
Inside a literal Envelope{ }
For structs of the same package (same file or another .go in the same folder with the same package). The menu opens by itself, without typing anything, at three moments: when typing { (leaves Envelope{|}), when accepting Envelope{} from the type menu, and when pressing Enter after { or after a field: value,:
| Option | Does |
|---|---|
fill all fields | inserts all missing fields, with default values; Tab jumps from value to value |
Kind, ID, SentAt… | a single field: Kind: █, with the cursor on the value |
- Fields you already put do not show up again; it repeats after each
field: value,. - Default values by type:
string→"", numbers →0,bool→false, pointers/slices/maps →nil,time.Time{},Inner{}(struct from the file). - Works with
Envelope{,&Envelope{and elements of[]Envelope{ {…} }. - With
Envelope{|}on a single line, it opens the lines for you: leavesEnvelope{/ fields /}. - A
package x_testdoes not see the structs ofpackage x(it does not compile without qualifying), so it does not offer them. - It does not trigger in positional literals (
T{a, b}) nor inside another field's value (e.g. call arguments). - Structs from other packages (
http.Client{): gopls completes it as usual. For those, use<leader>gs(GoFillStruct). - Buffer words do not show up here, and gopls fields are hidden to avoid duplicates.
Inside a function
Statement snippets show up (below). Declaration ones are hidden. Inside a struct none show up.
Declaring sentinel errors (goerrors)
Type only the error name: the rest appears by itself, in gray, and grows with each letter (utils/goerrors.lua).
ErrDescriptionTooLong| → ErrDescriptionTooLong = errors.New("description too long")
| Where | You type | Written |
|---|---|---|
inside var ( … ) | ErrNotFound | ErrNotFound = errors.New("not found") |
after var | ErrInvalidID | ErrInvalidID = errors.New("invalid id") |
| package level | ErrEmptyTitle | var ErrEmptyTitle = errors.New("empty title") |
| Key | Action |
|---|---|
Tab | accepts the gray text (cursor at end of line) |
Enter | accepts it and opens a new line (ideal to continue with the next error in the block) |
Esc | discards it |
- Splits the name by capitals and lowercases it (
TitleTooLong→title too long,HTTPError→http error). - Also accepts unexported names (
errNotFound). It needsErr+ capital letter (Error…does not trigger). - It only shows up with the cursor at the end of the line; while there is a suggestion the completion menu does not open.
- The
errorsimport is added by goimports on save. - It does not show up inside functions (sentinels are not declared there).
Statement snippets (nvim/snippets/go.json)
Full names: you find them by typing any part (wrap → errorsis…, ctx → the context ones).
| Snippet | Inserts |
|---|---|
iferr | if err != nil { return err } (err is left selected so you can change it; only inside functions) |
errorsis | if errors.Is(err, ErrX) { … } |
errorsas | var t *MyError; if errors.As(err, &t) { … } |
errorsentinel | var ErrX = errors.New("…") (package level only; message derived from the name: see above) |
contexttimeout | ctx, cancel := context.WithTimeout(…) + defer cancel() |
contextcancel | context.WithCancel + defer cancel() |
waitgroup | sync.WaitGroup over a range |
errgroup | errgroup.WithContext + g.Go + g.Wait |
mutex | mu.Lock() + defer mu.Unlock() |
once | once.Do(func() { … }) |
selectcontext | select with ctx.Done() |
forrangeindex | for i, v := range items |
forrangeint | for i := range n (Go 1.22+) |
testsubtest | t.Run("name", func(t *testing.T) { … }) |
testtable | table-driven test with subtests (package level only) |
testhelper | helper with t.Helper() (package level only) |
httphandlerjson | HTTP handler that responds with JSON (package level only) |
jsondecodebody | decode the body's JSON |
slog | slog.Info/Error/Warn/Debug(…) |
deferfunc | defer func() { … }() |
embed | //go:embed (package level only) |
The friendly-snippets ones that are not duplicated are also still available (if, ife, ir, for, forr, switch, sel, go, df, fp, ff, lp, make, tf, tdt, hand…). The friendly declaration ones (func, meth, tys, tyi, tyf, finit, fmain) are hidden at package level because gosmart replaces them with more variants.
Where everything lives
| File | Role |
|---|---|
lua/languages/go.lua | declaration: gopls, goimports, gofumpt, golangci-lint, govet |
lua/languages/actions/go.lua | <leader>g menu actions |
lua/gosetup/ | <leader>gS: presets (presets/<topic>/<variant>), vars.lua (module, Go version, main), init.lua (topics, flows) |
lua/plugins/languages/go.lua | go.nvim (tag_transform = "snakecase", tag_options = false) |
lua/servers/gopls.lua | gopls settings |
lua/utils/gotags.lua | blink source: struct tags |
lua/utils/gosmart.lua | blink source: func/type/methods/constructor/interface + filters |
lua/utils/goindex.lua | shared index (treesitter over text, per file/package/module) for the helpers below |
lua/utils/goiferr.lua · gomap.lua · gowire.lua · goenum.lua · goport.lua · gohttp.lua | if err with returns, mappers, wiring, enums, interface from a struct, HTTP handlers |
lua/utils/goimpl.lua | missing methods to satisfy an interface (receiver and declaration), bodies that call the dependency |
lua/utils/gomodel.lua | *_model.go-only shortcuts: functable, funcdomain, functodomain, funcfromdomain |
lua/utils/goerrors.lua | ghost text for sentinel errors (ErrX → ErrX = errors.New("x")); Tab/Enter are wired in blink.lua |
lua/plugins/editing/blink.lua | registers the sources and filters (buffer, lsp, snippets) |
snippets/go.json | statement snippets |
Notes
- If the treesitter tree comes out broken (unclosed brace), the filter prefers to show everything rather than hide something useful.
- To tweak or add
func/type/methodvariants:FUNCS,TYPESandMETHODStables inutils/gosmart.lua. - To add a statement snippet:
snippets/go.json, and if it only applies inside functions, add its name toFUNC_ONLYingosmart.lua.
Languages · docs/cheatsheets/dart-flutter.md
Dart / Flutter
Filetype: dart. The keys are in CHEATSHEET.md (generated; not duplicated here).
Tools
| What | With |
|---|---|
| LSP | dartls, managed by flutter-tools.nvim (not by vim.lsp.enable); not started in light mode |
Format (on save and <leader>fm) | dart_format |
| Treesitter | dart |
| Debugger | nvim-dap + nvim-dap-view (plugins/tools/dap.lua), loaded only when you debug |
| Code generation | dart run build_runner from lua/dartgen (watch in the background, state in the statusline) |
| Completion | utils/dartsmart.lua (declarations for the project's stack) + the LSP + the Flutter snippets of friendly-snippets |
Run, hot reload, debug
Four keys start an app (lua/dartrun):
| Key | Does |
|---|---|
<leader>dr | run on the device and entry point you used last, at once |
<leader>dd | the same, with the debug panel open from the start |
<leader>ds | the same in profile mode (performance: no debugger, <leader>dt opens DevTools) |
<leader>dD | asks the device, the entry point (lib/main_dev.dart…) and the mode (debug / profile / release) |
The device, the entry point and the last choice are remembered per project (stdpath("state")/dartrun.json), so after the first run r, d and s ask nothing; with a device missing they ask only for it (a single connected device is taken without asking). Devices come from flutter devices --machine (~0.5 s) and are sorted last used, phones, emulators, desktop, web; the ones the project has no folder for (web/, ios/…) are not offered. <leader>ds and <leader>dD while an app runs stop it and start the new one (r/d never interrupt it). The iOS simulator only runs debug (Flutter's own rule; Android emulators do run profile). Project configurations you declare yourself (require("flutter-tools").setup_project, with flavors or dart-define) win for r and d; a plain Dart package (no Flutter) is run by flutter-tools as before. With FVM, the project's SDK (.fvm/flutter_sdk) is used when it is installed.
Statusline. A chip follows the app: ◌ Pixel 7 while it builds, ▶ Pixel 7 running, ▶ Pixel 7 · profile (blue: it is not the build you debug), ⏸ Pixel 7 stopped at a breakpoint (yellow). It disappears when the session ends.
Quiet by default. A run puts nothing on the screen: the app's log goes to a hidden buffer (<leader>dl opens or closes it; settings.flutter.log = "split" opens it in a bottom split at every run, as before), the debug panel (nvim-dap-view) opens with <leader>dd or when the app stops at a breakpoint, and its errors still arrive as notes. <leader>dH hides whatever is open (log, debug panel, widget outline); with nothing open it shows the log.
Profile in practice. <leader>ds on a phone (or Linux/Chrome): hot reload keeps working, breakpoints do not. Open DevTools with <leader>dt for the Performance and CPU tabs. Profile on a simulator or an emulator measures the host's speed, not the device's: use a real device for numbers.
It runs through nvim-dap, so a breakpoint (<leader>db, <leader>dB with a condition) stops it in any run; <leader>dd does the same explicitly and opens the panel at once. Saving a .dart file hot-reloads the running app and saving pubspec.yaml runs pub get (flutter-tools). While stopped: <leader>dc continue, <leader>dn step over, <leader>di into, <leader>dO out; the panel (watches, scopes, breakpoints, threads, REPL) opens and closes with the session (<leader>du toggles it). The launch configuration is flutter-tools' own ("Launch flutter" on lib/main.dart, or the target of its project config): never empty dap.configurations.dart, or nothing launches.
Code generation (build_runner)
For packages with build_runner in pubspec.yaml (freezed, json_serializable, riverpod_generator, drift…):
<leader>dgstarts / stopsbuild_runner watchfor the file's package, in the background. The statusline showsgen …while it builds,genwhen it is up to date andgen ✗ Nwhen it failed.- A failed build puts its errors as diagnostics on the file and line they point at (source = the builder, e.g.
json_serializable) plus one note; the next good build clears them and saysCodegen fixed. <leader>dLshows the whole output;<leader>dGbuilds once.- Opening a file whose generated part (
part 'x.g.dart';,part 'x.freezed.dart';) does not exist says so once. - Light mode:
<leader>dgbuilds once instead of keeping a watch (a Dart VM stays resident).
Both output formats are understood (build_runner ≥ 2.5: Built with build_runner… / E builder on file:; older: [INFO] Succeeded after… / [SEVERE]). build_runner 2.16 removed --delete-conflicting-outputs: it is not passed.
Tests inside the editor
| Key | Runs |
|---|---|
<leader>dm | the test or group under the cursor (the whole file when the cursor is outside any) |
<leader>dM | the file |
<leader>dA | every test of the package |
<leader>d. | the last run again |
<leader>dT | jumps between lib/x.dart and test/x_test.dart, creating the test file (not saved) with its imports and a group |
<leader>dw | the raw output of the last run (a compile error that stopped it before any test, toolchain warnings) |
flutter test or dart test (whichever the package is) with the JSON reporter (lua/darttest). Each test line gets a mark as it finishes (✓ with its time, ✗, ○ skipped, … running) and the statusline counts test ✓12 ✗1 …. A failure becomes a diagnostic on the line its stack trace points at (the expect that failed) with the expected/actual text and whatever the test printed; the failures go to the quickfix list and one note sums the run up (Tests failed: 2 of 3 · 1 skipped · calc add fails). Results are kept per file, so a test file opened later shows them. A test is found by its literal name, groups included (--plain-name "calc add sums"): a name with interpolation ('renders $x') cannot be targeted, run its group or the file. Flutter compiles before the first test: a single widget test takes ~45 s the first time on a big app, and much less afterwards.
Dependencies
<leader>dU writes, next to each dependency of pubspec.yaml that is behind, what it can move to: ↑ 2.5.6 (green: pub upgrade takes it, the constraint allows it), latest 3.0.0 (change the constraint) (yellow) or discontinued / security advisory (red); saving pubspec.yaml clears them. <leader>dP adds packages (name, dev:name, name:^1.2.0, several at once) with pub add; its error, if any, is one note. Both ask pub.dev (network). <leader>dp is pub get (saving pubspec.yaml also runs it).
Translations
Saving an .arb file in a package with l10n.yaml runs flutter gen-l10n in the background, so AppLocalizations.of(context).newKey exists for the LSP at once; a mistake (an ICU syntax error, a missing placeholder) comes back as one note with gen-l10n's own message.
Completion for the stack
At the start of a top-level declaration (column 0) the completion offers what pubspec.yaml says the package uses, named after the file (snippet_filter_bar.dart → SnippetFilterBar, cart_provider.dart → cart):
| Item | Needs | Gives |
|---|---|---|
consumer, consumerstateful | flutter_riverpod / hooks_riverpod | ConsumerWidget, ConsumerStatefulWidget + its State |
riverpod, riverpodnotifier | riverpod_annotation | @riverpod function provider, @riverpod Notifier class |
freezed, freezedunion | freezed | @freezed abstract class (with fromJson when json_serializable is there), sealed union |
stateless, stateful | flutter | StatelessWidget, StatefulWidget + its State |
Accepting one also adds the import and part lines it needs and the file lacks (utils/dartheader.lua), so part 'x.g.dart'; is never forgotten. They are applied after the snippet (the source's execute), not as additionalTextEdits: blink applies those after expanding the snippet, and in a new file an insert at the typed line lands below the class.
The agent with Dart
<leader>at(convert) follows the stack: JSON → freezed classes (or@JsonSerializable()without freezed), a SQL table → a driftTable. The import andpartlines are added when you accept, and a snake_case key the model left without@JsonKey(name: …)gets it by rule (it would deserialize as null). Skipped whenbuild.yamlhasfield_rename.- After accepting a change, saving runs
dart analyzeon that file (machine format) and blames only the errors inside what was accepted. - "Fix from output" (
<leader>aAo/aAO) readsflutter analyzeanddart analyzeoutput. - Generated files (
*.g.dart,*.freezed.dart,*.gr.dart,*.mocks.dart, protobuf) are never edited by the agent (explain/ask still work there) and are left out of its context and of code search (utils/generated.lua).
Pitfalls
- Requires
pubspec.yaml: it detects whether the project is Flutter (flutter:block) or plain Dart and uses the right command (flutter test/dart test, etc.). - The Dart debug adapter pauses "on entry" and resumes by itself: calling
continuethere gets nvim-dap's "not focused on any stopped thread" question. - Neovim's snippet parser rejects nested placeholders (
${1:Future<${2:T}>}): keep one placeholder per type.
Languages · docs/cheatsheets/typescript.md
TypeScript / JS
Filetypes: typescript, typescriptreact, javascript, javascriptreact. Pack typescript.
Tools
| What | With |
|---|---|
| LSP | vtsls (TS/JS), eslint (where the project has an eslint config), biome (where it has a biome.json) |
| Format on save | biome where the project has a biome.json, prettier everywhere else |
| Lint | eslint_d |
| Extra | treesitter (javascript, typescript, tsx, jsdoc), nvim-ts-autotag, package-info (versions in package.json), Tailwind color preview |
The package manager comes from the lockfile: pnpm-lock.yaml → pnpm, yarn.lock → yarn, bun.lock(b) → bun, otherwise npm. The project's own binaries run with its exec (pnpm exec, yarn, bunx, npx --no-install): nothing is ever downloaded.
<leader>t menu (only in ts/tsx/js/jsx)
The keys are in CHEATSHEET.md. What they do:
- Scripts: a picker of the
package.jsonscripts,dev,build,lint(eslint when there is no script). - Typecheck:
tsc --noEmiton the whole project, its errors in the quickfix list (no terminal to read). - Tests in the editor: the runner comes from
package.json(vitest or jest in the dependencies or thetestscript, elsebun testin a bun project). The test under the cursor is theit/test(ordescribe) around it,.only/.skip/.eachincluded, selected by its full name; the file; the project; the last run again. Marks per test line, failures as diagnostics on the line the stack trace points at, the quickfix list. The file toggle jumps betweenx.tsandx.test.ts(orx.spec.tswhen that is the one there). - Code actions (vtsls): organize imports, add missing imports, fix all; go to the source definition (the
.tsfile instead of the.d.ts). - Packages: install, add (what you type:
-D vitest), remove, outdated. - Setup (
<leader>tS): tsconfig (strict Node/Bun, strict React with a bundler, a library that emits.d.ts), Biome (recommended or strict), ESLint flat config with typescript-eslint, Prettier, Vitest config (Node or React), a Dockerfile (Node multi-stage or Bun) and its.dockerignore, GitHub Actions (install, lint, typecheck, test, build with the project's package manager),.nvmrc,.editorconfig, a Makefile preset,.gitignore.
Snippets
describetest, ittest, zodschema (with its type), resulttype, asyncfunction, exhaustive (a switch the compiler checks); in TSX also component (typed props) and usehook.
Notes
- Biome and Prettier never both format: Biome only runs where a
biome.jsonis. npx --no-installfails when the tool is not innode_modules: install it in the project first.
Languages · docs/cheatsheets/python.md
Python
Filetype: python. Pack python (installer checklist, or <leader>Sp → Languages).
Tools
| What | With |
|---|---|
| Types, navigation, hover | basedpyright (type checking "standard"; a pyproject.toml/pyrightconfig.json setting wins) |
| Lint, quick fixes, imports | ruff (its server; in light mode it runs as a linter instead) |
| Format on save | ruff (organize imports, then format) |
| Debug | debugpy (Mason) |
| Treesitter | python, toml, requirements |
The project's interpreter is used everywhere: a .venv/ or venv/ at the root (uv, python -m venv, poetry with in-project venvs) is basedpyright's interpreter, so the imports you installed resolve. A uv project (one with uv.lock) runs through uv run; otherwise the .venv python; otherwise python3. e in the menu shows which one is in use.
<leader>p menu (only in *.py)
The keys are in CHEATSHEET.md. What they do:
- Run: the file, or a REPL with it loaded.
- Tests in the editor (the same keys in every language with tests): the test under the cursor (a
test_…function, a method of aTest…class, a wholeTest…class), the file, the project, the last run again. pytest runs with a JUnit report: each test line gets a mark (✓ passed, ✗ failed, ○ skipped, … running), a failure is a diagnostic on the line of the test file where the traceback stops, the failures go to the quickfix list. Parametrized cases count as their function. From a module (not a test), the key runs its test file (tests/test_<name>.py). The file toggle jumps between a module and its test (creatingtests/test_<name>.pywhen there is none). The output key shows what pytest printed. - Coverage:
pytest --cov(needspytest-cov). - uv: add packages (
--dev pytestworks: what you type goes afteruv add), remove, sync, upgrade the lock. - ruff: check the project, fix what it can, organize imports in the file.
- Debug (
<leader>pD): the file, or the test under the cursor through pytest, with the project's interpreter; breakpoints, continue, steps and the panel, as for Dart. - Setup (
<leader>pS): ruff config (standard or strict),pytest.ini,.python-version, a uv Dockerfile and its.dockerignore, GitHub Actions (uv: lockfile install, ruff, pytest), pre-commit hooks,.editorconfig, a Makefile preset,.gitignore. Filled frompyproject.toml(name,requires-python); never overwrites.
Snippets
testfunction, parametrize, fixture, dataclass (frozen, slots), ifmain, exception (with its data).
Pitfalls
- basedpyright needs the dependencies installed in the
.venvto resolve imports:uv syncfirst. - Tests from a class that does not start with
Testare not collected by pytest, so they get no mark either.
Languages · docs/cheatsheets/rust.md
Rust
Filetype: rust. Pack rust.
Tools
| What | With |
|---|---|
| LSP | rust-analyzer with clippy as its check (lints as diagnostics, not only compiler errors), proc macros, inlay hints |
| Format on save | rustfmt (from the toolchain: rustup component add rustfmt) |
| Debug | codelldb (Mason) |
| Treesitter | rust, toml |
<leader>r menu (only in *.rs)
The keys are in CHEATSHEET.md. What they do, at the crate root (Cargo.toml):
- cargo: run, build, check, clippy (all targets), clippy
--fix, add/remove crates (what you type:serde --features derive), update,doc --open. - Tests in the editor: the
#[test]function under the cursor (#[tokio::test],#[rstest]too) or themodaround it, by its path (calc::tests::adds,--exact), the file's module, the crate, the last run again. Integration tests (tests/x.rs) run as their own target. Marks per test, a failure is a diagnostic on the line it panicked at with the assertion's message, the quickfix list. (Rust keeps unit tests in the file, so there is no file toggle.) - rust-analyzer: expand the macro under the cursor (in a float), open the crate's
Cargo.toml, explain the error code of the line (rustc --explain E0382). - Debug (
<leader>rD): the crate's binary aftercargo build, or the test under the cursor (its test binary fromcargo test --no-run, run with--exact); breakpoints, steps, the panel. - Setup (
<leader>rS):rustfmt.toml,rust-toolchain.toml(stable + rustfmt, clippy, rust-src),clippy.toml,deny.toml(cargo-deny: advisories, licenses), a Dockerfile (Debian slim or distroless, dependencies cached) and its.dockerignore, GitHub Actions (fmt, clippy-D warnings, tests, cache),.editorconfig, a Makefile preset,.gitignore.
Snippets
testsmodule (#[cfg(test)] mod tests with use super::*), testfunction, errorenum (thiserror), implblock, tokiomain, derive.
Pitfalls
- The first rust-analyzer start indexes the crate and its dependencies: the first diagnostics take a moment.
- clippy as the check runs on save; on a very large workspace switch it back with a
.neoconf.json("rust-analyzer": { "check": { "command": "check" } }).
Languages · docs/cheatsheets/sql.md
SQL
Filetype: sql. Pack sql.
Tools
| What | With |
|---|---|
| Completion of tables and columns | sqls, connected to the project's database (below) |
| Format on save | sql-formatter in the project's dialect (PostgreSQL, or SQLite when the connection is SQLite) |
| Run | psql / sqlite3 (they must be installed): no resident process |
| Treesitter | sql |
Connections: found, never asked
In this order, without duplicates:
settings.sql.connections({ name = "local", url = "postgres://…" }or{ name = "data", sqlite = "data/app.db" }).- The project's
.env,.env.local,.env.development:DATABASE_URL, then any*_DATABASE_URL,*_DB_URL,POSTGRES_URL,DB_URL. DATABASE_URLof the environment Neovim was started in.- The SQLite files under the root (
*.db,*.sqlite,*.sqlite3; not innode_modules,.git,target,.venv).
The first is used; <leader>Qc picks another and it is remembered per project (state dir). Passwords are never shown (postgres://app:•••@localhost/app). A config.yml at the root (sqls' own) still wins for completion.
<leader>Q menu (only in *.sql)
The keys are in CHEATSHEET.md. What they do:
- Run the statement under the cursor (in visual mode: the selection), or the whole file. A statement ends at a
;outside strings, comments and dollar-quoted bodies ($$ … $$, as PostgreSQL functions are written). The result opens in one split (sql://results): the connection, how long it took, the rows as the client draws them (psql's table, sqlite3's boxes;∅for NULL);rthere runs it again,qcloses. - Asks before it hurts: a DELETE or UPDATE with no WHERE, DROP TABLE/SCHEMA, TRUNCATE, ALTER TABLE … DROP (
settings.sql.confirm_risky). - Explain: the plan of the statement (
EXPLAIN, SQLite'sEXPLAIN QUERY PLAN); explain analyze runs it, so anything but a read asks first. - Schema: the tables (
\dt+,.tables), the table under the cursor described (\d+,.schema).
Snippets
createtable (identity id, timestamps), createindex, upsert (on conflict … do update), withquery, transaction.
Pitfalls
- psql reads
~/.pgpassandPG*variables as usual: a URL without a password works when those have it. - A long query stops after
settings.sql.timeout_ms(30 s).
Languages · docs/cheatsheets/swift.md
Swift
Filetype: swift. Pack swift. For packages, CLIs and servers on Linux/WSL or macOS; Xcode projects are not covered.
Tools
| What | With |
|---|---|
| LSP | sourcekit-lsp (from the toolchain; enabled only when it is on PATH) |
| Format on save | swift format (Swift 6 toolchains) |
| Lint | swiftlint when it is installed |
| Debug | lldb-dap (from the toolchain) |
| Treesitter | swift |
Nothing is downloaded for Swift: everything comes with the toolchain (https://swift.org/install).
<leader>i menu (only in *.swift)
The keys are in CHEATSHEET.md. What they do, at the package root (Package.swift):
- SPM: run, build (debug, release), format the whole package, swiftlint, package update/resolve/clean, open
Package.swift. - Tests in the editor: XCTest (
func test…()in anXCTestCase, filterCalcTests/testAdd) and Swift Testing (@Testfunctions,@Suitetypes); the file runs its test types. Marks per test, failures as diagnostics on the line of the assertion (XCTAssertEqual failed…,Expectation failed…), the quickfix list. The file toggle jumps betweenSources/<Target>/X.swiftandTests/<Target>Tests/XTests.swift. - Debug (
<leader>iD): the package's executable afterswift build. - Setup (
<leader>iS):.swift-format,.swiftlint.yml, a server Dockerfile (static stdlib, slim runtime) and its.dockerignore, GitHub Actions on Linux (theswift:<tools version>image),.editorconfig, a Makefile preset,.gitignore.
Snippets
xctestcase, swifttest, swiftsuite, errorenum.
Languages · docs/cheatsheets/java-kotlin.md
Java / Kotlin
Filetypes: java, kotlin. They share the same actions (Gradle or Maven).
Tools
| What | With |
|---|---|
| Treesitter | java, kotlin |
| LSP / formatter / linter | none declared in the registry; the menus run the build tasks |
For jdtls / kotlin-language-server, add them inlua/languages/java.luaandkotlin.lua(lsp,mason).
Menus: <leader>j (Java) and <leader>k (Kotlin)
The keys are in CHEATSHEET.md (generated; not duplicated here).
It detects the project by gradlew, build.gradle(.kts), settings.gradle(.kts) or pom.xml. It uses the wrappers (./gradlew, ./mvnw) if they exist; otherwise gradle / mvn. If there is no Gradle/Maven project, it warns instead of failing.
| Action | Gradle | Maven |
|---|---|---|
| run | run | spring-boot:run if the pom uses Spring Boot; otherwise compile exec:java |
| build | build | package |
| test | test | test |
| check / verify | check | verify |
| clean | clean | clean |
Languages · docs/cheatsheets/lua.md
Lua
Filetype: lua.
Tools
| What | With |
|---|---|
| LSP | lua_ls (NvChad enables it) |
Format (on save and <leader>fm) | stylua (always installed, also in light mode) |
| Treesitter | lua, vim, vimdoc |
| Extra | lazydev: Neovim API types and completion of require() / plugin modules |
Completion
In Lua the blink sources are: lazydev, LSP, path, snippets, buffer (lazydev with priority).
Useful commands
| Command | Action |
|---|---|
:luafile % | run the current file |
:lua =expr | evaluate and print an expression |
:Lazy sync | install/update plugins (the git pull auto-reload does not do it) |
Auto-reload
After git pull, the .githooks/post-merge hook leaves a marker and, when nvim opens (UIEnter), core/autocmds/reload.lua runs luafile $MYVIMRC. Enable it once per clone:
git config core.hooksPath .githooks
It reloads Lua, but does not install/remove plugins or treesitter parsers.
Languages · docs/cheatsheets/markdown.md
Markdown
Filetype: markdown.
Tools
| What | With |
|---|---|
Format (on save and <leader>fm) | markdownlint-cli2 (fixes rules) and then prettier |
| Lint | markdownlint |
| Render | render-markdown.nvim (styled headings, lists, code, checkboxes) |
| Treesitter | markdown |
<leader>m menu (only in *.md)
The keys are in CHEATSHEET.md (generated; not duplicated here).
- The diagnostics toggle hides the linter's diagnostics only in this buffer (the linter keeps running).
- With spell checking on (es, en):
]s/[sjump between errors andz=suggests corrections (native Vim).
Languages · docs/cheatsheets/web.md
Web
HTML
| What | With |
|---|---|
| LSP | html and emmet_ls (expands Emmet abbreviations: ul>li*3 + completion) |
Format (on save and <leader>fm) | prettier |
| Extra | nvim-ts-autotag: typing <div> closes </div> and renames its pair |
CSS / SCSS / Less
| What | With |
|---|---|
| LSP | cssls |
| Format | prettier |
| Color preview | nvim-highlight-colors (hex, rgb, Tailwind) |
<leader>s menu (only in css, scss, less)
The keys are in CHEATSHEET.md (generated; not duplicated here).
Tailwind
tailwindcssserver (cross-cutting: it has no filetype of its own, it applies wherever the project uses Tailwind).- Class completion, and Tailwind color preview in the buffer.
Astro
| What | With |
|---|---|
| LSP | astro-language-server |
| Format | prettier — requires prettier-plugin-astro in the project (node_modules) or installed globally; Mason only installs prettier |
Languages · docs/cheatsheets/data-formats.md
Data formats
They have no leader menu; they rely on LSP, format on save (<leader>fm) and linters.
| Language | LSP | Format | Lint | Notes |
|---|---|---|---|---|
| JSON | jsonls + SchemaStore (automatic schemas for package.json, tsconfig, etc.) | prettier | jsonlint | |
| YAML | — | prettier | — | treesitter only |
| TOML | — | prettier | — | treesitter only |
| Dockerfile | dockerls and docker_compose_language_service | — | — | lazydocker available via TermExec |
| Prisma | prismals | via the LSP (lsp_format = "fallback") | — | no formatter in conform |
YarnSpinner (*.yarn) | — | — | ysc (optional) | see below |
YarnSpinner
ftdetect/yarnspinner.vim:*.yarn→ filetypeyarnspinner.syntax/yarnspinner.vim: highlighting for dialogue scripts.- The
ysclinter is optional: it is only enabled if it is on the PATH (dotnet tool install -g yarn-spinner, requiresdotnet). Error format:path(lnum,col): severity CODE: message.
Internals · AGENTS.md
Architecture
Neovim + NvChad v2.5 config — not an application. No CI or build system; the only automatic checks are scripts/gen-cheatsheet.sh --check and scripts/test-agent.sh (agent parsers and target, no network).
Repo structure
install.sh/install.ps1— ONE-COMMAND installer (Linux/WSL/macOS · Windows;curl … | bashbootstraps by fetching the repo into~/.local/share/leader, non-interactive by default,--askprompts,--agentdefaults toauto, plugins viaLazy! restore, per-step timings): dependencies, Neovim ≥ 0.11,nvim/link, hooks, plugins, Mason and parsers;--lang a,bleaves only those languages (writeslanguages.disabledtolua/local.lua, without overwriting an existing one). Per-machine options:--lang,--theme,--light/--no-light(written tolua/local.luabyscripts/install-local.lua, ONE implementation shared byinstall.shandinstall.ps1, never overwriting an existinglocal.lua) and--agent auto|backend/model|none(usesscripts/agent-setup.sh; skipped with--no-plugins). Usesscripts/install-mason.lua,scripts/install-treesitter.luaandinstall-local.lua; tests inscripts/test-install.sh;scripts/test.shruns all the suites.VERSIONis the config version (bumped withchore(release)+ tagvX.Y.Z)nvim/— mirrors%LOCALAPPDATA%\nvimon Windows. Symlink it (or use the installer):New-Item -ItemType Junction -Path "$env:LOCALAPPDATA\nvim" -Target "C:\path\to\leader\nvim".agents/skills/caveman*— OpenCode skills (lockfile:skills-lock.json)CHEATSHEET.md— GENERATED, don't edit: all the keys (leader menus, LSP, completion, nvim-tree, globals) read from the real keymaps byscripts/gen-cheatsheet.sh; the.githooks/pre-commithook regenerates it if the commit touchesnvim/.scripts/gen-cheatsheet.sh --checkfails if it is stale. It is the ONLY source of keys: don't copy key tables into the docsdocs/cheatsheets/— only what is NOT derived from code (what each thing does, workflows, tools, pitfalls):README.md+ one page per language (go.mdis the most detailed). When changing a language's menus, snippets or tools, update its page; no key tables (those come from the code)scripts/—gen-cheatsheet.sh(+.lua): cheatsheet generator (headless nvim with the real config, ignoreslocal.luaviaNVIM_NO_LOCAL=1)scripts/test-agent.sh(+test-agent.lua,test-agent-backends.lua,fake-llm.py): inline agent tests (protocol, parsers, diff, target, local backends and setup against fake Ollama/OpenAI servers); no network or tokensscripts/agent-setup.sh: setup of local backends and models from the terminal (report,--start,--pull,--auto,--use,--json).gitignore— excludes.claude/(machine-specific Claude config)app/— the desktop app (leader, Rust workspace;app/install.sh, or./install.sh --app). See "Desktop app" below
How it works
- Plugins auto-install via
lazy.nvimon first open - Activate the auto-reload hook once per clone:
git config core.hooksPath .githooks(the hook picks the right data dir for Windows or Linux) - LSP servers and tools (gopls, vtsls, stylua, prettier, eslint_d, dart, etc.) auto-install via
mason-tool-installer - No manual setup beyond the symlink (or
./install.sh, which also leaves plugins, tools and parsers already installed). A plugin with a heavybuildthat depends on a language must carryenabledaccording tolanguages.disabled(seeplugins/languages/go.lua), otherwise--langwould install its tools anyway - Auto-reload:
.githooks/post-mergecreates a marker ongit pull;core/autocmds/reload.luadetects it onUIEnterand runsluafile $MYVIMRCto reload config automatically
Architecture
One file = one responsibility. Minimal entry point in nvim/init.lua; everything else is modular:
nvim/
├── init.lua # bootstrap only (lazy + core)
└── lua/
├── core/ # startup: settings, light, lazy, options, clipboard, theme
│ ├── settings.lua # ALL configurable settings (+ local overrides)
│ ├── keymaps/ # cleanup (removes NvChad's) · general
│ └── autocmds/ # reload · treesitter · ui
├── languages/ # 1 file per language: DECLARES treesitter, lsp, mason, formatters, linters, menu
│ ├── init.lua # registry: aggregates the declarations and serves them to each consumer
│ └── actions/ # menu actions (go, dart, typescript, python, rust, jvm...)
├── appearance/ # default theme vs terminal-inherited (Alacritty → base46)
├── themes/ # base46 user themes (terminal.lua is generated from the palette)
├── servers/ # per-LSP-server overrides (servers/<name>.lua) + loader
├── agent/ # inline agent (<leader>a): backends/ actions/ context/ output/ ui/ handoff/ (see below)
├── herdr/ # the workspace of agents (<leader>W): cli/ init/ panel/ report/ setup/ (see below)
├── menus/ # contextual leader menus (buffer-local) + Git context
├── plugins/<group>/ # 1 lazy spec per plugin: core lsp tools syntax languages navigation editing git ui
├── ui/ # start/ (start screen) + dashboard.lua (its buttons), arts/ (braille art catalog), highlights, lualine, flat/ (the flat design)
├── utils/ # pure helpers: terminal, project, actions, git, log, telescope
└── chadrc.lua # only composes: theme + dashboard + NvChad ui
Adding a language: create lua/languages/<name>.lua (contract in languages/init.lua) and add it to NAMES. There is no need to touch mason, treesitter, conform, nvim-lint or LSP: they come from the registry. Overrides for a server: lua/servers/<server>.lua.
Adding a plugin: a file in lua/plugins/<group>/. New groups: add them to GROUPS in core/lazy.lua.
Adding a dashboard art: create lua/ui/arts/<name>.lua (returns the list of lines; braille-dot art only, U+2800–U+28FF, no ASCII/blocks). It is discovered automatically: ui/arts/init.lua scans the folder, with no list to maintain. It is chosen from the dashboard's "Change Art" button (ca); the choice is saved in stdpath("state")/dashboard.json. Initial value: settings.dashboard.art.
File operations (lua/files/, <leader>F; a/r/d inside nvim-tree use the same modals instead of nvim-tree's prompts, <leader>nd/nx are gone): acts on the row under the cursor in the tree, else on the current buffer's file (target.lua). New: one input, ; separates several, a trailing / makes a folder, the first file opens (from the tree, the tree closes first like <CR>). Rename: pre-filled name, a / in it moves the file. Move: a picker of the project's folders (ops.folders: depth 5, skips .git/node_modules/vendor/target/dist/build/.venv, capped) plus "new folder…". Duplicate: name_copy.ext, _copy2… Delete: confirm modal (folder: contents count; unsaved open files warned) then ops.trash moves it to stdpath("state")/trash/<stamp>-<name> with an index.json (30 days) and <leader>Fu restores the last one, refusing to overwrite. ops never overwrites and falls back to copy+delete across file systems (EXDEV). After a move buffers.renamed renames the SAME buffers: nvim_buf_set_name leaves a ghost buffer with the old name (deleted by its previous name, not the folder's), and the next :w says "File exists" unless the buffer is reloaded from the new place (edit!), so unsaved lines are saved before and put back after; LSP servers get workspace/willRenameFiles (edits applied before) and didRenameFiles. Tests: scripts/test-files.sh.
Settings panel (lua/prefs/',<leader>S):<leader>Spis one flat list (a telescope dropdown, the same widget as the agent model selector) of what is worth changing without editing a file. A group is a file inprefs/items/returning items (group,label,kindtoggle/choice/action,id= path incore.settings,applyfor the live effect,restart; contract at the top ofprefs/init.lua; new group = file +NAMES). Enter flips a toggle, steps through up to 3 values or opens a picker for more, and runs an action; the list reopens on the row you changed. A choice is written tocore.settingsin place (so every module that reads settings where it is used sees it at once) and saved byprefs/store.luainstdpath("state")/prefs.jsonas a flatpath → valuemap thatcore/settings.luamerges AFTERlocal.lua(and ignores underNVIM_NO_LOCAL, which keeps the cheatsheet machine-independent). A choice whosevalueis nil (light modeauto) removes the saved key. Items with their own persistence (diagnostic blocks,utils/diagblocks.luaget/set) have noidand read/write through their owner. Whatever is read once at startup (flat design, session autosave, light mode) is markedrestartand says so;conformreadsformat_on_savethrough a function so<leader>Sfis live.prefs/info.lua(purelines(facts)) andprefs/update.lua(git pull --ff-onlyon the clone, thenLazy restore; never merges). Tests:scripts/test-prefs.sh`.
Terminal-inherited theme (<leader>S): reads alacritty.toml (Linux, Windows or WSL under /mnt/*/Users/*/AppData), resolves its imports and generates a base46 theme (lua/themes/terminal.lua) with the terminal's exact background, interpolated grays and accents with minimum contrast (theme.terminal.min_contrast; never dimmed toward the background, comments = the terminal's bright black). The flat design is FAITHFUL to it (palette.from(..., { faithful = true })): the editor stays on the terminal's background (deep = bg, so the rebase is a no-op and Neovim and the terminal are one surface) and Visual is the terminal's own selection color; only floats/panels (soft) step away from it. It is remembered across sessions (state in stdpath("state")/appearance.json + a marker next to the base46 cache) and followed LIVE: while the active theme is the terminal one, a poll (theme.terminal.watch_ms, 1000 ms; 0 = off) stats the config files and recompiles when it detects a change (inotify doesn't work: on WSL the config lives on /mnt/c). With the default theme nothing runs. It also re-syncs when nvim opens and when focus returns. The compiled marker is SHARED by every open Neovim (it sits next to the shared base46 cache), so live updates (appearance.refresh, poll and FocusGained) compare against the signature THIS instance applied, never the marker: otherwise the first instance to recompile leaves all the others painted with the old palette. sync (marker) is only for startup. Startup cost: the marker also stores the parsed palette, the config path and its theme file; while the recorded files are untouched (alacritty.unchanged: only stat, memoized for 500 ms) initial, sync, refresh and alacritty.load use it instead of searching /mnt/*/Users/* and parsing TOML (on WSL that was ~60 ms per start and ~40 ms per focus return; now the terminal theme starts as fast as the default one). Custom path: theme.terminal.config.
Sessions: persistence.nvim saves on exit and core/autocmds/session.lua also saves in the background every session.autosave_ms (only if something changed and without closing your auxiliary windows), so a power cut doesn't lose the state; autosave_ms = 0 disables autosave. The session menus are in the Gotchas below.
Herdr (lua/herdr/, <leader>W menu, doc in docs/cheatsheets/herdr.md): Herdr is the terminal workspace that runs the coding agents; Leader talks to its CLI (herdr agent|workspace|pane|integration …, JSON over the local socket). One folder per piece: cli.lua (the ONLY place that knows the binary, the JSON and the timeouts), init.lua (the facade: agents/workspaces/pane/read/explain/focus/prompt/status/version, plus the registration of nvim as an agent), panel.lua (the breakdown: one row per agent grouped by workspace, its state, its transcript and WHY Herdr says so — utils/market.lua; it refreshes itself every herdr.observe_ms), report.lua (what THIS nvim is doing, told to Herdr: working while the agent runs, blocked while a proposal waits, idle otherwise — it hangs off agent/events.lua, the flow never knows), setup.lua (what herdr is on this machine and how to install/configure it). Settings in settings.herdr: enabled (auto = the binary decides), register (auto = inside Herdr), observe_ms, lines; the panel in prefs/items/herdr.lua. Pitfalls measured: herdr agent read REFUSES the scrollback of an agent that is working (agent_not_idle: "use --source visible"), so the panel falls back to the visible screen; the JSON error's message is what is shown (not the whole object); and the resume command of pane report-agent (the -- RESUME_COMMAND) only exists from 0.9.5 — 0.9.1 answers "unknown option: --", so the registration is tried with it and, if it is refused, again without it (the version is only a hint: a wrong guess never loses the registration).
Inline agent (lua/agent/, <leader>a menu, doc in docs/cheatsheets/agent.md): ONE-shot actions (implement, review, explain, ask, fix, document, refactor, hand off to Herdr) on the function under the cursor or the selection, with no chat or sidebar. One folder, one contract per piece: backends/<cli>.lua (build/parser; claude, opencode, ollama, openai (LM Studio/llama.cpp/vLLM) and command for CLIs declared in agent.custom), actions/<action>.lua (target, context, prompt, output), context/ (treesitter target + budgeted providers: rules, types the target uses (treesitter + LSP, any language), LSP signatures, diagnostics with their line, outline), output/<kind>.lua (presenters: replace = inline diff with accept/reject, findings = diagnostics, text = window with streaming), ui/ (spinner, preview, window, picker). Adding a backend/action/presenter/provider = one file + its NAMES list; the menu comes from the action registry. protocol.lua keeps the output format demanded of the model TOGETHER with its parser. Settings in settings.agent; the backend+model chosen with <leader>am are saved in stdpath("state")/agent.json. Local models and setup (backends/{http,ollama,openai,command}.lua, setup/): setup/detect.lua measures CLIs, servers (incl. the Windows host from WSL) and GPU/RAM in parallel without touching anything; recommend.lua (pure) proposes models by VRAM; actions.lua is the only thing that modifies (start ollama serve, /api/pull, remember endpoints, set backend/model); open it with <leader>am → Setup or scripts/agent-setup.sh. Ollama sizes num_ctx per request (otherwise it truncates silently) and light_backend leaves docs/explain on a local model without moving the main one. Everything loads lazily (the agent facade only on first use; lualine looks at package.loaded). Measured pitfalls: claude -p by default drags ~46k input tokens, with --setting-sources "" --tools "" --system-prompt ~600 (don't use --bare: it disables the OAuth login); the CLIs run in stdpath("cache")/agent (without the repo's AGENTS.md); opencode falls back to the default agent (with tools) if it can't find the requested one, opencode latency (measured, opencode-go/deepseek-v4.1-flash, one-line task): cold opencode run ~7-9 s and ~20k input tokens because the global MCP servers' schemas ride along (agent.opencode.mcp=false disables them by name, ~5 s and ~830 tokens); a private opencode serve + HTTP POST /session/:id/message (backends/opencode_server.lua, session created ahead, basic-auth password) is ~2 s; run --attach (v1.19) emits no text, so it is not used; the 2.x CLI (measured, 2.0.31) is detected by v2() in the same file and changes the shape (agents/system/permissions, variant in -m model#variant, run --standalone because the background service never sees the inline config, no --pure; warm server off until its v2 HTTP API is driven, ~3 s per action, and its JSON stream carries no usage events); vim.system{cwd=} does not update $PWD and opencode resolves its project from it, so runner.lua sets PWD (otherwise the repo's AGENTS.md/CLAUDE.md, ~10k tokens, leaked into every prompt); opencode run hangs waiting on an inherited stdin when called by hand without </dev/null; and its route could not be tested with network in the development environment (parser and command were, with synthetic events and scripts/test-agent.sh). .agent.md at the project root adds conventions to every prompt. DDD (lua/agent/ddd/, <leader>aD menu, a separate feature): local static analysis of layers, imports, naming and aggregates (port/implementation/wiring/use cases) that costs 0 tokens (diagnose: diagnostics, quickfix and panel), and LLM actions (implement, review, missing, ask) with their own registry (ddd/actions) that reuse the same pipeline and receive the <ddd> section (provider context/providers/ddd.lua). Only what is inside layer directories is read; settings in agent.ddd. Hexagonal architecture levels: one folder and one menu per level — agent/ddd/ (<leader>aD), agent/repository/ (<leader>aP: port, implement, review, check), agent/service/ (<leader>aU, use cases: implement, review, deps) —, each with its own action registry (same contract and pipeline as agent/actions) and a local 0-token analysis; what is shared (rg, type reading) lives in agent/discovery.lua; <leader>a is left for generic things. Project documents (agent/kit/, <leader>aS, not a level: it writes files, not code): AGENTS.md (model reads snapshot.collect: tree, languages, manifests, Makefile targets, scripts, README; A = 0-token template.lua), STACK.md (stack presets per category + principle presets appended by compose.set_principles), DESIGN.md (design-system presets, or described with a named preset as <base>), SPEC.md (free text in draft.lua → requirements/acceptance criteria/open questions); sections fixed in compose.DOCS and checked on every preset by scripts/test-kit.sh; replace = confirm + files.ops.trash; new documents are linked from AGENTS.md "Read first"; c makes CLAUDE.md import @AGENTS.md. Build with agent (agent/kit/build/, <leader>aSb + b all, t foundation, s domain, d design): phases ready → plan → review → write → check → fix → use → look → done. Scopes (scope.lua, one table per scope: docs it reads/requires, plan rules, file-request line, existing/wiring/use flags): a layer (foundation from STACK.md, domain from SPEC.md, design from DESIGN.md) builds on the project's files (scope.existing, git's list minus documents/binaries/generated/dot folders and the scope's own previous files; <project> in the plan request; seeded into written so every file request sees their API, <current> for an existing file the plan changes); a need on an existing file is not a plan problem (plan.parse(text, existing, scope)); a changed existing file is existed (never in created(): never remembered, stale-trashed or cleaned); run and file list are saved per scope (state.path(root, scope), manifest_path(root, scope), "all" keeps the old names); foundation/domain skip the wiring check and the use phase. Tests: scripts/test-kit-scope.lua. Task scopes feature and recipe carry run.task ({id, title, text}); their plan/file/ask are functions of it (scope.rules(name, task)), they are additive (no file list, nothing stale, nothing of the project skipped), one saved run per scope (another task starts afresh). Feature (kit/feature.lua, aSbf): picker of SPEC.md's FR-n (coverage marks) or a new one (draft → prompts.feature JSON → pure feature.add writes it in SPEC.md's own style: heading/list, next id, edge cases, Qn questions; confirm; old SPEC.md to the trash); the use phase reads only the requirement (use_docs). Recipes (kit/recipes.lua, aSr, also in the library): presets/recipes/<group>/*.md with <!-- input: key | question --> lines and {{key}}/_snake/_camel/_pascal/_kebab placeholders; for: supports a+b and a+!b; tested: every recipe parses, fits a real stack preset, fills every placeholder. SPEC.md modules (kit/specmodules.lua, aSm, also in the library; aSm opens the full-screen marketplace kit/specmarket.lua (on utils/market.lua, which kit/addonmarket.lua — the customize step of aSt/aSd/aSk/aSK — shares; narrow terminals show one pane at a time; t/d open the STACK.md/DESIGN.md add-ons a module goes with; ★ = SPEC.md already mentions it): three floats (list, reading pane, strip of what SPEC.md gets), Space pick, ⏎ read and leave requirements out, p the text that would be added, A add after the usual confirmation that returns to it when cancelled; plan/restrict/added_lines are pure): presets/spec/<group>/*.md in SPEC.md's shape without ids (### Title, statement, Given/When/Then criteria; - [Title] … edge cases/questions; <!-- needs: … -->); pure apply gives each requirement the next FR-n through feature.add (document style), extras through feature.add_extras, skips a title SPEC.md has (extras go to that FR), marks <!-- module: key --> (idempotent; feature.block ends at such a line), resolve puts needs first; never removes. Tests: scripts/test-kit-specmodules.lua (every module: criteria shape, tags, needs, questions). Coverage (kit/coverage.lua, aSv, 0 tokens, pure checks + collect): FR-n ↔ tests (test text names the id, or saved plans' verifies), STACK names missing from the manifest, unused DESIGN color tokens (every spelling), folders no document mentions; ⏎ on an untested FR opens its feature build. Records (0 tokens, hooked into the build by record()): kit/adr.lua writes the plan's new decisions as docs/adr/NNNN-*.md + index when a review is accepted (and adr.summary sends every decision still in force to the next plan requests as <decisions> with plan.DECISIONS_RULE; a plan that reverses one writes Supersedes ADR NNNN: …, recorded in the new ADR's Status and as superseded by in the index, and the old one leaves the summary), kit/progress.lua rewrites PROGRESS.md (Now/Done/Open/Next, Done kept as a log) when a run settles; both linked from AGENTS.md via compose.link_file, excluded from scope.keep. snapshot.root() inside the build tab is the run's root. Tests: scripts/test-kit-records.lua, scripts/test-kit-feature.lua. Release kit (lua/release/, <leader>GR c/p/l): changelog.lua (pure: parse git log records, semver bump, grouped section, insert at the top replacing the same version, version from VERSION/package.json/pubspec/Cargo/pyproject), checklist.lua (pure: STACK.md quality gates (only local commands run: runnable), version/changelog, CI, shipping rows), init.lua (git reads, the flows; the PR description is one light request copied to the clipboard; never tags/pushes/posts). Tests: scripts/test-release.sh. Quality harness, each piece one file: clean.lua (pure: a model's answer as a file — fences with prose, echoed <file> tags, leaked tool-call markup such as DeepSeek's |DSML| and leading chatter go; problem() says what is still not a file and the request is asked again ONCE, prompt.again; a broken fix keeps the old file), wiring.lua (pure, 0 tokens: names the plan's exports promise that no other non-test file mentions; methods must appear as .Name (a style called Expression must not hide Calculator.Expression()), lists like Title, Muted styles split, framework hooks (Init/Update/View/String/Error, bubbles' ShortHelp/FullHelp…) excluded; behaviours fail the check, values only feed the use verdict), the plan's wiring rows (FR → user action → code → what they see) go into every file's cached system prompt, and use/ (the app driven as its user: launch.lua recognizes the kind from the manifest and code, never from the model; scenario.lua writes scenarios ONCE per build from SPEC/DESIGN — expectations never from the code — and every round replays them; drivers/ tui (private tmux server -L, -f own conf, the app under sh -c '…; printf "[exited with %s]"; sleep' because a dead pane loses its output; keys ONE at a time: a burst reaches Bubble Tea as one KeyMsg that no binding matches), cli, web (+ Electron; web_runner.cjs with the project's or the global Playwright, the server started detached and killed as a group), android (adb + uiautomator, needs a device you started); judge.lua free checks per frame + one verdict request whose free failures it can never overrule; async.lua runs a driver as a coroutine so the editor never blocks). Goldens are trashed before any use/look fix and looked at again (look_rounds, use_rounds). New driver = one file with available/prepare/play/finish + its line in use.DRIVERS. Fixes are edit blocks (edits.lua, pure: SEARCH/REPLACE applied line-wise, trailing spaces ignored, ambiguous or missing SEARCH refused; shrunk catches a fragment taken for the file; the one retry after failed blocks asks for the whole file). Every model JSON goes through clean.json (first { to last }, then %b{}: Lua's %b{} alone cuts at a } inside a string; vim.json.decode rejects an explicit nil opts); invalid scenarios or verdict JSON is asked again once. Free checks: verify.unhandled_actions (data-action|key|op|command… names, words of 3+ letters, that no script mentions), verify.drop_versions (invented Go pseudo-versions → go mod edit -droprequire), plan.generated (goldens, lockfiles, build output are never planned); not_see shorter than 3 characters is ignored (a keypad shows every digit). The verdict reads the wiring code (scenario.controls) and only the rows that changed per step (judge.delta); fix_round resets per repair cycle and live.user carries the use findings into the next diagnoses. Measured on calc-lab (opencode deepseek-v4.1-flash): see docs/cheatsheets/agent.md. Tests: scripts/test-kit-use.lua (run by scripts/test-kit.sh; tmux and Playwright parts run for real when installed). Tests (agent/tests/, <leader>aT: write/mock/fix + local gaps) and lenses (agent/lenses/, <leader>aL: security/performance/concurrency/errors/api, they come out as diagnostics with their origin) are two more levels with their own registry and menu. The pipeline recognizes action hooks: retarget (the result goes to ANOTHER file; the target becomes the end of the test file and the original is the subject the context comes from, with job.view so the spinner shows on the tested code), merge (the model returns only what is new, protocol.APPEND_RULES), reveal (opens the destination to the side) and prepare (prior work WITHOUT a model, e.g. running the test: if it passes, 0 tokens); lenses use gate (words: with none, they don't call the model). The findings of a lens only replace those with the same source. Adding a level = folder + registry + menus/agent_<level>.lua + line in menus/init.lua. Adapters (agent/repository/contracts/, providers contracts and schema, Repository-level actions in <leader>aP (port, implement, review, check)): if the target is an adapter (e.g. PostgresOrderRepository) the following are injected, with their path: the port it implements (by name or implements/impl X for/var _ X, searched with rg), the missing methods, the entity the port names, a sibling implementation as a style example and the real table (SQL/Prisma migrations) with dialect/driver/parameters; silent if the code is not an adapter; scope type = the type under the cursor or its receiver's. Persistent findings (agent/persist.lua + core/autocmds/agent.lua): those from review/ddd review/ddd diagnose are saved in stdpath("state")/agent-findings.json with their line's text and restored (re-anchored) when the file is opened; fix and ddd fix read them via the findings provider and, on accept, the resolved ones are removed. <leader>ax stop does NOT delete them; they are cleared one at a time with <leader>ac clear (cursor line or selection), with <leader>aC (file) or with :AgentClear [file|all]. Two models per backend (tiers main/light; docs and explain go to the light one, <leader>am → Light model); i/d/f open the inline instruction field (empty Enter = no instruction), a/R require it. Short menu: i d r e a f R g(handoff, Herdr only) h(history) m x l (plus c/C clear). Two-level fix: f = inline (target = the lines of the diagnostic under the cursor, scope diagnostic in context/target.lua; it carries <around> and, via the LSP's typeDefinition on the diagnostic's identifiers, the types they point to) and F = block (the whole function, same types). runs/ saves every run in stdpath("state")/agent-history.jsonl (prompt, response, tokens, cost, what you did with it) and <leader>ah shows it with a spend report; claude -p runs with DISABLE_PROMPT_CACHING=1 (the cache only charged writes: -40 % per call). The colors in agent/ui/highlights.lua are derived from the theme palette (no linking to Diff*/Comment) and recomputed on User NvThemeReload. Advanced menu: the batch features are not in the everyday <leader>a; actions flagged advanced = true (fix all a, fix from output o/O, plan p) and the proposal queue (] [ y l, labels from batch/queue.lua) are bound under <leader>aA by menus/agent_advanced.lua. A new batch-style action sets the flag and picks a key unused in THAT menu.
Agentic layer (lua/agent/: events, guards/, budget, verify/, refine, batch/, fixall/, errors/, review/, plan/; doc in docs/cheatsheets/agent.md): features that hang off the inline agent's pipeline WITHOUT editing the flow they hang from (the open/closed rule of this folder). events.lua is a tiny pub/sub (proposal.accepting/accepted/dropped, job.settled; handlers run in pcall), features.lua is the composition root that wires listeners (verify) and is called when the pipeline loads. pipeline.run(action) resolves the target from the cursor and asks the instruction, then calls pipeline.launch(spec) (action, target, subject, selection, input, extra sections, meta, preapproved): every other feature launches through launch with a target it already knows. action.standalone = true skips target resolution and calls action.handler({ selection, action }) (fix all, fix from output, plan: all in the Advanced menu). guards/init.lua runs after the prompt is built and BEFORE the request: check(req) per guard (budget is the first; add one = file + NAMES) returns nil, block (notification, job denied) or confirm (one modal with every reason, utils.modal.confirm with opts.on_cancel so a "no" releases the job: runner.deny records nothing and emits job.settled denied). budget.lua is PURE (tokens ≈ chars / 3.5, price per token learned from the history of that model, caps day_usd/session_usd block, confirm_tokens/confirm_usd/warn_ratio ask); runs.call records one-shot calls (commit draft, staged review, plan: output = "oneshot") so the spend is complete, and runs/advice.lua (pure) feeds the report's Budget/Suggestions. Answer cache (cache/: pure core.lua = key/eligible/prune, init.lua = file in stdpath("state")/agent-cache.json): pipeline.launch asks cache.lookup(job) right after the prompt is built and BEFORE the guards; the key is the sha256 of backend+model+effort+system+prompt, so any change to code, context, diagnostics or instruction misses; a hit goes through runner.settle (the same path a model answer takes, 0 tokens, stats.line says cached) but is NOT recorded in runs/: it is not a model call, so history and spend report stay about real runs, and the cache keeps its own counters (cache.stats(): hits, tokens and cost saved, shown by <leader>aAk), a success stores in runner. Only text/findings outputs by default (settings.agent.cache.outputs, action.cache = true|false): a replace proposal is never reused. meta.fresh skips the lookup. Tests MUST NOT touch the real cache (K.fake() points it at a temp file), <leader>aAk shows its stats and clears it. Keys a hint advertises must work where the cursor is: the answer window (ui/float.lua) never takes focus, so float.enable_followup() binds the follow-up key on the code buffer while the answer is shown (released on close, WinClosed or cursor move; replace.rebind gives it back to a waiting proposal, and replace.unbind only deletes mappings whose desc says proposal). float.dismiss() is what the global <Esc> (core/keymaps/general.lua, after the diagnostic popup) calls to close the answer from the code (and to stop a call still streaming). Test: scripts/test-agent-keys.lua. Next edit (actions/next.lua, edits.lua, provider edits, scope window in context/target.lua, wired by core/autocmds/agent.lua: BufEnter baseline, InsertLeave/TextChanged record): edits.hunks/render are pure; check stops before any request when the ring is empty. Docs output is diff-only: actions/docs.lua sets merge = docplace.merge and asks for protocol.DOC_RULES (ONLY the comment); docplace.lua is pure (comment above the function replacing the old one, attributes stay between; Python's docstring goes into the body, replacing the old one; a model that retypes the whole function anyway wins as is). refactor stays whole-function on purpose: returning line ranges would make anchoring errors cost more than the output tokens saved. Code search is NOT part of the agent (lua/codesearch/, <leader>fs) but the agent can reuse its index: scopes.symbols(text, lang) returns { name, kind, first, last } for functions and types (an arrow function takes the name it is assigned to); index.build(root, cb) is incremental. Verify (verify/): accepting remembers the buffer's non-agent diagnostics (namespaces of findings and ddd excluded), accepted waits for the server to settle (settle.lua: no DiagnosticChanged for settle_ms, never past max_wait_ms; nothing heard = no verdict, no "verified" line), regressions.lua (pure) compares them as a multiset by source+code+message (not line), retry.lua turns the new ones into a batch of verify_fix (derived from fix_block; the attempt counter travels in job.meta.attempt), build.lua arms a one-shot BufWritePost that builds (go build, cargo check, py_compile or verify.command) because a compiler reads the disk, and blames only errors inside the accepted range ±2 (extmark anchors follow later edits). Refine (refine.lua): Tab on a proposal derives <action>_refine (task + "start from <previous_proposal>", instruction appended as priority), supersedes the old proposal (replace.supersede, its buffer never changed; proposal.raw is the model output BEFORE merge) and launches with the SAME target (anchor.range refreshed); Tab on an answer asks ask with a <conversation> section (meta.thread, oldest turns dropped past 4k chars). Batch (batch/init.lua): items {label, target, extra (table or fn(done_items)), input, diags}; ONE confirmation with the total (budget.check on overhead + targets; a cap blocks, confirm = false skips the question), items run sequentially each launched with preapproved = true and an extmark anchor re-read at launch (the range may have moved), progress in the action label (fix all 2/3, only when several) and in job.meta.hint (shown by the replace presenter), denied/cancelled abort the rest; batch/queue.lua moves through replace.pending() (step, accept_all via replace.accept_proposal(p, { keep_cursor }), list = quickfix). Its keys (<leader>aA], [, y, l) live in menus/agent_advanced.lua. Fix all groups LSP/linter/findings/DDD diagnostics by enclosing function, else type, else adjacent lines (fixall/group.lua, pure, bottom of the file first). Errors (errors/): parse.lua runs the output through getqflist({ lines, efm }) once per tool format and keeps the format that explains most lines (go, tsc, cargo, python, javac, generic; no buffers are left behind), source.lua reads the last terminal buffer, else the quickfix, <leader>aAO runs a command (tests/run.exec(..., { raw = true })); each batch piece gets only ITS errors as <build_errors>. Review staged (review/): diff.lua (pure) numbers the staged diff by new-file line, static.lua (pure) finds secrets/conflict markers/leftovers/sensitive paths in added lines (Lua patterns have no {n}: repeated classes), init.lua sends ONE numbered request only if real code remains, drops findings that do not point at a shown line, and places them with findings.put (open buffers) or persist.merge (the rest). Plans (plan/): locate.lua (references, completed with git grep when the server never leaves the starting file; collapse and rank pure) → prompt.lua (signatures only; validate drops invented files/symbols) → ui.lua (interactive panel through ui/panel opts.on_open/opts.hint) → steps.lua (each step is a plan_step piece whose context carries <plan> with THIS STEP marked and <earlier_steps> = what the previous steps proposed) → batch; snapshot.lua is git stash create. Tests: scripts/test-agent-{flow,batch,review,plan}.lua on scripts/agent-kit.lua (fake backend, temp project, canned modal answers, K.no_lsp() stubs vim.lsp.start: a real gopls would fire diagnostic events at random moments). Pitfalls met: modal.confirm's y must mark the answer before closing or WinClosed reads it as a "no"; commit-panel agent line chunks are trimmed from the END (fit(chunks, width, 3)), so what must stay visible is inserted at index 3; a timer callback can be queued twice before the first one closes it (ui/flat/fresh.lua checks is_closing).
Project tasks (lua/tasks/, <leader>R, menu menus/tasks.lua, doc in docs/cheatsheets/tasks.md): the targets of the project's Makefile in ONE picker, 0 tokens, nothing resident. It works with a Makefile and ONLY with a Makefile: every action (r l d a p o q s) calls tasks.need(dir) first, which says "no Makefile above … (<leader>Rc creates one from a preset)" and does nothing; there are no other runners on purpose (just, task, mise and package.json scripts were removed). make.lua finds the Makefile (GNUmakefile > makefile > Makefile, nearest at or above the current file's folder, never above the git root: util.stop, so the machine's ~/Makefile is not the project's) and lists its targets. Make cannot list its tasks, so two sources are put together: make -qp (database, runs nothing, exit 1 on purpose) gives the .PHONY targets after includes and .DEFAULT_GOAL (only trusted when the output says "GNU Make"), the Makefile's TEXT gives descriptions (target: ## text, or a comment right above); without GNU make the text alone is parsed. A task is a .PHONY target or a described one (with neither, every plain target). Pure parsers: parse_db, parse_file, build. init.lua caches per Makefile (mtime+size of it and its *.mk, <C-r> in the picker forces), sorts (default goal first, then the file's order) and owns the flows (pick, last, default = a bare make, ask, dry = make -n, quickfix, create). Targets run in the integrated terminal through utils/terminal.lua (exec.lua: pure line, busy = the shell has children, stop = Ctrl-C; running while another task is alive asks first, otherwise the line would be typed into the running process), so <leader>ao reads their output; <leader>Rq turns it into the quickfix with the agent's own errorformat parser. store.lua keeps the last target per project (and the last anywhere) as argv+cwd+arguments in stdpath("state")/tasks.json. Presets (presets/<language>/<preset>.mk, create.lua, <leader>Rc): plain Makefiles, one folder per language (go, python, rust, typescript, dart, java, kotlin, c, docker, shell; a folder dropped in is discovered, its label is in presets.LABELS). The first line is a # description comment (it stays in the created file), every target carries ## text (so the picker lists it) and every preset has a help target; recipe lines MUST use tabs. Creating asks language (the one the project looks like first: presets.guess from marker files, else the filetype) then preset with the file as preview, writes <git root>/Makefile and opens it; it never overwrites and does nothing when a Makefile exists above. scripts/test-tasks.sh runs a REAL make over every preset (make -qp must read it, .PHONY must include help, every listed target needs a description). settings.tasks.timeout_ms. Tests: scripts/test-tasks.sh.
.gitignore (lua/gitignore/, <leader>Gi, doc in docs/cheatsheets/gitignore.md): writes a .gitignore from what the project looks like, 0 tokens. Topics are plain files presets/<name>.gitignore (presets/init.lua: line 1 # Title: what it ignores is the section header, line 2 # detect: names globs folders/ always is not written; secrets first, base last). detect.lua (scan: names under the root, settings.git.ignore.depth/max, dependency/build folders counted but not walked; pure match/run), compose.lua (PURE: build adds only the rules existing lacks, never twice across topics, blocks with nothing new vanish with their comment; missing, rules), init.lua (checklist utils/modal.checklist with the detected topics checked and why; write never overwrites, appends; afterwards git ls-files -ci --exclude-standard warns about tracked files the rules now match). Rules that people commit on purpose stay out (no Cargo.lock, data/, secrets.* as a glob, .claude/); scripts/test-gitignore.sh checks with real git check-ignore that nothing a project needs is ignored by all topics together. utils/modal.lua got checklist (rows to toggle, Space/x/a, Enter) and an internal render so a modal can redraw.
Go project setup (lua/gosetup/, <leader>gS inside the Go menu, doc in docs/cheatsheets/go.md): config files for a Go project, one key each: golangci-lint v2 (minimal/standard/strict), Air (basic/api), Dockerfile (alpine/distroless), .dockerignore, GitHub Actions, lefthook, .editorconfig; Sp = checklist of all, Sm Makefile (tasks preset), Sg .gitignore. Presets are plain files presets/<topic>/<variant>.<ext> (line 1 # description stays, line 2 # target: path is not written); vars.lua fills {{module}} {{go}} {{main}} {{binary}} from go.mod and the real main package (pure module/go_version/main_dir/binary, collect reads the disk). init.lua owns TOPICS (key, label, default variant, alts that count as present, misnamed names the tool ignores: .golangcli.yml → confirm modal → rename through files.ops + files.buffers), never overwrites (an existing file is opened), previews the rendered file in the variant picker (tasks.picker.choose, files rendered into stdpath("cache")/gosetup-preview), and hints after creating (golangci v1 vs the v2 config, air/lefthook missing). The menu context got an optional groups = { S = "setup" } (which-key names of sub-prefixes, menus/init.lua). scripts/test-gosetup.sh has the real tools verify the presets (golangci-lint v2 config verify, air's keys vs air init, lefthook validate; skipped when absent).
Dart / Flutter (doc in docs/cheatsheets/dart-flutter.md): flutter-tools owns dartls and run/hot reload; in light mode plugins/languages/flutter.lua replaces flutter-tools.lsp.attach (its ftplugin starts dartls on every Dart buffer through it) so the commands stay and the server does not. Debugging is nvim-dap + nvim-dap-view (plugins/tools/dap.lua, lazy: flutter-tools requires dap), debugger.run_via_dap = true so breakpoints work in any run; never set debugger.register_configurations to something that empties dap.configurations.dart ("No launch configuration"). Code generation (lua/dartgen/): parse.lua is pure (build_runner ≥ 2.5 Starting build #N / Built with… / Failed to build… / E builder on file: + indented message and package:x/y.dart:L:C, and the old [INFO]/[SEVERE]), init.lua runs dart run build_runner watch|build per package (vim.system, stopped with SIGINT), errors → diagnostics in namespace dartgen on their file, statusline component components.dartgen_status (DartgenStatus/DartgenFailed), the missing-part note on BufReadPost; light mode builds once. Completion (utils/dartsmart.lua, blink source for dart): declarations for the stack read from pubspec.yaml (utils/dartheader.lua stack), named after the file; the import/part lines (dartheader.edits, shared with <leader>at) are applied in Source:execute after the snippet, never as additionalTextEdits (blink applies those after expanding it, and in a new file they land below the class). Neovim's snippet parser rejects nested placeholders. Generated files (utils/generated.lua): one list (*.g.dart, *.freezed.dart, protobuf…) used by the agent's types, code search, agent/discovery.lua and the pipeline (editing actions refuse there). Tests (lua/darttest/): parse.lua is pure over the JSON reporter (testStart/error/print/testDone/done; testWidgets reports flutter_test's own line, its place is root_line/root_url; the failing line comes from the stack trace frame in the test file; the loading … pseudo-test and non-JSON lines are skipped). init.lua finds the test under the cursor with treesitter (Dart has no call nodes: identifier + selector(argument_part); names joined with spaces = what --plain-name matches; interpolated names cannot be targeted), keeps results per file (marks in darttest.marks, diagnostics in darttest, sorted), quickfix and one note; a new run kills the running one. Run (lua/dartrun/): r/d/s/D all go through start(opts) (debug, mode, ask): device (devices.lua: flutter devices --machine → pure parse/usable/order/entries; the last one used, the only one, or a picker) → entry point (lib/main*.dart, last one first) → mode, then commands.run(opts, { cwd, target }, launch_config) with OUR launch configuration (launch_config, pure): -d <id> and --profile/--release go in toolArgs, NEVER in args: flutter's debug adapter only looks for the mode in toolArgs (profileMode), otherwise it starts the app --start-paused as if debugging. Choices are remembered per package in dartrun.json ({ target, device, device_name }; the first format, the target alone as a string, is still read). User setup_project configs win for r/d; a non-Flutter package goes back to flutter-tools. s/D stop a running app (stop_then waits for dap.session() to disappear) before the new one. state.lua is the statusline chip: info is what dartrun launched, whether the app runs is asked to flutter-tools each time; flutter's adapter 3.47 emits flutter.appStarted while flutter-tools waits for app.started (never comes), so started follows the adapter's event; the chip is refreshed explicitly (lualine.refresh) on debugger events. panels.lua hides the log, the dap-view panel and the outline (<leader>dH). A run is quiet: settings.flutter.log = "hidden" sets flutter-tools' log.shown = true so the log goes to a hidden buffer, and plugins/tools/dap.lua opens dap-view for a Dart session only when dartrun.wants_panel() (<leader>dd) or on stopped with a reason other than entry (the adapter stops once at the start). pub (lua/dartpub/): outdated + annotations are pure (direct/dev only, sections tracked by top-level keys). flutter gen-l10n on .arb save lives in dartgen (the plugin also loads on *.arb). Tests: scripts/test-flutter.sh (fake dart replaying real build_runner and dart test output in scripts/fixtures/), scripts/test-agent-dart.lua.
Start screen (lua/ui/start/, buttons in ui/dashboard.lua): replaces NvChad's nvdash with the landing page's start screen: braille art (aqua), repo and branch (yellow), user (faint), then buttons of a fixed width, one soft block each (label left, shortcut right), the selected one solid blue; j/k, arrows or Tab move, Enter or a click activates, and each button's own shortcut (ff fo fw th ch ca rs) jumps straight to it. layout.lua is pure (tested in scripts/test-flat.sh) and responsive: a short window drops the gaps between buttons, then the art. init.lua owns the buffer (filetype nvdash, which the rest of the config already recognizes), hides the cursor while it is shown and redraws on resize and when the art changes. To add a button, add it to actions() in ui/dashboard.lua.
Tree: plugins/navigation/nvim-tree.lua hides the project row on top (root_folder_label = false: <CR> on it only went up a folder, - does that) and colors names that start with a dot (highlight_hidden = "name" → NvimTreeHiddenFileHL/FolderHL in ui/flat/groups.lua, purple with the contrast kept on light themes); dotfiles are filtered out by default and H shows them.
Flat design (lua/ui/flat/, settings.ui): the whole editor looks like the landing page's demo editor, view by view. The editor itself is painted deep (the demo's editor; the theme's own bg is the "page" around it): init.lua applies the groups and then groups.rebase moves EVERY group base46 or a plugin painted with the theme bg onto deep (also fg = bg, how themes hide characters), so gutters, panels and plugin windows follow. A float, menu or panel is a FILLED BLOCK (soft), never a box: winborder = "solid" adds one blank cell of padding and FloatBorder is painted like the block, so the "border" is air. Where the demo has 2-8px gaps or paddings the terminal uses half cells: ▀/▄/▐ in FlatEdge (block over editor). Selection is solid blue with on text, matches and keys are yellow, groups purple, hints faint, additions/deletions are veils (blended over deep). palette.lua derives the tokens (bg, deep, soft, fg, dim, faint, on, accents) from the active base46 theme (or the terminal-inherited one), groups.lua turns them into highlight groups (pure, tested by scripts/test-flat.sh), init.lua re-applies on ColorScheme, NvThemeReload and every LazyLoad and owns the global options (winborder, fillchars, showtabline = 0, blinking cursor). The views: telescope.lua is telescope's create_layout (results over the prompt, preview right, titles INSIDE the blocks in faint capitals, the previewed path in yellow as the preview's winbar, the hit line veiled only when the entry has a line; geometry is pure); whichkey.lua replaces which-key's view.show (a full-width soft panel over the statusline, 4 columns filled by ROWS, groups first, faint Space » +Find … Esc close · BS back footer; grid is pure) and labels.lua hides the words the open menu already says (the demo ports it as trimLabel); grug.lua (soft panel, Grug FAR · g? help header, each input a deep bar with its aqua label); chips.lua (harpoon's chosen row as a chip as wide as its text); notify.lua (the demo's note: bottom right, a row of air above and below the text so the thin level bar runs the block's whole height (half-cell rows cannot carry it), the lead phrase bold in the level color, fades in, one row of editor between notes and above the statusline, stacks above Neovim's own message window); Neovim's messages use the built-in message UI (vim._core.ui2, settings.ui.messages): no "Press ENTER" screen; short ones ("file" written, errors, warnings, a print) are routed into the notes by wrapping its msg_show (notify.message: kind → color, progress steps coalesced into one note, a repeated note refreshes instead of stacking), listings/long output/prompts stay in its own windows styled as blocks, g< for the full pager; motion.lua (fade-in of which-key (plus a 2-row slide), modals, harpoon and notes, ~110ms ease-out; every frame flushes its own redraw because which-key waits for keys in a loop that does not redraw, an animation yields as soon as someone else moves its window, and full-screen surfaces (telescope, commit panel) never fade: blending them repaints the whole terminal; settings.ui.motion = false turns it off); fresh.lua (yank and agent accepts glow yellow and fade); keys.lua (the demo's keycaps, off by default, :FlatKeys). Elsewhere: modals (utils/modal.lua: title in fg capitals, tree glyphs, input as a deep bar), the start screen buttons (row + ▀ edge), the lualine theme (ui/lualine/theme.lua: soft bar, mode/position chips of fixed width), diagnostics blocks (message source 1/N, no icon or label: the bar and the veil say the severity; at the text column) and their line popup, blink (no blank rows, label … detail, docs half a cell apart), the terminal float (NvChad's Alt+i, chadrc.term: theme bg, the demo's place), Trouble (flat list, Diagnostics N header), Diffview (soft Changes panel, HEAD/WORKING TREE headers, no diagnostics in diffs) and the commit/agent panels (commit/highlights.lua, agent/ui/highlights.lua). Plugins take their border from require("ui.flat").border() ("solid", "float" compact, "beside" for a block glued right of another; "single" with ui.flat = false). New surfaces must take their colors from groups.lua, never hardcode a color or a border style. The landing page's theme picker (docs/index.html) ports palette.lua to JS: keep both derivations in sync. To compare a view with the demo, capture nvim through a terminal emulator (pty + pyte → HTML) rather than tmux, which drops erased cells' colors.
Per-machine settings: lua/local.lua (git-ignored) returns a partial table that is merged over core/settings.lua (theme, disabled languages, format-on-save, vim options, clipboard, light mode...). Example: return { languages = { disabled = { "astro" } } }.
Language packs and their shared layer
The installer asks one thing in a terminal: which packs to set up (nvim/lua/languages/packs.lua: one pack per line, read by install.sh with sed before nvim is installed, by install.ps1 and install-local.lua through --packs/--expand). The base (Lua, Markdown, JSON, YAML, TOML) is always on; packs whose tools are on PATH start checked; --lang (packs or languages, all, none), --yes or no terminal skip the question; an existing local.lua is never asked about again. Later, <leader>Sp → Languages toggles a pack, saved as languages.packs.<id> (one key per pack: the prefs are deep-merged and vim.tbl_deep_extend merges lists by index, so a saved list would mix with local.lua's) and it wins over languages.disabled (languages.disabled(name) in the registry). Only languages that are on install their Mason tools.
What every language menu reuses (one folder each, a contract each):
langtest/, tests inside the editor (Dart keepsdarttest/, a streaming reporter). An adapter (adapters/pytest|jsunit|cargo|swift.lua) saystests(buf)(treesitter: id, name, range, kind),command(ctx)(scope nearest/file/all, report path) andparse(out, ctx)(results with status, file, fail_line, message);init.luaruns one process and paints marks (✓ ✗ ○ …), diagnostics (langtestnamespace), the quickfix list and a summary; results kept per file and repainted onBufReadPost. ids are what the runner prints (TestCalc::test_add,sum > adds,calc::tests::adds,CalcTests.testAdd); a result without the same id matches by a unique name. pytest via JUnit xunit1 (uv runin a uv project, else the .venv), vitest/jest JSON (--testLocationInResults), bun JUnit, cargo and swift text. Multibyte marks (✔ ✘) are matched with plain finds: a Lua pattern class reads bytes. Keysm M A . T O.langsetup/, project setup presets (Go keepsgosetup/):langs/<lang>.lua(topics, root,vars(root)read from package.json/pyproject/Cargo.toml/Package.swift) +presets/<lang>/<topic>/<variant>.<ext>with#or//header lines; JSON,.nvmrc,.python-versionand.swift-formatare written without the description (langsetup.bare). Never overwrites. Keys underS(plus Sp several, Sm Makefile, Sg .gitignore).langdebug/, nvim-dap per language, adapters registered on first use: debugpy (Mason's venv python, the project's interpreter), codelldb (cargo build/cargo test --no-run --message-format=json→ the executable), lldb-dap from the Swift toolchain. Keys underD.sqlrun/, SQL against PostgreSQL/SQLite with psql/sqlite3 (no resident process):connections.lua(settings, .env URL keys, the environment's DATABASE_URL, SQLite files; the pick remembered per project;sqls(root)feeds servers/sqls.lua),statement.lua(pure:;outside strings, comments and$$bodies;risky;readsfor EXPLAIN ANALYZE),init.lua(results splitsql://results, confirmations, explain, schema). The formatter's dialect follows the connection (formatter_options.sql_formatter.prepend_args).
Per language: Python (<leader>p: basedpyright "standard" with the .venv as interpreter, ruff server with hover off, uv add/remove/sync, coverage), TypeScript (<leader>t: biome only with a biome.json via require_cwd, else prettier; tsc --noEmit to quickfix; vtsls actions), Rust (<leader>r: rust-analyzer with clippy, expand macro, open Cargo.toml, rustc --explain), SQL (<leader>Q), Swift (<leader>i: SwiftPM, sourcekit-lsp only when on PATH), Make (treesitter + checkmake; tasks stay in <leader>R). Curated snippets per language in nvim/snippets/<ft>.json. Tests: scripts/test-langs.sh.
Key tools included
| Category | Tools |
|---|---|
| LSP | gopls, vtsls, biome, tailwindcss, eslint, jsonls+schemastore, prismals, dockerls, astro, basedpyright, ruff, rust-analyzer (clippy check), sqls, sourcekit-lsp (toolchain) |
| Formatters | stylua, goimports+gofumpt, biome or prettier (by project), dart_format, ruff, rustfmt, sql-formatter (dialect by connection), swift format |
| Linters | golangci-lint (govet in light mode), eslint_d, ruff (light mode), checkmake, swiftlint (when installed), markdownlint, jsonlint, ysc (YarnSpinner); a linter that is not installed is skipped |
| Completion | blink.cmp, mini.pairs |
| Terminal | toggleterm, lazydocker (via TermExec) |
| Navigation | harpoon (<leader>h*), flash.nvim (s/S), aerial (symbol outline), nvim-ufo (folds) |
| UI | lualine, gitsigns, treesitter-context (:TSContext toggle), vim-illuminate, render-markdown, todo-comments, nvim-highlight-colors (hex/rgb/Tailwind preview) |
| Other | lazydev (nvim API types in Lua), git-conflict (<leader>Go/GT/GB/GN/Gn/Gp/Gl), Telescope+fzf-native, nvim-tree, Trouble, autotag, git-blame, grug-far (<leader>fr global replace), diffview (<leader>Gd diff, <leader>Gf/Gr history), mini.surround, mini.move (Alt+hjkl), multicursor.nvim (Ctrl+↑/↓, Alt+n/p/a, Alt+click; loads on keys), inline agent (<leader>a, lua/agent/), neoconf, package-info, persistence (sessions) |
Performance
- git-blame is off by default — toggle with
<leader>Gb(noBufReadoverhead) - Telescope respects
.gitignoreinfind_files(skipnode_modules,dist/, etc.) - nvim-tree hides git-ignored dirs by default — toggle with
Iorgit.ignore = false - Plugins are lazy-loaded via events, ft, cmd, or keys (not on startup)
- Big files (
lua/core/bigfile.lua, knobs insettings.bigfile): overmax_bytes(1.5 MB), or a minified bundle (≥ 100 KB averaging ≥ 1000 bytes a line), the filetype becomesbigfilethrough a catch-allvim.filetype.addmatcher, so nothing that follows the filetype starts (treesitter, LSP, ufo, lint, conform); swap, syntax, folds, completion are off for that buffer. A 4 MB Lua file opens in ~135 ms instead of ~1.9 s.utils/comments.luafolds long comments before the first paint only up tosettings.comments.sync_max_lines(a synchronous parse costs ~2 µs a line); past that ufo does it asynchronously. Tests:scripts/test-bigfile.sh. - Benchmark (
make bench,scripts/bench.sh [--runs N] [--app PATH] [--app-only] [--json]): the app's phases to the first frame, Neovim's startup, opening a 1 MB / 4 MB / plain-text file, the first picker of a session, the code-search index (first session vs stored) and the picker's file list; median, best and worst of N. Compare two binaries by running it with--appon each, interleaved: this machine's noise (WSL) is larger than most single changes. Measured on WSL2 (Mesa d3d12): first frame ~160–190 ms, Neovim ready at ~60 ms.LEADER_TIMING=1printsgpu thread (ms)laps: creating the wgpuInstance(the driver loading) is ~90% of the GPU's startup (~75–100 ms), surface + adapter + device + pipeline together ~25 ms; the first present adds ~60 ms.Renderer::create_instanceneeds the display, not a window, somainstarts it on its own thread right after the event loop is built (~15–30 ms off the first frame). Warm-up: the first real frame cost ~140–175 ms of main-thread work (22 ms rasterizing glyphs, 80–110 ms in the firstsubmit: Mesa compiles the GL pipeline there, 40 ms in the first present).renderer::warm_updraws one throwaway quad into a 1x1 texture at the end ofRenderer::new, on the GPU thread, while Neovim loads: the first frame then renders in ~50–60 ms. It was tried before the hold below existed and made things worse (the GPU thread was the long pole then); with the first frame gated on the config, the GPU thread has slack and it pays. Vulkan is not available on this WSL, so GL stays. - Saving a big file (
lua/core/saveformat.lua,settings.format_on_save.async_over_lines, 3000): a save used to wait for the formatter, and that grows with the file (a 15k-line Lua file took 1.2–1.9 s with the language server as the fallback formatter; the write itself and every save hook together cost ~20 ms). Up to the limit the buffer is still formatted before the write (the file on disk is always formatted); past it the buffer is written at once (:wreturns in ~20 ms) and conform formats it in the background (format_after_save), then writes it again. Both options are functions that read the settings at call time, so<leader>Sfstill turns formatting off and on. Tests:scripts/test-bigfile.sh. - Linting is debounced (
plugins/tools/nvim-lint.lua,settings.lint.debounce_ms, 250): a linter is a process (the Node ones cost 100–200 ms of CPU to start) andInsertLeave, saves and the format-after-save rewrite arrive in bursts; one trailing timer per buffer turns 25 events into 1 run. The first lint of a file (BufReadPost) stays immediate. - Start screen git info (
utils/git.lua) is cached for 3 s per folder: the screen draws again on every resize and each draw ran threegitprocesses. - Heavy-edit benchmark (
make bench-edit,scripts/bench-edit.lua, real config in a Lua buffer with a UI attached): 15k-line paste ~10 ms, undo ~25 ms, copy ~18 ms, replace-all ~60 ms, a 300-notification burst ~5 ms, 5000-line save ~28 ms, reindent-all (gg=G, treesitter indent) ~800 ms, and per-key typing latency in a 15k-line buffer (p50 ~0.4 ms, p95 ~7–15 ms). Known limits that are Neovim's, not ours: the p95/p99 typing spikes come from the treesitter highlighter re-parsing a dense 15k-line buffer after each edit, and 5000 diagnostics in one buffer cost ~150 ms in core's underline/sign handlers (ourDiagnosticChangedlisteners take ≤2 ms; 500 diagnostics take ~16 ms). A frame is ~16 ms becausepresentblocks on vsync (Fifo is the only mode on WSL's GL) and a single key reaches the screen in 15–18 ms, one vblank at 60 Hz, so the render loop is already at the display's floor. - Lua gets
emmylua_lswhen it is installed (lua/servers/lua.lua,settings.lsp.lua_server= "auto" | "emmylua_ls" | "lua_ls"; Mason installsemmylua_lswith the full pack,lua_lsstays as the fallback andservers/lua_ls.luakeeps its trimmed settings).lua_lsparses every file of the workspace and of its libraries (single-threaded, ~4 ms a file) before it can answer across files and keeps nothing between sessions;maxPreloadhas no effect and trimming its libraries saves ~0.3 s. The Rust server indexed this repo's 670 files in ~0.6 s on its own progress events: cross-file go-to-definition answered at ~0.7 s against ~3.8 s on the bare workspace (3 of 3 rounds, only the server under test allowed to start), and with this config (the runtime as a library, lazydev, a loaded startup) ~2.6 s against ~4.2–6.5 s, a steady ~2.5x in five interleaved rounds under heavy load (hover with the real signature, 181 completion items and the undefined-variable diagnostic on both). Its library must be the runtime'sluadirectory: the type stubs alone (vim/_meta) left hover atany, completion empty andvim.*unknown, which a check that only looks for a non-empty hover does not catch. lazydev supports it (supported_clients). Pause/resume act onservers.active, the servers chosen at startup, not the declared ones. A first attempt compared the two with a probe that searched a symbol also present in the open file and then withlua_lsstill running next toemmylua_ls: both gave wrong answers (it looked slower), so a comparison needs only one server allowed to start and a symbol that exists in another file. - Language servers: stop, restart, pause (
lua/utils/lspctl.lua, menulua/menus/lsp.lua,<leader>L):sstops the servers of this buffer,Severy server of every project,rrestarts this buffer's,ppauses or resumes all of them (no server starts for any file until resumed),isays what is loading right now. A server's "Loading workspace" cannot be cached from here (lua_ls keeps nothing between sessions: ~0.8 s to start and ~2.8 s to parse this repo'snvim/), so this is the way out of waiting for one that is not worth it; the stop is forced and does not wait for the server (13 ms against the real lua_ls, in the middle of loading). Neovim 0.12'svim.lsp.get_clients()hides a server that has not answeredinitializeyet, which is exactly the one stuck starting (tsserver, jdtls, rust-analyzer on a big project), solspctl.all()also probes client ids; andvim.lsp.enable(name, true)does not look at buffers that already exist, so a restart firesFileTypefor the buffer again.servers/lua_ls.luaturns off the third-party scan and the prompt, and skips files over 100 KB and past 2000 files (~0.3 s on this repo; the runtime library stays, for go-to-definition). Tests:scripts/test-lspctl.sh(a server that never answers). - Tests and scripts never touch your state (
nvim/lua/plugins/editing/persistence.lua,scripts/hermetic.sh): Neovim saves a session on every exit, and a headless test that opened temp files in the repo replaced the repo's real session with files that no longer existed (restoring the project opened an empty buffer) and left ~1400 sessions of/tmpfolders, 58 of 60 "recent files" projects and ~1000 agent-history lines behind. Two layers: persistence is stopped when no UI is attached when it loads (a script, a test,--headless; the terminal and the desktop app always have one, and a real terminal run still saves), and every test script sourcesscripts/hermetic.sh, which gives each run a throwawayXDG_STATE_HOME(sessions, recent files, agent history, shada, logs). Checked by running the whole suite against a snapshot of the real state: byte-identical before and after. New scripts that start the real config must source it. - Idle preload (
lua/core/preload.lua,settings.preload): Telescope's modules load ~600 ms after startup, one small step per tick, never in light mode. The first<leader>ffshows results in ~37 ms instead of ~78 ms (the picker itself already streams: on 100k files the first rows appear beforerghas listed them all). - Code search on disk (
lua/codesearch/store.lua,settings.search.persist): the per-file symbols and theirmtime:sizestamps are kept as JSON in the state dir (one file per project, atomic, ignored when the format or folder differs; bumpstore.FORMATwhen whatindex.readextracts changes). The first search of the first session builds the index (~5 s on this repo's 1.6k files); every later session loads it (~120 ms) and re-reads only what changed. - Startup order of the app (
app.rs,platform::Cell): Neovim reads its config only once a UI attaches, so the app attaches before the window exists, with the grid computed from the last run's cell (display scale + cell size,~/.local/state/leader/cell) and the saved window size;resumedresizes if the real grid differs (a maximized window, a new font) and a first run, which has no cell yet, attaches the old way. Neovim's config starts ~50 ms sooner. The config tells the app it will announce its first complete screen (leader_holdatgui.setup,leader_readyaftergui.startuphas opened the project or the picker, with a:redrawfirst so the content is on the wire before the notice), and the app draws nothing until then, so the first thing on the window is the real themed editor, not Neovim's black default. A 500 ms deadline (HOLD_MAX) covers a config that holds and never releases; a config that never holds (someone else's) is not delayed at all.LEADER_TIMINGmarks the first frame drawn after the config's notice ascontent frame(what the benchmark reports ascontent_frame: the real editor on screen;first_framealone is not comparable between a build that draws black first and one that waits). Bench-only switches to measure what each piece buys on the same binary:BENCH_ENV="LEADER_NO_HOLD=1 LEADER_NO_WARM=1" make bench(ungated, no warm-up) vs the default; interleave the variants over several rounds. On this machine (WSL2, noisy, other agents running) the default reached the content ~25–75 ms sooner than the ungated build in 4 of 4 interleaved rounds (e.g. 195 vs 270 ms), and the window no longer opens on a black frame. - Reusing a window (
app/.../single.rs,leader --reuse/-rorLEADER_REUSE=1, opt-in): every normal window listens on a private socket ($XDG_RUNTIME_DIR/leader.sock, else the state folder;LEADER_SOCKEToverrides it; never in snapshot runs).leader --reuse file…sends its folder and paths and exits before building a window or starting Neovim (~6 ms against ~180 ms to a first frame); the window opens them throughrequire("gui").open_external(a folder becomes the project, files are edited) and comes forward (request_user_attentionwhere a compositor refuses focus). With no window listening it starts one as usual; "New window" in the app still spawns a separate process. The socket belongs to the most recently started window. ftdetect/yarnspinner.vim—*.yarn→yarnspinnerfiletypesyntax/yarnspinner.vim— syntax highlighting for YarnSpinner dialogue scripts
Gotchas
- Clipboard (WSL/SSH without DISPLAY):
core/clipboard.luauses OSC 52 to copy (Alacritty + herdr forward it); pasting from the system is Ctrl+Shift+V in the terminal - Undo, redo, clipboard, yank history (
lua/history/, global<leader>umenu, knobs insettings.historyandsettings.clipboard.keep):undo.luasplits an insert into undo steps like VS Code (after whitespace, a new line, or apause_mspause) by feeding Vim's own<C-g>ufromTextChangedI/ a timer, ONLY whengetchar(1) == 0and no register is executing: with keys still queued (a macro,., fast typing) the<C-g>uwould land in normal mode as<C-g>+uand undo the replay.vim.o.undolevels = vim.o.undolevelsdoes not break undo inside insert mode, so feeding is the only way;.still repeats the whole insert.u/<C-r>/U/<C-y>/<C-z>runnormal! u/<C-r>silently and flash the'[..']lines (ui.flat.fresh);<leader>uuis Neovim's built-in:Undotree(packadd nvim.undotree, moving in it moves the buffer),<leader>u[/]are:earlier 1f/:later 1f(the file at the previous/next save).yanks.luakeeps every TextYankPost newest first without duplicates, writesstdpath("state")/yanks.jsonon exit (merged with what another nvim saved;clearwrites at once or the merge would bring entries back);p/Pare wrapped to remember the paste so[y/]ycan undo it and put an older/newer entry through a borrowedzregister (restored). Withclipboard.keep,x X c Care expr maps returning"_x… only for the default register (an explicit"axstill copies) and visualpreturnsP; they are mapped AFTERmenus.setup()because loading which-key makes lazy delete thecstub of NvChad's spec, which would take ourcwith it.ui/flat/keys.luaignores the fed<C-g>u. Tests:scripts/test-history.sh(types into a child nvim withnvim_input, one key at a time). - ysc (YarnSpinner linter) is optional: it is enabled only if
yscis on the PATH (dotnet tool install -g yarn-spinner, requiresdotnet) :checkhealth masonwarns about missing ruby/gem/php/java/julia/composer/luarocks: it is informational only, no tool in the config needs them- Contextual leader menus (
lua/menus/+ each language'smenufield): each language has its own letter and only exists in buffers of that filetype (g Go, d Dart, r Rust, p Python, t TS/JS, m Markdown, s CSS, j Java, k Kotlin);<leader>GGit only inside a repo. To add a language: a new entry in thelangstable. Don't define a mapping that is both an action and a prefix of others (e.g.<leader>h+<leader>h1): which-key resolves it aftertimeoutlenand closes the popup - Long comments (global
<leader>Cmenu,utils/comments.lua): detects with treesitter (nodescomment,line_comment,block_comment... whichever exist in the grammar) blocks and runs of consecutive lines that are alone on their line, ofcomments.min_lines+ lines, andplugins/editing/ufo.luaadds them to nvim-ufo's folds withkind = "comment"(it doesn't duplicate those the provider already folds).Ccfolds and saves the "folded" preference (global,stdpath("state")/comments.json;comments.auto_collapseis only the initial value),Ceunfolds and turns it off,Cttoggles the one under the cursor without touching it. With the preference on, ufo closes the folds ofkind = "longcomment"(its own kind: treesitter's uses the node name and would also close the short ones) when each buffer is first shown:comments.sync_ufoadds/removes that kind in the LIVE listrequire("ufo.config").close_fold_kinds_for_ft.default; aBufWinEnteraligns the buffers that already existed and folds NOW (fold_now: synchronous:fold+ close, before the first redraw) the newly opened buffers, because ufo's provider is asynchronous and let the opened file show for a few tens of ms; ufo then rebuilds the folds and closes them by kind without it being noticeable. Comments at the end of a line of code don't count. The closed fold's text (fold_text) adds the first line with content if the first one is just/** - Code folds (global
<leader>zmenu,utils/folds.lua,menus/folds.lua): closes folds by what the code IS, on top of nvim-ufo's folds.f/t/ofold the function / type / object literal under the cursor (pressing again walks outwards: closure → its function),bthe innermost fold (zc, again = the one around it),mthe methods of the type you are in (type stays open),F/T/Oevery function / type / object in the file,aoutline only,eexpand all,sfocus (everything closed except what contains the cursor),uunfold here and inside (zO),ztoggle (za),1-3fold everything nested deeper than that level (ufo.closeFoldsWith). Functions and types come from thetextobjectsqueries (@function.outer,@class.outer, viaagent/context/scopes.lua, same definition asaf/ac); objects have no query, soutils/folds.luamatches EXACT node types (OBJECT: a substring match would catch argument lists andmap_types): add a language's literal there.close()closes the range's own fold; if the line has none (one-liner, ufo not done yet):foldclosewould hit the PARENT fold, so that is undone and a manual fold is made instead (ufo may redraw it on the next change). Ranges are closed inner-first (later start first). Single-line nodes are never folded. The long-comment folds have their own menu (<leader>C);ehere also opens them. - Conventional commits (
lua/commit/,<leader>Gmenu:Gcpanel,Gmpanel + AI draft,GMautomatic commit without a panel,Gaamend): ONE panel (commit/panel.lua, three stacked floats: files, anacwritemessage with filetypegitcommit, and an agent status line below, becausevirt_lines_aboveon row 0 doesn't get drawn) with the three entry points ofcommit/init.lua. One folder, one responsibility:conventional.lua(PURE:split,parse_header,checkfor the live indicator,rewriteof type/scope,suggest_type/scopes,clean_message),git.lua(status with +/− per file via--numstat, stage/unstage, async commit, branch/upstream),ai.lua(prompt +describe, which announces in the panel what will be sent BEFORE spending anything),auto.lua(GM: no panel and in the background),highlights.lua(derives from theAgent*ones, which come from the theme),panel.lua(UI). The AI goes throughrequire("agent").complete(agent/oneshot.lua: same backend/model as the actions, light tier reassignable withagent.tiers.commit, returnsusageas the callback's 3rd argument); the draft is never requested on its own (exceptGm) and replaces the message in an undoable way. The AI message ALWAYS comes out in English and on a single line (Conventional header; there is no language setting). It is fast on purpose: COMPACT diff (git.diff_for_ai: no git headers,-U1, no lockfiles/maps/snapshots,diff_max_charscap split among files;--src-prefix=a/ --dst-prefix=b/forced, because a globaldiff.mnemonicPrefixwritesc/ i/ w/and the parser saw NO files: the draft lost the diff, same inagent/review/init.lua), the branch and the types; subjects of previous commits are NOT sent (the model copied their language; the scope vocabulary is computed locally). Measured with haiku: ~2 s and ~$0.0004.GM: if there are staged files it commits only those; otherwise it stages everything modified exceptgit.commit.sensitive(.env, keys...), asks for the message, validates the header and commits; if anything fails (no backend, AI, invalid header, hook) it undoes what it staged and warns. The types and limits live insettings.git.commitand the.githooks/commit-msghook repeats the type list (shell): keep them identical. Pitfall: the close byWinClosedarrives queued; compare with the current panel before closing or it will close the next one you open. Tests:scripts/test-commit.lua(temp repo, fake backend, whole panel). - Code documentation (
utils/docgen.lua,<leader>Cmenu:Cdtemplate,Cmmissing): one entry per language inLANGS(where the comment goes and which lines) and, if "public" isn't "everything", inPUBLIC. Reusesutils/blocks.lua(blocks.list: functions/types without closures).export function/@decoratorare captured wrapped:decl_ofextracts the real declaration. Tag snippets (todo,fixme...) insnippets/all.jsonwith${LINE_COMMENT}(blink resolves it);todo-commentsalready highlights those tags. - Sessions (
persistence.nvim,utils/session.lua, global<leader>Pmenu): saves buffers and windows on exit, one per folder (and per git branch;main/masteruse the base). They are NOT restored automatically: the dashboard's "Restore Session" button (rs) (only if there is a session for that folder) or<leader>Pr;<leader>Pspicks a project,<leader>Plthe last one,<leader>Pwsaves now,<leader>Pxdoesn't save on exit. Before saving, auxiliary windows (tree, terminal, outline) are closed. Opening a single file (nvim x) and quitting overwrites that folder's session: use<leader>Pxif you don't want that. What it includes:options.sessionoptionsincore/settings.lua. Persistent undo (undofile) is enabled inoptions; browse it with<leader>uu - Autoread (
core/autocmds/autoread.lua):autoread+checktimeonFocusGained/TermClose/TermLeavereload what git or the formatters change outside nvim (never overwrites buffers with unsaved changes). Search withignorecase+smartcaseandinccommand=split(:spreview) inoptions - git-conflict.nvim: no default mappings (
co/cbwould shadowcoperators); the actions live in the<leader>Gmenu (o ours, T theirs, B both, N none, n/p next/previous, l list in quickfix) - Go: struct tags and snippets:
utils/gotags.luais a blink.cmp source (goonly) that derives the tag from the field NAME: inSentAt time.Time+ backtick it suggestsjson:"sent_at"(+,omitempty, camelCase, yaml, db...); afterjson:orjson:"it suggests the value and after,the options (omitempty,omitzero,string). Inside a tag the first item comes preselected (Enter accepts; outside itpreselectstays off). Withgorm:gorm:"suggests full values according to the field's name and TYPE (primaryKey;type:text,type:text;not null,autoCreateTime,foreignKey:XID...; nonot nullif it is a pointer) and after;single options (separator;, not,; textEdit from the last;); the backtick key menu also lists the gorm variants after json.utils/gomodel.lua(only*_model.goormodel.go; gosmart calls it when starting a declaration and puts its items AT THE TOP of the list):functable→func (XModel) TableName() string(derived name: withoutModel, snake_case, plural),funcdomain(toDomain + fromDomain),functodomain,funcfromdomain(func (m NoteModel) toDomain() *note.Note/fromDomain(n *note.Note) NoteModel; domainNoteModel→note.Note; the constructor is looked up in the imported package and, if it isn't there,Reconstitutestays as a placeholder). With several structs:functable X,from<Base>Domain. Only for structs that don't have the function yet. For structs already written:<leader>ga(json snake_case),<leader>gA(+omitempty),<leader>gD(remove) via go.nvim/gomodifytags; also<leader>gsfill struct,<leader>geif err,<leader>gTadd test. Own snippets innvim/snippets/go.json(VSCode format, blink reads them alongside friendly-snippets) with FULL NAMES, no acronyms, only for STATEMENTS and utilities (iferr,errorsis,waitgroup,contexttimeout,testtable,httphandlerjson...).utils/goindex.luais the shared index (treesitter over text: interfaces, structs, methods, functions, constants per file, package and module; cached by mtime; the unsaved buffer replaces its file) for the 0-token helpersgoiferr(if errwith the function's returns; the menu opens by itself and Enter accepts it afterx, err := …,vim.g.goiferr_preselect=falseturns it off),gomap(mappers betweenXResponse/XRequest/XModel… and its counterpart in the module),gowire(the module'sNewXconstructors with arguments resolved by type/interface),goenum(IsValid/Parse/UnmarshalText/String),goport(interface from a struct's methods andvar _ I = (*T)(nil)) andgohttp(handlers from a service; what changes per router —detection in go.mod, signature, route, query, body, response— lives ingohttpfw.lua), all called by gosmart (their items carry asortTextwith!to go on top) and tested inscripts/test-go-smart.lua.utils/goreconstitute.lua(typedrecon…/funcrecon…, ≥ 5 letters:Reconstitute(all the fields) *Xfor each struct DECLARED IN THE CURRENT FILE (treesitter:type ( … )blocks too; generic structs and other files of the package are left out) without one, no validation;ReconstituteXwith several structs; gosmart calls it fromdeclaration_items, outsideextra_declaration_itemsbecause that one only runs for thefunc…family)utils/gopackage.lua(not a completion source:gosmart.quiet_lspruns it on gopls'package Xitems atpackage …; puredecidefrom the folder's neighbours /cmd//main.go/ module root, purerankdropspackage mainwhere it makes no sense and setsitem.score_offset— blink honors it — so the folder's package leads;inforeads the neighbours' firstpackageline, skippinggo:build ignoreand comments) andutils/gorepository.lua(typedrepo/repository≥ 4 letters, ONLY when the file is in the domain layer byagent.ddd.layers.ofon its path under the module root and is not*_model.go: the persistence port of each aggregate of the PACKAGE (struct withid/ID/<S>ID, not*Model|DTO|Request|Response|Row|Entity) that has no port;Repositorywhen the package is named after the struct, elseXRepository; methods =settings.go.repository.methods) andutils/gogetters.lua(accessors of the unexported fields of the structs declared in the current file: typedgetters/funcgetters/accessors≥ 5 letters at package level, and as per-method items +all gettersafterfunc (n *Note)throughgoimpl.method_position; receiver from the struct's other methods, initialismsid→ID, slices/maps returned as copies unlesssettings.go.getters.clone = false;goindex.file_structs()is the treesitter reader both it andgoreconstituteuse) andutils/goimpl.lua(treesitter, 0 tokens; gosmart calls it): the methods a struct is missing to satisfy its interface — afterfunc (s *T)(automatic menu) and when starting a declaration —; the interface comes fromvar _ I = (*T)(nil), from the constructor that returns it, from the name (noteService~NoteService; also local module packages viago.mod) or from what is already implemented; the body calls the dependency in the same shape (return s.repo.FindByID(ctx, id)) or leaves a zero-valuereturn;…all missinguses$N(nvim only supports one$0). Tests:scripts/test-go-impl.lua(scripts/test-go.sh).utils/gosmart.lua(treesitter) is the contextual source: when STARTING a declaration (func,typeorNew…at column 0) it offersfuncvariants (simple,error,value,pointer,bool,result error,pointer error,ctx error,ctx result error,main,init), methods of EACH struct in the file (pointer,error, value,String),NewX(fields…)for structs without a constructor andtypevariants (struct, struct constructor, generic, interface*, enum string/iota, defined, func, error, options); aftertype Envelope→struct/interface/func; with the name of a package struct in VALUE position (x := Env,return Env, arguments) Enter inserts the literal ALREADY filled (Tab from value to value; in type position it doesn't apply); inside a literalEnvelope{ }(struct of the same package) it opens the menu by itself on Enter and offersfill all fields+ the missing fields with default values; insideinterface { }→ method signatures (method error,method ctx result error, embed...). Noise is silenced right there: no buffer words or the LSP's genericfunc/type, and statement snippets only show up inside functions (declaration ones, at package level). descconvention in keymaps: 2+ words and the FIRST one is the section ("Harpoon add file","Go run","General save"). The dashboard's Mappings button (NvCheatsheet) andCHEATSHEET.mdgroup by that word and discard single-word descs (the generator warns). The leader menus comply on their own (menus.descprepends the 1st word of the menu name). Neovim's native keys are excluded by measuringnvim --clean; the native groups that still leak into NvCheatsheet go inexcluded_groups(ui/dashboard.lua)- blink.cmp: Tab/Esc/jk (
plugins/editing/blink.lua,utils/blinkkeys.lua,core/keymaps/general.lua): with the menu openTab/S-Tabchoose a suggestion; without a menu, inside a snippet they jump between placeholders.Escandjkwith the menu open ONLY close it (cancel: undoes auto_insert, returns to what was typed) and keep insert + snippet; without a menu they leave insert. Flow to keep what was typed in a placeholder:Escand thenTab. Don't put the snippet jump BEFOREselect_nextin<Tab>: it prevents choosing suggestions inside a snippet - Treesitter builds parsers with the system C compiler (
gcc) and requires thetree-sitterbinary (tree-sitter-cli) - No standalone run: this is a config repo, must be symlinked to Neovim's config path
- lazy-lock.json committed for reproducible plugin versions
.claude/settings.local.jsonis NOT committed (machine-specific permissions)- Astro formatting requires
prettier-plugin-astroin the project'snode_modulesor installed globally — Mason only installsprettieritself - mini.surround keymaps:
saadd,sddelete,srreplace surrounding delimiters - flash.nvim keymaps:
sjump to any word label,Sjump to treesitter nodes (preserves nativerreplace char) - telescope-fzf-native requires
cmakeinstalled on the system (auto-disabled if missing viaenabled) - Auto-reload limitation:
luafile $MYVIMRCreloads Lua config but does NOT install/remove plugins or treesitter parsers. For plugin changes, use:Lazy syncor restart nvim. - Light mode (
nvim/lua/core/light.lua): auto-detects RAM ≤ 2GB and disables all resident stuff — LSP servers, go.nvim, golangci-lint (falls back togo vet). Mason installs onlygoimports+gofumpt. Keeps treesitter + conform (ephemeral formatters) + nvim-lint. Override withNVIM_LIGHT=1|0env,settings.light.forceorvim.g.light_mode.
Desktop app
app/ is a Cargo workspace (Rust 2024, deps pinned in app/Cargo.lock, cargo build --locked) that builds leader, a GPU front-end that runs nvim --embed with this config. Two crates, one direction of knowledge each:
leader-nvim(no window, no GPU, testable headless):rpc.rsmsgpack-rpc on plain threads (requests block,notifynever does; aHandlerserves what Neovim asks the UI, aWakerwakes the event loop after eachredrawbatch),event.rsthe typedredrawevents,grid.rsone cell grid,screen.rsthe whole UI state (grids, wherewin_pos/win_float_pos/msg_set_posput them, highlights, mode, cursor, options) andlayers()bottom to top.leader-gui(theleaderbinary):app.rswinit's event loop,scene.rsscreen → instanced quads on the CPU,renderer.rswgpu (ONE pipeline, ONE draw call per frame, a shelf-packed glyph atlas that grows),shader.wgsl,font.rs(cosmic-text shaping with font fallback, swash rasterization, cell metrics),boxdraw.rs,input.rs(winit keys → Neovim key notation),clipboard.rs,platform.rs(WSL env, backends, saved window size).
Rules that keep it fast and correct:
- Nothing runs between events: no render loop, no polling. A frame is drawn right on Neovim's
flush(not throughrequest_redraw, whose scheduling can add a frame), on resize/expose, and at cursor blink edges (ControlFlow::WaitUntil). Input isnotify(fire and forget). - Startup overlaps: the font database loads on a thread from the first instruction,
nvim --embedis spawned before the window exists, attach happens as soon as the cell size is known, and the GPU starts on its own thread while Neovim reads the config. Nothing is drawn before Neovim's first flush (no flash).LEADER_TIMING=1prints every phase and input → flush → frame. - Before attach (Neovim reads no config until
nvim_ui_attach)clipboard::SETUP_LUAsetsvim.g.leader_guiand avim.g.clipboardthat calls back over RPC (leader_clipboard_get/set, served by arboard);core/clipboard.luasteps aside whenvim.g.leader_guiis set,lua/gui/init.luasetsguifontfromsettings.gui.fontunless the user chose one (Neovim's own default is a generic list, so "chosen" = differs fromnvim_get_option_info2("guifont").default; the app ignores that default too). - Grid 1 is stale under windows: with
ext_multigridNeovim leaves old cells on the default grid where windows now are, so only grid 1 may skip painting its default background; every window and float paints all of its cells. - Block elements and box drawing (U+2580–U+259F, U+2500–U+257F) are rectangles from
boxdraw.rs, never font glyphs: the flat design's▀/▄half cells (FlatEdge) must tile with no seam. Braille and everything else comes from the font. - Cursor: shape and size from
mode_info_set, colors from itsattr_id(0 = reverse of the cell);blend = 100on that highlight hides it (the start screen'sNvStartCursor); blinking followsguicursor's blinkwait/on/off and restarts only when the cursor or mode changes. - App bar (
appbar.rs): the tools on both sides are generic (left/rightlists of{ icon, label, tip, action, menu, active }from the config; a label draws a ▾ when it opens a menu,activekeeps the block lit); every tool, the mark and the window controls show a tooltip after 550 ms under the pointer (a flat block under the bar, drawn over the editor); a right click anywhere on the bar but the controls asks the config to customize it. Its text is smaller than the editor's (font::SMALL, measured intoFonts::smalland rasterized at that size: the glyph cache keys carry the size;scene::cell_text(..., small)), its height still counted in editor lines. The window has no system decorations; the bar is ours and part of the editor: it is painted with Neovim's default background (screen.colors.bg, so it follows a theme switch at once) and steps back (its content fades toward the background, 180 ms) while a modal is open: any float bigger than 12% of the editor (Telescope, the welcome screen, which-key's panel; notifications, keycaps and menus are too small). Onelayout()serves drawing and the mouse. Window controls: three small flat squares (no rounding, like everything flat), minimize · maximize · close on the right by default (controls = "left"moves them, close first), quiet until the group is hovered, then yellow/green/red with −/+/×. Left: Leader's mark (the brand'sclassicNfrombrand/dots.js, what its favicon uses at small sizes; antialiased discs, shader kindCIRCLE; on hover a wave of swelling, warming dots; a click opens the app menu), the project's name with ▾ (the projects menu), ⌂ and ↺ (each can be hidden:buttons = { mark, project, home, history }). Center: the current file's path, centered on the window while it fits, leading folders giving way to "…" (the last real file is kept while Telescope/the tree/a terminal has focus, nothing on the start screen). Right: the activity (newest LSP$/progress, lazy.nvim's install/update/sync/restore, mason-tool-installer's run, a save; a three-dot spinner, then a check for 1.6 s; a 2 px progress line along the bar's bottom edge, filled to the percentage or sweeping when unknown; fades in and out), then genericchips({ text, fg, bg (= a pill), gap, action }) the CONFIG decides:lua/gui/init.luasends only what the statusline does not show ("N to finish" from the install checks withaction = "welcome", recording, gitsigns' added/changed/removed for the file, non-utf-8 encoding / non-unix line endings; LSP clients and the indent only whenshow.lsp/show.indentturn them on), with colors fromui.flat.palette.current()(mark =green), overrpcnotify(vim.g.leader_gui_channel, "leader_appbar", …), coalesced to one per tick and only when the JSON differs. Never repeat the statusline there (mode, branch, project, diagnostics, position). Clicks call the config by name: a path part →require("gui").click(i)(0-based, folders then the file: Telescopefind_filesin the folder, nvim-treefind_filefor the file); the mark →action("menu", col)(the app menu), the project →action("projects", col), ⌂ →action("home")(the start screen, in a normal window), ↺ →action("history")(Telescope oldfiles), a chip with an action → that action;colis the editor column under the button (Bar::anchor), where its menu opens. The window shortcuts use the same actions (F10 menu, Ctrl+Shift+O projects, Ctrl+Shift+H/R/W) or the window itself (F11 fullscreen, Ctrl+Shift+Enter maximize, Ctrl+Shift+M minimize, Ctrl+Shift+N a newleaderprocess); the config's menus ask the window for those by name withrpcnotify(chan, "leader_window", name)(fullscreen,maximize,minimize,new_window,zoom_in,zoom_out,zoom_reset;App::window_command). - Menus, palette and projects (
lua/gui/menu.lua,lua/gui/picker.lua,lua/gui/projects.lua): a menu is a stack of flat floats (the root at row 0 under its button, a submenu beside its item;GuiMenu*groups, the chosen row whole:cursorlineopt = "line"because the config's isnumber; the cursor hidden with the start screen'sNvStartCursorwhile any is open). Items:{ label, keys | run | items, hint, detail, current, when }or{ sep = true };keystypes the config's own mapping (so the item does exactly what the key does) and its hint ismenu.describe(keys)("Space f w", "Ctrl+Shift+R");whenhides it (Git only inside a repo);menu.visibledrops doubled/edge separators; j/k skip separators, Enter/l/click run or open the submenu, h goes back, Esc/q or focus leaving every menu closes them. The app menu (gui.app_menu) follows Zed's: File, Edit, Selection, View, Go, Run, Git, Agent, Window, Help + palette, project picker, home, quit; every key in it must be a real keymap of the config (check with anvim_get_keymapdump before adding one). The command palette (gui.palette, Ctrl+Shift+P) is Telescope overmenu.flatten(app_menu)("File › Save") plus every normal keymap whosedeschas 2+ words (the config's own; Neovim's:helpones are skipped). Projects: every folder Neovim works in is remembered onVimEnter/DirChanged(stdpath("state")/leader-projects.json, newest first, 30 at most, never the home folder or/);projects.switch(dir)asks to save unsaved files (never drops them), saves the current session (persistence.nvim, auxiliary windows closed first), wipes the buffers,cds, remembers it and restores that folder's session or opens the start screen. The picker (picker.open, Ctrl+Shift+O) isutils/market.lua(every market lights the chosen row whole,cursorlineopt=linebecause the config's isnumber, withMarketSeland a▌MarketBaroverlay in theMarketSelnamespace, and hides the cursor block until it closes;spec.hjklmakes l/→ only move to the details and h/← back, so only ⏎ or a click opens a project): Recent, On this machine (fd's, elsefind's.gitfolders under the home, depth 5, in the background; caches, toolchains and hidden folders left out; fd prints folders with a trailing/, strip it beforedirname), Start (path, new project = mkdir + git init, clone); the pane is drawn for any row (return nothing for titles), with text-onlyGuiPicker*groups because it sits on the editor's color; facts per folder (picker.info: stack frompicker.MARKERS, the README's first lines, persistence's session file by its name from ONE listing per opening, then branch/changes/last commit fromgit status --porcelain --branch+git log -1run in the background, ONE folder at a time:m.wantis the newest asked for, so a fast sweep starts git only for the row it stops on; the pane says "reading git …" and is redrawn when the answer is for the row still under the cursor). Everything is cached on disk bygui/projcache.lua(stdpath("state")/leader-projects-cache.json, NEVER the file ofgui/projects.lua's recent projects,leader-projects.json, which must stay a JSON list (projects.listignores anything else); read once per session, written when the picker closes or a search ends): facts per folder with a stamp of three stats (the folder,.git/HEAD,.git/index; taken AFTER git answers, becausegit statusrewrites the index) and the found list, searched again only when older thansettings.gui.projects.rescan(1 h) or withR. Pinned projects (projects.pin/pinned, apinnedflag on the entry;trimdrops the oldest unpinned past 30; a folder never opened can be pinned) come first in the picker (p) and in the projects menu (★).market.refresh(m, true)is the cursor-only path (list not redrawn: bar, pane and strip only); use it for moves and hovers. The app sends at most one hover in flight (hover_sent/hover_nextinapp.rs): the pointer's newest cell goes when Neovim has drawn the last one or afterHOVER_WAIT(40 ms), so sweeping across a list is one hover per frame, not one per cell. The pointer never scrolls:utils/mouse.followsets the window'sscrolloffto 0 and keeps its top line (with the config's scrolloff 5 a hovered row near an edge scrolled the list, brought a new row under the still pointer, and the list ran away); Telescope's hover does the same on its results window. - Recent files per project (
lua/recent/,<leader>fo, the start screen's Recent Files, the app's ↺ andCtrl+Shift+R): not Neovim's globaloldfiles.list.luais pure (build(root, { store, oldfiles, open, exists, now, max }): open buffers first, then the store newest first, thenoldfilesthat lie inside the project, never git/node_modules/plugin files, gone files dropped, no repeats),store.luakeepsstdpath("state")/recent-files.json(project →{ path, at }[]; a file is recorded under the git root above it, else under the folder you are in when it is inside it, else its own folder, so opening a file of another project never lands in this one's list; saved 5 s after a change and on exit; a file that is not a table is ignored),init.luarecords on BufEnter/BufWritePost and is the Telescope picker (the project = git root of the working folder;<C-a>toggles every project,<C-x>forgets an entry). Listed buffers count loaded or not.settings.recent.max. Tests:scripts/test-recent.sh. - Browse folders (
lua/gui/browse.lua, "Browse folders…" in the picker's Start group, keyb, File menu and projects menu): Yazi-like onutils/market.lua: the folders of one directory on the left, what the one under the cursor holds on the right (subfolders and files, plus branch/commit/stack for a git project throughpicker.pane),h/←/Backspace up (the cursor lands on the folder you came from),l/→/click in,⏎opens it as the project (picker.enter_dir),onew window,ppin,.hidden,~home,/filter. It starts in the folder above the current project with the project selected. Listings are read once per visit (m.ls). A fresh picker cache must still be DRAWN when the picker opens (market.refreshaftersearch): without it only the recent projects showed. - The hidden cursor has ONE owner list (
utils/cursor.lua): the start screen, the menus, the full-screen lists ("nv": still shown while typing a filter) and the setup callcursor.hide(owner)/cursor.show(owner); the realguicursoris saved when the first owner hides it and restored when the last lets go, a value that already namesNvStartCursoris never taken as the real one, andcursor.repair()(on BufEnter/WinEnter/ModeChanged) puts it right if nothing owns it. Never writevim.o.guicursorfor hiding anywhere else: each page saving and restoring by itself left the cursor invisible when they overlapped (a list over the start screen). - Desktop packaging (
app/package.sh,app/package/,app/assets/,packaging/; details for people inpackaging/README.md): the application id isio.github.sazardev.Leader(Wayland app_id,.desktopfile name,StartupWMClass, AppStream id: keep the four equal,scripts/test-package.shchecks it;platform::APP_ID); the icon name isleader.package/stage-tree.sh BINARY DEST VERSIONis the ONE place that says what gets installed (binary,leader-setupwith@VERSION@, the.desktopfile withExec=leader, hicolor icons, AppStream metadata whose<releases>come from CHANGELOG.md throughmetainfo-releases.py, man pages, license, this version's changelog); the tarball, the.deb,app/install.sh(which stages, then runs the package'sinstall.sh --no-configwithLEADER_LOCAL_BUILD=1) and the Arch PKGBUILDs all call it, so add an installed file THERE.package/install.shcopiesbin/andshare/into a prefix, rewritesExec=/TryExec=to the absolute path, records every file inshare/leader-editor/manifest(uninstall removes exactly those, plus what older versions put down, incl. the oldleader.desktop).package.shbuilds with cargo-zigbuild for glibc 2.31, then writes the tarball, a hand-built.deb(ar + two tarballs, no dpkg-deb;Depends: libc6, git, curl,Recommends: neovim (>= 0.11)),leader-editor-amd64.deb(same file, versionless name for stable links),SHA256SUMSand a detached.ascfor each, signed with the key "Leader Releases" in~/.gnupg-leader-releases(fingerprint8E437A7936E48EAEDA88CD73A7DCC3DBB671476A, pinned inleader-installer.sh, invalidpgpkeysof the -bin PKGBUILD and inpackaging/README.md; no passphrase so a release signs without a prompt; its revocation certificate is in that folder).leader-installer.shrequires a signature made by exactly that key whengpgexists (a forged or missing one aborts; nogpg= checksum only, said out loud;LEADER_VERIFY=0skips). The binary: nonvimon PATH → a notification and exit (a menu launch has no terminal); no Neovim config at all andleader-setupon PATH → it opens the window with+terminal leader-setup(someone else's config is left alone); the window icon is the embedded 128 px RGBA (assets/make-icons.sh: dotted N from 48 px, the solid N below).packaging/arch/{leader-editor-bin,leader-editor}/PKGBUILDandupdate.sh(after a release: version, checksums,.SRCINFO). Screenshots for the site and the store live indocs/screenshots/(taken with a demo HOME of fictional projects, never the real one). Tests:scripts/test-package.sh. - Esc closes whatever is extra (
lua/utils/escape.lua, the global normal-mode<Esc>): one ordered chain: the diagnostic popup and the agent's answer (never focused), then the window you are in when it is extra (classify, pure and tested: a float; a help/quickfix/panel/scratch split; an extra page that has a tab to itself closes the tab; never the last window or tab, never a modified buffer, the file tree only gives the focus back,market/nvdash/leadersetupkeep their own Esc, a float withvim.w.leader_keep = truestays), then focusable floats left elsewhere (hover, preview; non-focusable overlays such as the notifier and keycaps stay), then back-after-gdand:noh. Telescope closes on the FIRST Esc also from insert (defaults.mappings), the setup closes on Esc (free unless an install runs; Backspace goes back a step).PANELSin that file is the list of filetypes that count as panels: add yours there.utils.market.newis atomic: if anything fails while it opens, the windows are closed, the cursor comes back and the error is shown (a page that cannot be closed is never left behind). Tests:scripts/test-escape.sh. - Speed rules for the agent and the UI: streaming answers are redrawn at most once per
settings.agent.stream_ms(40) per job (runner.stream_soon; the final text is always drawn bydone),ndjsonwalks its buffer once, Telescope's flat layout wipes its preview buffer with the window (one buffer used to leak per open), per-event handlers must stay under ~1 ms (BufEnter ≈ 0.8, CursorMoved 0.1, TextChanged ≈ 1 measured). Stress check used for this: open/close each UI 60 times and compare buffers, windows, autocmds and memory (all flat now; open+close takes 5–32 ms). - Updates (
lua/update/,<leader>Su, the bar's badge, the logo's menu, Help › What's New / Check for Updates; knobs insettings.update):semver.luaandchangelog.luaare pure (versions compare as numbers, a pre-release sorts before its release; CHANGELOG.md is split on## [x.y.z] - dateand drawn as lines),source.lualooks GitHub up WITHOUT its API (rawVERSIONof the branch, the rawCHANGELOG.mdonly when a newer version exists, and the redirect ofreleases/latestfor the desktop app's published release; every request has a timeout and fails quietly;settings.update.base/releasespoint elsewhere, tests usefile://),store.luakeepsstdpath("state")/update.json(checked_at, latest, release, the changelog,skipped,announced),targets/<id>.luasay what is installed, what is newest and how to update (config= this clone:prefs.update.run{ done }→git pull --ff-only+Lazy restore;app= theleaderbinary, only inside the app: the release's installer in a terminal; a new target = a file + its id inupdate.TARGETS),init.luaisstatus(pure over the saved answer and the targets),check(async; skipped inside the interval,check_hours= 12, retried after 1 h when it failed; firesUser LeaderUpdateChecked; announces each version ONCE with a notification),skip,apply(a confirmation naming what will happen, targets one after the other, stops at the first that fails, then offers a restart in the app) andsetup(8 s after start, then hourly when due; never updates by itself).panel.luais the full-screen "What's new" (versions newer than yours ★ first, the changelog on the right,uupdate,rcheck now,sskip,oGitHub). The badge is theupdatechip (toolbar.CHIPS, actionupdate) and the logo's menu leads with★ Update to X…. Tests:scripts/test-update.sh`. - Install progress in numbers (
lua/gui/progress.lua): a tracker over a Mason batch (the language tools): per package waiting/running/done/failed, time, bytes in itsmason/staging/<name>folder and a smoothed speed, a "no data for Ns" warning afterSTALL(20 s), done of total, elapsed, a rough time left only after 3 have finished (never precise), a bar. Pure over an injected env (new,poll,finish,summary,describe,lines; tested with a fake Mason),begin_mason/end_masonwire it to the registry andMasonToolsStartingInstall/MasonToolsUpdateCompleted(a 1 s timer polls; started from ANY route: the bar's chip, the setup, the first-run installer). Shown in the app bar (7/9 · codelldb 12.7 MB 31 KB/s · 6m 40s, the progress line fills) and as the setup Install page's info block (a row per tool; Install is disabled and the status line counts while one runs; the page redraws on a 500 ms timer). At the end a notification says how many installed and which did not. - Long background work says how long (
gui/init.luaactivity): a running item keeps its first start (since); after 10 s the bar adds2m 05s(M.elapsed), redrawn every 5 s by a timer that stops when nothing runs, and a gopls item running over 30 s adds that big projects take minutes the first time. Why: gopls's first "loading packages" of a Go project using cgo (go-sqlite3 through gcc) is a cold compile of minutes (go list -compiled=true -deps=true); the Go build cache makes the next load take about a second, so it is not hung. If a load never ends, look for thego listchild of gopls (ps) and itsgccchildren. - Start and window (
lua/gui/startup.lua,settings.gui.window, the settings panel's App groupprefs/items/app.lua(items withwhen: listed only inside the app), the Window menu's submenus (pref_menuingui/init.lua) and the setup's Window step, all writing the same prefs):startup.decideis pure (a folder argument → that project; files → nothing; started inside a git repo that is not ~ → that repo; elseon_start:askthe picker over the start screen (default),last,home; nothing under the first launch's setup, whose Finish opens the picker). Entering goes throughprojects.enter(dir)(whatswitchdoes, also when Neovim is already in that folder). The window:state().window={ size, position, maximized }tells the app what to keep (appbar::Keep); the app tracks the size/place while the window is neither maximized nor fullscreen (note_normal, on Resized/Moved) and writes~/.local/state/leader/windowon exit (platform::Saved: line 1W Hor-, thenpos X Y,max; old one-line files still read); a saved place is used only if it is still on a monitor. Wayland never reports a window's place: the app setsvim.g.leader_gui_can_position = falseand the setup says so. The frame isgui.appbar.enabled(Leader's square bar vs the system's decorations, switched live).oasks the app fornew_window:<dir>. - Mouse (
lua/utils/mouse.lua,lua/gui/mouse.lua): hover is Neovim's own<MouseMove>: the app sends the pointer's moves (nvim_input_mouse("move", ""), once per cell,App::hover_nvim) only whilemousemoveeventis on, which Neovim reports to the UI (option_set). The config turns the option on only while a buffer that asked for hover is focused (mouse.on(buf, { move, click, press, modes })setsb:leader_hover;mouse.syncon BufEnter/WinEnter/FileType): anywhere else a waiting key sequence (which-key's leader menu, an operator) never receives a<MouseMove>, so never add hover to normal editing buffers. Handlers getgetmousepos()and route bywinid(one buffer's mappings serve every window of a menu stack or a market). Wired:gui/menu.lua(hover selects, an item with ▸ opens its submenu without taking the keyboard, click runs),utils/market.lua(every full-screen list: hover selects and refreshes the pane, click reads/folds, the summary's key chips are buttons:m.chipsfrommouse.chipsof the hint row, a click feeds the chip's key into the list or pane),utils/modal.lua(button chips found while rendering,BUTTONS[buf]; the one under the pointer takes the primary color; a click feeds its key, in normal and insert mode for the input modal; the checklist's items follow the pointer and toggle on click throughhooks), the welcome screen, the start screen (ui/start), Telescope's prompt (gui/mouse.telescope, mapped after Telescope's own: hover =set_selection, press on a result = select and open, press outside its windows = close) and nvim-tree (hover follows, one click opens). Clicks work in a terminal too (mouse=a); hover only in the app. The app shows an arrow over floats and an I-beam over text. Tests: stubvim.fn.getmouseposand call the buffer mapping's callback (scripts/test-gui.lua), the protocol intests/embed.rs. - Guided setup (
lua/gui/wizard.lua, when it opens:lua/gui/onboarding.lua; checks:lua/gui/checks.lua; system packages:lua/gui/sysdeps.lua; saved state:lua/gui/state.lua,stdpath("state")/leader-gui.json): its own full-screen float (not a market), one wizard at a time (wizard.current); while it is open the statusline and tabline are hidden and the cursor block too (NvStartCursor), all restored on close. The page has a fixed layout that never scrolls: the steps across the top (✓ done and clickable, the current one lit, ○ ahead; only numbers when the window is narrow), the title and what to do, the choices flowing into up to three columns, and at the bottom a status line (✓ and what was chosen, or ● what is still missing), the buttons (Back on the left, the step's own and Next on the right) and the keys. Each step hasready(w)→ ok, what is missing: Next stays disabled until then (a theme picked; a language or Only the basics; a model, an agent to install or No agent for now; Install done or Skip for now). Drawn on a small canvas (segments by display column,fillpaints a background under text through a column → byte map; every clickable thing leaves a region, the whole cell for a choice; widths must be integers or the extmarks fail). Keys: arrows/hjkl move, Space chooses, Enter chooses then goes on (the primary button in Install/Review), Backspace back, Tab to the buttons, Esc/q leave (free unless an install runs); the mouse throughutils/mouse(hover focuses, click chooses, steps and buttons click). API for tests:open(opts)→w(w.items,w.buttons,w.regions),go,next,choose(w, index),close. Opens by itself only on the first launch (1.2 s after VimEnter); afterwards Ctrl+Shift+W, Help › Welcome and the bar's "to finish" chip open it (on Review once finished). Choices apply as they are made and are saved where the settings panel saves them: theme throughappearance.use(previewed when the focus rests on a theme, 110 ms debounce), packs aslanguages.packs.<id>in prefs/store when you leave the step (they start after a restart: the Review offers "Restart Leader" = the app'srestart:<dir>window command, a new window thenconfirm qall), features throughprefs.cycle(their ids inwizard.FEATURES), the agent throughagent.selection.set. Install:sysdepsis pure (tools with their package per manager,manager()in pacman > apt > dnf > zypper > apk > brew order,command()with sudo unless root/brew and apt's update first,missing()with pack runtimes only for packs that are on); the plan = missing system tools + failing checks that carry afix+ the agents asked for (wizard.AGENT_INSTALL); nothing runs before the Install button; the system part (and agent installers) runs as ONE shell command in a floating terminal over the wizard (zindex 250,jobstart(..., { term = true }), the command printed first, sudo prompts there; it closes itself on success, on failure its title says so and q goes back), then Leader's fixes run and their status comes from re-running the checks every 1.5 s; a 120 ms timer animates the spinners while anything runs. Never run a package manager without the user pressing Install, never hide the command. - Toolbar (
lua/gui/toolbar.lua): the catalogTOOLS(id, side, on by default, icon, optional label, tooltip with its key, action,activefor toggles like the tree) andCHIPS; your on/off choices live in gui/state (toolbar = { id = bool, ["chip.<id>"] = bool }) and win oversettings.gui.appbar.buttons/show.toolbar.customizeis amodal.checklist(groups Left of the path / Right of the path / Information); a right click on the bar sendsaction("customize"). Actions:theme(gui.theme_menu: items withpreviewthat the menu runs on hover/cursor after 90 ms;opts.revertundoes them when the menus close without a choice),agent(gui.agent_menu: available backends as submenus of their models, the current one dotted, then<leader>am, agent setup, the wizard),tree,terminal,problems,settings,customize,palette,home,history,projects. - Colors are blended in sRGB like a terminal (non-sRGB surface when there is one; otherwise the shader converts).
- WSL:
platform::prepare_envfillsWAYLAND_DISPLAY/DISPLAYfrom WSLg and setsGALLIUM_DRIVER=d3d12(the Windows GPU through/dev/dxg; without it Mesa falls back to llvmpipe) before anything starts; WSL has no Vulkan driver, so the backend is GL there (Vulkan + GL elsewhere,WGPU_BACKENDoverrides). WSLg's GL only offersFifopresentation, so a frame waits for vsync there. - Release package (
app/package.sh):cargo zigbuildforx86_64-unknown-linux-gnu.2.31(the binary needs glibc ≤ 2.30 symbols and links only libc/libm/libdl/pthread; Wayland, X11, Vulkan and GL are dlopened), thenapp/dist/leader-linux-x86_64.tar.gz(+.sha256: bin/leader, the menu entry and icon,app/package/install.sh, VERSION, the version's CHANGELOG section, a README) andleader-installer.sh(app/package/leader-installer.sh: downloads the latest release's package, checks the SHA-256, runs its installer). The package's installer is guided and asks nothing in a pipe: checks the machine (Linux x86_64, glibc 2.31+, a desktop or WSLg), installs the app, then Neovim and the config through the maininstall.sh(fetched from master) when they are missing or older than the app (nolua/gui/), and offers to open Leader (its first launch is the welcome screen). Check a package in a cleanubuntu:20.04container (LEADER_RELEASE_URL=file:///dist) before uploading. A release =chore(release): vX.Y.Z(VERSION + both crates' versions + CHANGELOG) + tag, thengh release createwith the package, its checksum, the bootstrap and screenshots (taken with a demo HOME, never one listing private projects), throughGH_CONFIG_DIR=~/.config/gh-personal. - Tests:
scripts/test-app.sh(unit tests +tests/embed.rsagainst a realnvim --embed --clean: windows, floats, resize, Neovim calling the UI back; skipped without cargo).--guialso opens the real window:LEADER_SNAPSHOT=out.pngdraws the frame offscreen to a PNG afterLEADER_SNAPSHOT_MS(default 1500), optionally after typingLEADER_KEYS, then quits; use it to look at a change (LEADER_KEYS=" ff"= Telescope," "= which-key). To try the worktree's config instead of the linked one:XDG_CONFIG_HOME=/tmp/xwith/tmp/x/nvim→ thisnvim/(plugins stay shared). - Not done yet: ligatures, IME composition (dead keys and AltGr work; AltGr that arrives as Ctrl+Alt types its character), smooth scrolling, native cmdline/popupmenu/messages (
ext_*), macOS/Windows builds.