Flemma ships optional integrations for popular Neovim plugins. None create hard dependencies — they're inert unless you wire them into your config.
Add the bundled component to show the active model and thinking level:
require("lualine").setup({
sections = {
lualine_x = {
{ "flemma", icon = "∴" },
"encoding",
"filetype",
},
},
})The component only renders in chat buffers and returns an empty string otherwise.
The display format uses the same Lua template syntax as .chat files ({{ expression }} and {% lua %}) and can be set in two places:
- Lualine section config (per-component, takes precedence):
{ "flemma", format = "{{ provider.name }}:{{ model.name }}" } - Flemma config (global default, via
ui.statusline.formatin the configuration reference)
The lualine option is useful when you include the component multiple times or want to keep all display config in one place. The shipped default leads with a muted provider/ prefix and the model name, then optional thinking level, projected input tokens for the next request as a percentage of the model context window, session stats (request count + cost) once a request lands, and a ⧖ indicator while async tool sources are loading. Muted segments — the provider/ prefix and the · separators — are wrapped with %#FlemmaStatusTextMuted#…%* so they dim without breaking the statusline background. See lua/flemma/config/schema.lua for the literal template.
Leading and trailing ASCII whitespace is trimmed from string formats before rendering. This makes multiline Lua strings convenient:
format = [[
{{ model.name }}
]]Use non-breaking spaces (\u{00a0}) at the edge if intentional leading or trailing spacing should remain visible.
Typical renderings:
anthropic/claude-sonnet-4-6 (high)— thinking active, no requests yetanthropic/claude-sonnet-4-6 (high) · 1% · Σ3 $0.12— buffer estimate and session historyanthropic/claude-sonnet-4-6 ⧖— async tool sources still loading
Variables are lazy-evaluated — only variables referenced by the format string trigger data lookups. Values are raw Lua values where possible; use format.* helpers for display formatting.
Config state:
| Expression | Example | Description |
|---|---|---|
model.name |
claude-sonnet-4-6 |
Current model name |
model.max_input_tokens |
1000000 |
Current model input context limit |
model.max_output_tokens |
64000 |
Current model output limit |
provider.name |
anthropic |
Current provider name |
thinking.enabled |
true |
Whether thinking/reasoning is active |
thinking.level |
high, medium, low |
Thinking/reasoning level (nil when inactive) |
booting |
true |
True while async tool sources are loading |
Session totals (cumulative across all requests in this Neovim session):
| Expression | Example | Description |
|---|---|---|
session.cost |
1.23 |
Total session cost |
session.requests |
5 |
Number of completed requests |
session.tokens.input |
15000 |
Total input tokens |
session.tokens.output |
25000 |
Total output tokens |
Last request:
| Expression | Example | Description |
|---|---|---|
last.cost |
0.38 |
Cost of the most recent request |
last.tokens.input |
10000 |
Input tokens of the most recent request |
last.tokens.output |
5000 |
Output tokens of the most recent request |
Current buffer (projected for the next request):
| Expression | Example | Description |
|---|---|---|
buffer.tokens.input |
8670 |
Projected input tokens for the next request |
The buffer estimate is resolver-driven: referencing buffer.tokens.input in the format string is what installs the subsystem. The default statusline format includes it, so default users get debounced estimates automatically; users with custom formats only get estimates if they include the variable. Fetches run 2.5 s after the user pauses editing and dispatch against the buffer's active provider. Anthropic, OpenAI, Google Vertex AI, and Moonshot adapters implement the underlying try_estimate_usage hook, as does the experimental Codex adapter (which estimates locally via a serialized-payload bytes / 4 heuristic rather than a remote count); other providers silently render the segment empty. Failures clear the cache rather than showing a stale number.
Note
For OpenAI, Flemma uses POST /v1/responses/input_tokens. OpenAI's docs and live probe responses observed during implementation did not expose cost, rate-limit, or quota metadata for that endpoint, so treat each estimate conservatively as a real API request that may count against account limits.
Subscription rate limits (from the most recent request — subscription-based providers only):
| Expression | Example | Description |
|---|---|---|
subscription.plan_name |
Plus |
Plan display name (nil for non-subscription providers) |
subscription.primary.used_percent |
2 |
Primary window usage (0-100), e.g., 5-hour window |
subscription.primary.label |
5h |
Primary window duration as compact label |
subscription.secondary.used_percent |
0 |
Secondary window usage (0-100), e.g., 7-day window |
subscription.secondary.label |
7d |
Secondary window duration as compact label |
subscription.windows |
(table) | Raw ordered array of { used_percent, window_seconds } tables |
Subscription data arrives via HTTP response headers from providers like Codex (ChatGPT subscriptions). The resolvers return nil for non-subscription providers, so conditionals collapse cleanly:
format = [[
%#FlemmaStatusTextMuted#{{ provider.name }}/%*{{ model.name }}
{%- if thinking.enabled then %} ({{ thinking.level }}){% end %}
{%- if subscription.primary.used_percent then %} %#FlemmaStatusTextMuted#·%* %#FlemmaStatusTextMuted#{{ subscription.primary.label }}:%*{{ subscription.primary.used_percent }}%% %#FlemmaStatusTextMuted#{{ subscription.secondary.label }}:%*{{ subscription.secondary.used_percent }}%%{% end %}
{%- if session.cost then %} %#FlemmaStatusTextMuted#·%* Σ{{ session.requests }} {{ format.money(session.cost) }}{% end %}
]]
-- Renders: codex/gpt-5.5 · 5h:2% 7d:0% · Σ3 $0.000Session/request numeric values return nil when no requests have been made, so they work naturally with Lua conditionals.
| Syntax | Purpose | Example |
|---|---|---|
{{ expr }} |
Expression output | {{ model.name }} → o3 |
{% lua %} |
Lua control flow/code | {% if thinking.enabled then %}...{% end %} |
{{-/-}} |
Trim around output | {{- model.name -}} |
{%-/-%} |
Trim around code | {%- if booting then -%} |
| Helper | Example | Description |
|---|---|---|
format.number(session.requests) |
5 |
Comma-separated number |
format.tokens(session.tokens.input) |
15K |
Compact token count |
format.money(session.cost) |
$1.23 |
USD display |
format.percent(buffer.tokens.input / model.max_input_tokens, 1) |
0.9% |
Decimal ratio to percentage |
Vim's raw statusline escapes pass through Flemma's template engine unchanged, so you can mix highlight groups inline:
| Escape | Purpose |
|---|---|
%#GroupName# |
Switch the active highlight to GroupName for following text |
%* |
Restore the statusline's default highlight (see :help statusline) |
%#FlemmaStatusTextMuted# |
Theme-neutral dim fg anchored to StatusLine.bg — provided by Flemma |
When rendered through the bundled lualine component, %* and %#FlemmaStatusTextMuted# are auto-rewritten so they anchor to the active section hl (lualine_c_normal, lualine_c_insert, …) instead of plain StatusLine. This means you can write a format string once and it renders correctly in both raw statusline and lualine contexts:
-- Model name followed by a dimmed divider and session cost
format = "{{ model.name }}{% if session.cost then %} %#FlemmaStatusTextMuted#╱%* {{ format.money(session.cost) }}{% end %}"Any Vim-registered statusline group works (%#Comment#, %#WarningMsg#, …) but most carry their own background, producing a visible step against the statusline. FlemmaStatusTextMuted is deliberately constructed to avoid that.
-- Default: "claude-sonnet-4-5 (high)" or "claude-sonnet-4-5"
format = "{{ model.name }}{% if thinking.enabled then %} ({{ thinking.level }}){% end %}"
-- Provider prefix: "anthropic:claude-sonnet-4-5"
format = "{{ provider.name }}:{{ model.name }}"
-- Square brackets for thinking: "o3 [high]" or "o3"
format = "{{ model.name }}{% if thinking.enabled then %} [{{ thinking.level }}]{% end %}"
-- Provider-conditional label: "A: claude-sonnet-4-5" or "O: o3"
format = "{% if provider.name == 'anthropic' then %}A{% else %}O{% end %}: {{ model.name }}"
-- Running session cost: "claude-sonnet-4-5 $1.23" or "claude-sonnet-4-5"
format = "{{ model.name }}{% if session.cost then %} {{ format.money(session.cost) }}{% end %}"
-- Full dashboard: "claude-sonnet-4-5 (high) $1.23 [5]"
format = "{{ model.name }}{% if thinking.enabled then %} ({{ thinking.level }}){% end %}{% if session.cost then %} {{ format.money(session.cost) }}{% end %}{% if session.requests then %} [{{ session.requests }}]{% end %}"The component only shows data in chat buffers and respects per-buffer overrides from flemma.opt — if frontmatter changes the thinking level, the statusline reflects it.
Show a busy indicator on .chat tabs while a request or tool execution is in-flight:
require("bufferline").setup({
options = {
get_element_icon = require("flemma.integrations.bufferline").get_element_icon,
},
})When a buffer is busy, its tab icon changes to (highlighted with FlemmaBusy, which defaults to DiagnosticWarn). When idle, the icon falls through to nvim-web-devicons as normal.
Pass { icon = "..." } to use a different character:
get_element_icon = require("flemma.integrations.bufferline").get_element_icon({ icon = "+" })The module subscribes to four Flemma lifecycle hooks via the hooks.on API (the same events also fire as the correspondingly-named User autocmds):
| Event | Effect |
|---|---|
FlemmaRequestSending |
Increment busy counter |
FlemmaToolExecuting |
Increment busy counter |
FlemmaRequestFinished |
Decrement busy counter |
FlemmaToolCompleted |
Decrement busy counter |
A buffer shows the busy icon while its counter is above zero. This handles overlapping request and tool lifecycles naturally. When a buffer is wiped, its counter is cleared.
The FlemmaBusy highlight group is configurable via highlights.busy in your Flemma setup:
require("flemma").setup({
highlights = {
busy = "DiagnosticWarn", -- default; a highlight group name or "#RRGGBB" hex string works as shorthand, or pass an HlOp from require("flemma.hl") (e.g. h.link/h.hex) for richer composition
},
}).chat buffers are structurally a sequence of role-delimited messages, not one long Markdown document. nvim-treesitter-context's sticky context window otherwise picks up stray # headings from earlier messages and shows misleading results. Wire Flemma's helper into your treesitter-context config to skip the context window on .chat buffers only:
require("treesitter-context").setup({
on_attach = require("flemma.integrations.nvim-treesitter-context").on_attach,
})If you already have an on_attach callback, compose with .wrap(existing):
require("treesitter-context").setup({
on_attach = require("flemma.integrations.nvim-treesitter-context").wrap(function(bufnr)
-- your existing filter
return vim.bo[bufnr].buftype == ""
end),
})The helper keys off vim.bo[bufnr].filetype == "chat" — it has no dependency on treesitter-context itself and is inert unless you wire it up.
Two integrations need no setup beyond installing the upstream plugin:
rcarriga/nvim-notify — when installed, Flemma routes its notifications through nvim-notify automatically (transient cost/usage messages, error banners, etc.). No config; the integration module in lua/flemma/integrations/nvim-notify.lua is loaded by flemma.notify when the upstream plugin is present.
nvim-tree/nvim-web-devicons — when installed, Flemma registers a .chat filetype icon so file pickers, bufferline tabs, and statusline components show the icon next to chat buffers. Override the glyph via the integrations.devicons config block (see configuration.md for the full schema):
require("flemma").setup({
integrations = {
devicons = {
enabled = true, -- set false to skip registration entirely
icon = "∴", -- default glyph; pick whatever fits your nerd font
},
},
})