--- 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