repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
neovim/lua/bugabinga/pivi_status.lua
Raw--- Statusline consumer for pivi.
--- It reads the published cache only and never requires pivi, so the
--- statusline keeps working when pivi is absent, unloaded, or broken.
local statusline = require 'bugabinga.statusline'
local auto = require 'std.auto'
local glyph = require 'std.icon'
-- The progress frames the rest of the configuration already animates with.
local SPINNER = {
glyph.progress_0, glyph.progress_1, glyph.progress_2, glyph.progress_3, glyph.progress_4,
glyph.progress_5, glyph.progress_6, glyph.progress_7, glyph.progress_8, glyph.progress_9,
}
local INTERVAL = 90
local state = {
frame = 1,
timer = nil,
}
local sessions = function()
local value = vim.g.pivi
if type( value ) ~= 'table' then return nil, {} end
return value.active, value.sessions or {}
end
local busy = function( entry )
return entry ~= nil
and entry.connection ~= 'stopped'
and ( entry.run == 'running' or entry.run == 'retrying' or entry.compacting == true )
end
local pending = function( entry )
if not entry or type( entry.queue ) ~= 'table' then return 0 end
return #( entry.queue.steering or {} ) + #( entry.queue.follow_up or {} )
end
--- Abbreviates a token count: 88 -> 88, 17000 -> 17k, 2600000 -> 2.6M.
local humanize = function( count )
count = tonumber( count ) or 0
if count >= 1000000 then return ( '%.1fM' ):format( count / 1000000 ) end
if count >= 1000 then return ( '%dk' ):format( math.floor( count / 1000 ) ) end
return tostring( math.floor( count ) )
end
--- The model id without its provider prefix: `openai/gpt-5` -> `gpt-5`.
local short_model = function( id )
if type( id ) ~= 'string' or id == '' then return nil end
return ( id:gsub( '.*/', '' ) )
end
--- Compact, quiet rendering of the richer status pivi now publishes:
--- model and thinking, context fill, token flow, and running cost.
--- Only fields that exist are shown, so an idle session with nothing to
--- report stays the bare marker the previous consumer always drew.
local meta = function( entry )
local parts = {}
local model = short_model( entry.model )
if model then
table.insert( parts, glyph.robot .. ' ' .. model .. ( entry.thinking and ( '·' .. entry.thinking ) or '' ) )
end
if entry.context and entry.context.percent then
table.insert( parts, glyph.gauge .. ' ' .. ( '%d%%' ):format( math.floor( entry.context.percent + 0.5 ) ) )
end
if entry.tokens and ( entry.tokens.input or entry.tokens.output ) then
table.insert( parts, '↑' .. humanize( entry.tokens.input ) .. ' ↓' .. humanize( entry.tokens.output ) )
end
if entry.cost then
table.insert( parts, glyph.dollar .. ( '%.2f' ):format( entry.cost ) )
end
if #parts == 0 then return '' end
return '%#NonText#' .. table.concat( parts, ' ' .. glyph.vertical_bar .. ' ' ) .. '%*'
end
local label = function( entry )
if entry.error then
return '%#ErrorMsg#' .. glyph.error .. ' ' .. ( entry.tool and entry.tool.name or 'error' ) .. '%*'
end
if entry.compacting then return '%#WarningMsg#π ' .. glyph.lazy .. ' compacting%*' end
if entry.run == 'retrying' then return '%#WarningMsg#π ' .. glyph.warn .. ' retry%*' end
if entry.tool then return '%#DiffChange#π ' .. tostring( entry.tool.name ) .. '%*' end
if entry.run == 'running' then return '%#DiffChange#π ' .. glyph.star .. ' thinking%*' end
return 'π'
end
local stop_spinner = function()
if not state.timer then return end
state.timer:stop()
state.timer:close()
state.timer = nil
end
local start_spinner = function()
if state.timer then return end
state.timer = vim.uv.new_timer()
state.timer:start( INTERVAL, INTERVAL, vim.schedule_wrap( function()
state.frame = state.frame % #SPINNER + 1
vim.cmd.redrawstatus()
end ) )
end
--- Cached only: evaluating the statusline never talks to Pi.
local component = function()
local active, all = sessions()
if not active then return '' end
local entry = all[active]
if not entry then return '' end
local pieces = { label( entry ), }
if busy( entry ) then table.insert( pieces, 1, SPINNER[state.frame] ) end
local waiting = pending( entry )
if waiting > 0 then table.insert( pieces, '+' .. tostring( waiting ) ) end
local detail = meta( entry )
if detail ~= '' then table.insert( pieces, detail ) end
return table.concat( pieces, ' ' )
end
statusline.register( component )
auto 'pivi_statusline' {
events = 'User',
pattern = 'PiviStatus',
description = 'Animate the pivi statusline while a run is active.',
command = function()
local active, all = sessions()
local entry = active and all[active] or nil
if busy( entry ) then
start_spinner()
else
stop_spinner()
end
vim.cmd.redrawstatus()
end,
}
auto 'pivi_activity' {
events = 'User',
pattern = 'PiviActivity',
description = 'Announce agent findings that landed in a native list.',
command = function( event )
local data = event.data
if type( data ) ~= 'table' or data.kind ~= 'findings' then return end
local count = tonumber( data.count ) or 0
if count == 0 then return end
vim.notify(
( 'π %d finding%s in the quickfix list' ):format( count, count == 1 and '' or 's' ),
vim.log.levels.INFO
)
end,
}
_G.BugabingaPiviStatus = {
component = component,
busy = busy,
pending = pending,
label = label,
meta = meta,
}
return _G.BugabingaPiviStatus