-- Inline LLM (ghost-text) completion tuned for writing markdown/prose. -- Reuses ZAI_API_KEY from the environment (same key the :AI command uses). -- Suggestions are virtual text, auto-triggered behind a debounce delay. local PROSE_FT = { markdown = true, ["md-render"] = true, text = true, rst = true, tex = true, gitcommit = true, mail = true, } return { "milanglacier/minuet-ai.nvim", dependencies = { "nvim-lua/plenary.nvim" }, -- Load eagerly: minuet only sets its buffer-local auto-trigger flag from a -- FileType autocmd registered in setup(). Lazy-loading on InsertEnter would -- register that autocmd *after* the current buffer's FileType already fired, -- so virtualtext would never auto-trigger on the buffer you're editing. lazy = false, opts = { provider = "openai_compatible", -- One natural continuation, not three alternatives — prose has a single -- "next sentence", and fewer candidates means less to read past. n_completions = 1, -- Give the model more of the text *before* the cursor (default 0.75). context_ratio = 0.85, -- Cap how much surrounding text is sent. Smaller = lower, steadier latency -- (the full default of 16000 chars is overkill for prose continuation and -- inflates time-to-first-token). 4000 chars ≈ plenty of preceding context. context_window = 4000, -- Delay knobs: wait for an idle pause, and rate-limit requests. Groq is fast -- and deterministic, so we can afford a snappier debounce than the cloud -- defaults; throttle stays moderate to respect the free-tier rate limit. throttle = 800, -- min ms between requests debounce = 400, -- ms of idle typing before a request fires provider_options = { openai_compatible = { api_key = "GROQ_API_KEY", -- env var NAME; minuet reads it at runtime name = "Groq", -- Groq's LPU inference gives ~80-100ms time-to-first-token with very low -- variance — that consistency is the whole reason for moving off Z.AI's -- hit-or-miss coding endpoint. OpenAI-compatible, so the schema is unchanged. end_point = "https://api.groq.com/openai/v1/chat/completions", -- llama-3.3-70b is Groq's flagship general model: strong prose, still fast -- on their hardware. If you ever want the absolute lowest latency, swap to -- "llama-3.1-8b-instant" (faster, slightly weaker prose). model = "llama-3.3-70b-versatile", -- Swap minuet's "code completion engine" system prompt for a prose one -- in writing filetypes; keep the default (string) for code. Guidelines -- and few-shots are left at defaults so minuet's parsing -- still works. system = { prompt = function() if PROSE_FT[vim.bo.filetype] then return [[ You are a prose writing assistant embedded in a text editor. Continue the author's text at the marker naturally and fluently, matching their voice, tone, and sentence rhythm. - Output ONLY the continuation text — no preamble, no explanation. - Never wrap the output in markdown code fences or backticks. - Do not add headings, bullets, or numbering unless the surrounding text already uses them. - Keep it to at most 1-3 sentences and stop at a natural boundary.]] end return require("minuet.config").default_system_prefix_first.prompt end, }, optional = { -- Short, focused completions. (No `thinking` field here — that was a -- Z.AI-specific param; Groq's Llama models 400 on unknown body fields.) max_tokens = 96, temperature = 0.3, }, }, }, virtualtext = { auto_trigger_ft = { "*" }, -- Alt keymaps are kept as a secondary path, but the primary accept key is -- , wired in config() below (Alt+* doesn't fire in Ghostty without -- macos-option-as-alt, and Tab is the natural "accept" key for prose). keymap = { accept = "", -- accept full suggestion accept_line = "", -- accept one line prev = "", next = "", dismiss = "", }, }, }, config = function(_, opts) require("minuet").setup(opts) -- Smart : accept a visible minuet ghost-text suggestion; otherwise fall -- back to the exact super-tab behavior we had before (jump an active snippet, -- else insert a literal Tab). Blink's menu navigates with /, not -- Tab, so this doesn't fight the completion popup. local vt = require("minuet.virtualtext").action vim.keymap.set("i", "", function() if vt.is_visible() then vt.accept() elseif vim.snippet.active({ direction = 1 }) then vim.snippet.jump(1) else vim.api.nvim_feedkeys( vim.api.nvim_replace_termcodes("", true, false, true), "n", false ) end end, { desc = "minuet: accept suggestion, else snippet jump / Tab" }) end, }