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