How Request Status Tracking Works Internally in ThePrimeagen's 99 Plugin

ThePrimeagen's 99 plugin implements request status tracking through the RequestStatus class in lua/99/ops/request_status.lua, which uses a self-rescheduling vim.defer_fn loop to update a Braille-character spinner, manage a rolling buffer of status messages, and render output either as Neovim virtual text via marks or through a user-provided callback.

The 99 plugin by ThePrimeagen provides live feedback during asynchronous operations like LSP requests. At the heart of this functionality lies a sophisticated request status tracking system that combines visual spinners, message buffering, and Neovim's virtual text API. Understanding this implementation reveals how to build non-blocking status indicators in Lua-based Neovim plugins.

Core Architecture of the RequestStatus Class

The RequestStatus class defined in lua/99/ops/request_status.lua serves as the central coordinator for all status tracking operations. It aggregates three distinct subsystems to create a cohesive user experience.

The Three Core Components

Every RequestStatus instance combines these pieces:

  • StatusLine – A spinning Braille-character indicator that advances on each tick to provide visual feedback that work is in progress.
  • lines buffer – A Lua table holding the most recent status messages, capped by the max_lines parameter to prevent unbounded growth.
  • mark or callback – Either a Mark object from lua/99/ops/marks.lua that updates Neovim virtual text, or a user-provided function that receives the status lines array.

Constructor: Initializing the Tracker

The RequestStatus.new() function (lines 46-58 of request_status.lua) sets up the initial state:

function RequestStatus.new(update_time, max_lines, title_line, mark_or_fn)
  local self = setmetatable({}, RequestStatus)
  self.update_time = update_time                      -- ms between spinner updates
  self.max_lines   = max_lines
  self.status_line = StatusLine.new(title_line)      -- spinner + title
  self.lines       = {}
  self.running     = false
  if type(mark_or_fn) == "function" then
    self.callback = mark_or_fn                       -- user-supplied fn(status_lines)
  else
    self.mark = mark_or_fn                            -- a 99.ops.marks.Mark object
  end
  return self
end

The constructor distinguishes between callback-based and mark-based rendering by checking the type of the fourth argument.

The Status Update Loop

The heart of the request status tracking system is the self-rescheduling timer loop that updates the visual state without blocking Neovim's main thread.

Starting the Spinner with vim.defer_fn

The RequestStatus:start() method (lines 78-96) initiates the update cycle:

function RequestStatus:start()
  local function update_spinner()
    if not self.running then return end

    self.status_line:update()                     -- advance spinner index
    if self.mark then
      self.mark:set_virtual_text(self:get())      -- update virtual text
    end
    if self.callback then
      self.callback(self:get())                   -- invoke user callback
    end
    vim.defer_fn(update_spinner, self.update_time) -- schedule next tick
  end

  self.running = true
  vim.defer_fn(update_spinner, self.update_time)   -- first tick
end

This implementation uses vim.defer_fn to schedule the next iteration, creating a non-blocking loop that terminates automatically when self.running becomes false.

Rendering the Status Output

The RequestStatus:get() method (lines 61-67) assembles the display array:

function RequestStatus:get()
  local result = { self.status_line:to_string() }   -- spinner + title
  for _, line in ipairs(self.lines) do
    table.insert(result, line)                     -- recent status lines
  end
  return result
end

The StatusLine:to_string() method (lines 24-28 of the same file) renders the current Braille character from the braille_chars table combined with the title text.

Pushing New Status Messages

To add messages to the rolling buffer, the RequestStatus:push() method (lines 70-75) maintains the size constraint:

function RequestStatus:push(line)
  table.insert(self.lines, line)
  if #self.lines > self.max_lines - 1 then         -- keep only `max_lines-1` messages
    table.remove(self.lines, 1)                    -- drop the oldest
  end
end

This ensures the status display never exceeds the configured max_lines limit, preventing UI overflow during long-running operations.

Integration with Neovim's Virtual Text

The request status tracking system offers two output mechanisms: direct virtual text attachment via the Mark class, or custom handling through user callbacks.

Using Marks for Buffer Attachment

When initialized with a Mark object (from lua/99/ops/marks.lua), the RequestStatus instance updates virtual text directly on a buffer line. The Mark:set_virtual_text() method receives the string array from RequestStatus:get() and renders it as ephemeral text attached to the buffer coordinates defined during mark creation.

This approach is ideal for showing status inline with code, such as displaying "⠹ Running LSP request" on the line where the request originated.

Callback-Based Custom Rendering

Alternatively, passing a function as the fourth argument to RequestStatus.new() enables callback-driven updates:

local function ui_update(lines)
  -- `lines` is a table like { "⠹ TITLE", "step 1", "step 2" }
  vim.api.nvim_echo({ { table.concat(lines, "\n") } }, false, {})
end

local status = RequestStatus.new(200, 4, "Deploying", ui_update)

This decouples the status tracker from Neovim's virtual text system, allowing integration with external UI plugins, logging systems, or remote monitoring tools.

Practical Implementation Examples

Example 1: Show a Spinner in the Current Buffer

This example demonstrates attaching a status tracker to a specific buffer line using the Mark class:

local RequestStatus = require("99.ops.request_status")
local Mark          = require("99.ops.marks")
local test_utils    = require("99.test.test_utils")   -- helper to obtain a buffer

local buf = test_utils.create_file({ "function foo() end" }, "lua", 1, 1)
local point = require("99.geo").Point:from_1_based(1, 1)
local mark = Mark.mark_point(buf, point)

-- Show a spinner that updates every 150 ms and keeps up to 5 lines
local status = RequestStatus.new(150, 5, "Running LSP request", mark)
status:start()

-- Append status messages as the request progresses
status:push("Sending query…")
status:push("Awaiting response…")
-- …
-- When done:
status:stop()

Example 2: Feed Status to a Custom UI via Callback

For scenarios requiring custom display logic, use the callback interface:

local RequestStatus = require("99.ops.request_status")

local function ui_update(lines)
  -- `lines` is a table like { "⠹ TITLE", "step 1", "step 2" }
  vim.api.nvim_echo({ { table.concat(lines, "\n") } }, false, {})
end

local status = RequestStatus.new(200, 4, "Deploying", ui_update)
status:start()

-- Simulate progress
status:push("Building")
vim.wait(500)               -- wait a bit
status:push("Uploading")
vim.wait(500)
status:stop()

Summary

  • The request status tracking system centers on the RequestStatus class in lua/99/ops/request_status.lua, which orchestrates visual feedback for asynchronous operations.
  • It combines a Braille-character spinner (StatusLine), a rolling message buffer (lines), and either a virtual text mark or user callback for output.
  • The update mechanism relies on vim.defer_fn to create a non-blocking loop that refreshes the display every update_time milliseconds until stop() sets running to false.
  • Message history is automatically capped using max_lines, ensuring the UI remains concise during long-running tasks.

Frequently Asked Questions

How does the RequestStatus class update the spinner animation without blocking Neovim?

The class uses vim.defer_fn to schedule asynchronous updates. When start() is called, it initiates a recursive function that updates the spinner index, renders the current state, and then reschedules itself after update_time milliseconds. This creates a cooperative multitasking loop that yields control back to Neovim between ticks, preventing UI freezing.

What is the difference between using a Mark and a callback in RequestStatus?

When initialized with a Mark object (from lua/99/ops/marks.lua), RequestStatus calls mark:set_virtual_text() on every tick to display status directly on a specific buffer line as ephemeral virtual text. When initialized with a callback function, the class passes the status line array to that function instead, allowing developers to route the data to custom UI components, log files, or external monitoring systems.

How does the status message buffer prevent memory leaks during long operations?

The RequestStatus:push() method enforces a hard limit on stored messages through the max_lines parameter set during construction. When new lines are added via table.insert(), the code checks if the buffer exceeds max_lines - 1 entries. If so, it immediately removes the oldest entry with table.remove(self.lines, 1), ensuring the memory footprint remains constant regardless of operation duration.

Where is the Braille spinner character sequence defined and how is it advanced?

The Braille spinner sequence is managed by the StatusLine class instantiated within RequestStatus. The StatusLine:update() method advances an internal index that cycles through a table of Braille characters (defined as braille_chars). On each tick of the main defer loop, self.status_line:update() is called to advance the spinner, and self.status_line:to_string() renders the current Braille character concatenated with the title line provided during construction.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →