Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

neovim/lua/bugabinga/pivi/backfill.lua

Raw
--- Turns a session's recorded history into an ordered list of render ops.
--- Pure: it reads `get_entries` data and never touches a buffer or Pi.
--- The stored shape differs from the live stream, so this is a distinct
--- adapter: tool calls live inside assistant messages and tool results are
--- their own `toolResult` messages, correlated by call id. Reported details
--- survive restoration; a result without details degrades to its own text.

--- The text of a message's content, whether a string or a list of parts.
local text_of = function( content )
  if type( content ) == 'string' then return content end
  if type( content ) ~= 'table' then return '' end
  local pieces = {}
  for _, part in ipairs( content ) do
    if type( part ) == 'table' and part.type == 'text' and type( part.text ) == 'string' then
      table.insert( pieces, part.text )
    end
  end
  return table.concat( pieces, '' )
end

--- A content value as a list of parts.
local parts_of = function( content )
  if type( content ) == 'string' then return { { type = 'text', text = content, }, } end
  if type( content ) ~= 'table' then return {} end
  return content
end

--- Indexes tool results by the call they answer.
local results_by_call = function( entries )
  local results = {}
  for _, item in ipairs( entries ) do
    local message = item.type == 'message' and item.message or nil
    if type( message ) == 'table' and message.role == 'toolResult' and message.toolCallId then
      results[message.toolCallId] = { content = message.content, details = message.details, failed = message.isError == true, }
    end
  end
  return results
end

--- Plans the entries a resumed transcript should rebuild, in order.
--- @param data table a `get_entries` response payload, or its `entries`
--- @return table ops ordered render operations
local plan = function( data )
  local entries = {}
  if type( data ) == 'table' then
    entries = type( data.entries ) == 'table' and data.entries or data
  end
  if type( entries ) ~= 'table' then return {} end

  if type( data ) == 'table' and data.leafId ~= nil then
    if data.leafId == vim.NIL then return {} end
    if type( data.leafId ) ~= 'string' or data.leafId == '' then return {} end
    local by_id = {}
    for _, item in ipairs( entries ) do
      if type( item ) ~= 'table' or type( item.id ) ~= 'string' or item.id == '' or by_id[item.id] then
        return {}
      end
      by_id[item.id] = item
    end
    local path, seen = {}, {}
    local id = data.leafId
    while id ~= nil and id ~= vim.NIL do
      if type( id ) ~= 'string' or seen[id] or not by_id[id] then return {} end
      seen[id] = true
      local item = by_id[id]
      path[#path + 1] = item
      id = item.parentId
    end
    entries = {}
    for index = #path, 1, -1 do entries[#entries + 1] = path[index] end
  else
    for _, item in ipairs( entries ) do
      if type( item ) ~= 'table' or item.id ~= nil or item.parentId ~= nil then return {} end
    end
  end

  local results = results_by_call( entries )
  local ops = {}

  for _, item in ipairs( entries ) do
    local message = item.type == 'message' and item.message or nil
    if type( message ) == 'table' then
      local role = message.role

      if role == 'user' then
        local text = text_of( message.content )
        if vim.trim( text ) ~= '' then table.insert( ops, { kind = 'prompt', text = text, } ) end
      elseif role == 'bashExecution' then
        table.insert( ops, {
          kind = 'tool', tool = message.excludeFromContext and 'bang!' or 'bang',
          args = { command = message.command, },
          result = {
            content = { { type = 'text', text = message.output or '', }, },
            details = { truncation = { truncated = message.truncated == true, }, fullOutputPath = message.fullOutputPath, },
          },
          failed = message.cancelled == true or ( message.exitCode ~= nil and message.exitCode ~= 0 ),
        } )
      elseif role == 'assistant' then
        local pending = {}
        local flush = function()
          local text = table.concat( pending, '' )
          if vim.trim( text ) ~= '' then table.insert( ops, { kind = 'answer', text = text, } ) end
          pending = {}
        end
        for _, part in ipairs( parts_of( message.content ) ) do
          if type( part ) == 'table' then
            if part.type == 'text' and type( part.text ) == 'string' then
              table.insert( pending, part.text )
            elseif ( part.type == 'toolCall' or part.type == 'tool_call' or part.type == 'tool_use' ) and part.id then
              flush()
              local answer = results[part.id] or {}
              table.insert( ops, {
                kind = 'tool',
                tool = part.name,
                args = part.arguments,
                call = part.id,
                result = { content = answer.content, details = answer.details, },
                failed = answer.failed == true,
              } )
            end
          end
        end
        flush()
      end
    end
  end

  return ops
end

return {
  plan = plan,
}