local copy = function( state ) local next_state = vim.deepcopy( state ) return next_state end local initial = function() return { connection = 'starting', run = 'idle', tool = nil, queue = { steering = {}, follow_up = {}, }, compacting = false, retry = nil, error = nil, model = nil, thinking = nil, session = nil, context = nil, exit_code = nil, } end --- Folds one RPC event into cached semantic status. --- Pure: no editor state is read or written here. --- @param state table current status --- @param event table one RPC event --- @return table state the next status local reduce = function( state, event ) if type( event ) ~= 'table' or type( event.type ) ~= 'string' then return state end local next_state = copy( state ) local kind = event.type if kind == 'agent_start' then next_state.connection = 'running' next_state.run = 'running' next_state.error = nil elseif kind == 'agent_end' then next_state.run = event.willRetry and 'retrying' or 'running' elseif kind == 'agent_settled' then next_state.run = 'idle' next_state.tool = nil next_state.retry = nil elseif kind == 'tool_execution_start' then next_state.run = 'running' next_state.tool = { id = event.toolCallId, name = event.toolName, } elseif kind == 'tool_execution_end' then if next_state.tool and next_state.tool.id == event.toolCallId then next_state.tool = nil end elseif kind == 'queue_update' then next_state.queue = { steering = event.steering or {}, follow_up = event.followUp or {}, } elseif kind == 'compaction_start' then next_state.compacting = true elseif kind == 'compaction_end' then next_state.compacting = false if event.errorMessage then next_state.error = event.errorMessage end elseif kind == 'auto_retry_start' then next_state.run = 'retrying' next_state.retry = { attempt = event.attempt, max = event.maxAttempts, delay = event.delayMs, } next_state.error = event.errorMessage elseif kind == 'auto_retry_end' then next_state.retry = nil if event.success then next_state.run = 'running' next_state.error = nil else next_state.run = 'idle' next_state.error = event.finalError end elseif kind == 'extension_error' then next_state.error = event.error elseif kind == 'message_end' then local message = event.message if message and message.errorMessage then next_state.error = message.errorMessage end end return next_state end --- Folds a `get_state` response payload into cached status. local reduce_state = function( state, data ) if type( data ) ~= 'table' then return state end local next_state = copy( state ) next_state.connection = 'running' next_state.model = data.model and data.model.id or next_state.model next_state.thinking = data.thinkingLevel or next_state.thinking next_state.session = { id = data.sessionId, name = data.sessionName, file = data.sessionFile, messages = data.messageCount, } if data.isStreaming ~= nil and next_state.run == 'idle' and data.isStreaming then next_state.run = 'running' end next_state.compacting = data.isCompacting == true return next_state end --- Folds a `get_session_stats` response payload into cached status. local reduce_stats = function( state, data ) if type( data ) ~= 'table' then return state end local next_state = copy( state ) local usage = data.contextUsage if usage then next_state.context = { tokens = usage.tokens, window = usage.contextWindow, percent = usage.percent, } end next_state.cost = data.cost return next_state end local stopped = function( state, exit_code ) local next_state = copy( state ) next_state.connection = 'stopped' next_state.run = 'idle' next_state.tool = nil next_state.exit_code = exit_code return next_state end local is_busy = function( state ) return state.run == 'running' or state.run == 'retrying' or state.compacting == true end local pending_count = function( state ) return #( state.queue.steering or {} ) + #( state.queue.follow_up or {} ) end --- Short human summary suitable for a statusline. local summary = function( state ) if state.connection == 'stopped' then return 'pi:off' end if state.run == 'retrying' then return 'pi:retry' end if state.error then return 'pi:error' end if state.compacting then return 'pi:compacting' end if state.tool then return 'pi:' .. tostring( state.tool.name ) end if state.run == 'running' then return 'pi:run' end local pending = pending_count( state ) if pending > 0 then return 'pi:idle+' .. tostring( pending ) end return 'pi:idle' end return { initial = initial, reduce = reduce, reduce_state = reduce_state, reduce_stats = reduce_stats, stopped = stopped, is_busy = is_busy, pending_count = pending_count, summary = summary, }