refactor(history): move message rendering into history module

Move code related to converting history messages into UI representation
from utils into history/render.lua for better code organization and
clean up the implementation.
This commit is contained in:
Dmitry Torokhov
2025-07-15 14:30:00 -07:00
committed by yetone
parent 06d9d8cec3
commit 966eef743b
4 changed files with 177 additions and 153 deletions

View File

@@ -0,0 +1,171 @@
local Helpers = require("avante.history.helpers")
local Line = require("avante.ui.line")
local Utils = require("avante.utils")
local M = {}
---Converts text into format suitable for UI
---@param text string
---@return avante.ui.Line[]
local function text_to_lines(text)
local text_lines = vim.split(text, "\n")
local lines = {}
for _, text_line in ipairs(text_lines) do
table.insert(lines, Line:new({ { text_line } }))
end
return lines
end
---Converts "thinking" item into format suitable for UI
---@param item AvanteLLMMessageContentItem
---@return avante.ui.Line[]
local function thinking_to_lines(item)
local text = item.thinking or item.data or ""
local text_lines = vim.split(text, "\n")
local ui_lines = {}
table.insert(ui_lines, Line:new({ { Utils.icon("🤔 ") .. "Thought content:" } }))
table.insert(ui_lines, Line:new({ { "" } }))
for _, text_line in ipairs(text_lines) do
table.insert(ui_lines, Line:new({ { "> " .. text_line } }))
end
return ui_lines
end
---Converts logs generated by a tool during execution into format suitable for UI
---@param tool_name string
---@param logs string[]
---@return avante.ui.Line[]
local function tool_logs_to_lines(tool_name, logs)
local ui_lines = {}
local num_logs = #logs
for log_idx = 1, num_logs do
local log_lines = vim.split(logs[log_idx]:gsub("^%[" .. tool_name .. "%]: ", "", 1), "\n")
local num_lines = #log_lines
for line_idx = 1, num_lines do
local decoration = (log_idx == num_logs and line_idx == num_lines) and "╰─ " or ""
table.insert(ui_lines, Line:new({ { decoration }, { " " .. log_lines[line_idx] } }))
end
end
return ui_lines
end
local STATE_TO_HL = {
generating = "AvanteStateSpinnerToolCalling",
failed = "AvanteStateSpinnerFailed",
succeeded = "AvanteStateSpinnerSucceeded",
}
---Converts a tool invocation into format suitable for UI
---@param item AvanteLLMMessageContentItem
---@param messages avante.HistoryMessage[]
---@param logs string[]|nil
---@return avante.ui.Line[]
local function tool_to_lines(item, messages, logs)
local lines = {}
local result = Helpers.get_tool_result(item.id, messages)
local state
if not result then
state = "generating"
elseif result.is_error then
state = "failed"
else
state = "succeeded"
end
table.insert(
lines,
Line:new({
{ "╭─ " },
{ " " .. item.name .. " ", STATE_TO_HL[state] },
{ " " .. state },
})
)
if logs then vim.list_extend(lines, tool_logs_to_lines(item.name, logs)) end
return lines
end
---Converts a message item into representation suitable for UI
---@param item AvanteLLMMessageContentItem
---@param message avante.HistoryMessage
---@param messages avante.HistoryMessage[]
---@return avante.ui.Line[]
local function message_content_item_to_lines(item, message, messages)
if type(item) == "string" then
return text_to_lines(item)
elseif type(item) == "table" then
if item.type == "thinking" or item.type == "redacted_thinking" then
return thinking_to_lines(item.thinking or item.data or "")
elseif item.type == "text" then
return text_to_lines(item.text)
elseif item.type == "image" then
return { Line:new({ { "![image](" .. item.source.media_type .. ": " .. item.source.data .. ")" } }) }
elseif item.type == "tool_use" then
local ok, llm_tool = pcall(require, "avante.llm_tools." .. item.name)
if ok then
local tool_result_message = Helpers.get_tool_result_message(message, messages)
---@cast llm_tool AvanteLLMTool
if llm_tool.on_render then
return llm_tool.on_render(item.input, {
logs = message.tool_use_logs,
state = message.state,
store = message.tool_use_store,
result_message = tool_result_message,
})
end
end
return tool_to_lines(item, messages, message.tool_use_logs)
end
end
return {}
end
---Converts a message into representation suitable for UI
---@param message avante.HistoryMessage
---@param messages avante.HistoryMessage[]
---@return avante.ui.Line[]
function M.message_to_lines(message, messages)
if message.displayed_content then return text_to_lines(message.displayed_content) end
local content = message.message.content
if type(content) == "string" then return text_to_lines(content) end
if vim.islist(content) then
local lines = {}
for _, item in ipairs(content) do
local item_lines = message_content_item_to_lines(item, message, messages)
lines = vim.list_extend(lines, item_lines)
end
return lines
end
return {}
end
---Converts a message item into text representation
---@param item AvanteLLMMessageContentItem
---@param message avante.HistoryMessage
---@param messages avante.HistoryMessage[]
---@return string
local function message_content_item_to_text(item, message, messages)
local lines = message_content_item_to_lines(item, message, messages)
return vim.iter(lines):map(function(line) return tostring(line) end):join("\n")
end
---Converts a message into text representation
---@param message avante.HistoryMessage
---@param messages avante.HistoryMessage[]
---@return string
function M.message_to_text(message, messages)
local content = message.message.content
if type(content) == "string" then return content end
if vim.islist(content) then
return vim
.iter(content)
:map(function(item) return message_content_item_to_text(item, message, messages) end)
:filter(function(text) return text ~= "" end)
:join("\n")
end
return ""
end
return M

View File

@@ -46,9 +46,10 @@ function M.summarize_memory(prev_memory, history_messages, cb)
cb(nil)
return
end
local Render = require("avante.history.render")
local conversation_items = vim
.iter(history_messages)
:map(function(msg) return msg.message.role .. ": " .. Utils.message_to_text(msg, history_messages) end)
:map(function(msg) return msg.message.role .. ": " .. Render.message_to_text(msg, history_messages) end)
:totable()
local conversation_text = table.concat(conversation_items, "\n")
local user_prompt = "Here is the conversation so far:\n"

View File

@@ -17,6 +17,7 @@ local RepoMap = require("avante.repo_map")
local FileSelector = require("avante.file_selector")
local LLMTools = require("avante.llm_tools")
local History = require("avante.history")
local Render = require("avante.history.render")
local Line = require("avante.ui.line")
local LRUCache = require("avante.utils.lru_cache")
@@ -1694,7 +1695,7 @@ end
---@return avante.ui.Line[]
local function _get_message_lines(message, messages, ctx)
if message.visible == false then return {} end
local lines = Utils.message_to_lines(message, messages)
local lines = Render.message_to_lines(message, messages)
if message.is_user_submission then
ctx.selected_filepaths = message.selected_filepaths
local text = table.concat(vim.tbl_map(function(line) return tostring(line) end, lines), "\n")
@@ -1793,7 +1794,7 @@ end
---@return string | nil
local function render_message(message, messages, ctx)
if message.visible == false then return nil end
local text = Utils.message_to_text(message, messages)
local text = Render.message_to_text(message, messages)
if text == "" then return nil end
if message.is_user_submission then
ctx.selected_filepaths = message.selected_filepaths
@@ -2070,7 +2071,7 @@ function Sidebar:add_history_messages(messages)
and messages[1].just_for_display ~= true
and messages[1].state == "generated"
then
local first_msg_text = Utils.message_to_text(messages[1], messages)
local first_msg_text = Render.message_to_text(messages[1], messages)
local lines_ = vim.iter(vim.split(first_msg_text, "\n")):filter(function(line) return line ~= "" end):totable()
if #lines_ > 0 then
self.chat_history.title = lines_[1]

View File

@@ -1520,155 +1520,6 @@ function M.is_edit_func_call_tool_use(tool_use)
return is_replace_func_call, is_str_replace_editor_func_call, is_str_replace_based_edit_tool_func_call, path
end
---@param text string
---@param hl string | nil
---@return avante.ui.Line[]
function M.text_to_lines(text, hl)
local Line = require("avante.ui.line")
local text_lines = vim.split(text, "\n")
local lines = {}
for _, text_line in ipairs(text_lines) do
local piece = { text_line }
if hl then table.insert(piece, hl) end
table.insert(lines, Line:new({ piece }))
end
return lines
end
---@param thinking_text string
---@param hl string | nil
---@return avante.ui.Line[]
function M.thinking_to_lines(thinking_text, hl)
local Line = require("avante.ui.line")
local text_lines = vim.split(thinking_text, "\n")
local lines = {}
table.insert(lines, Line:new({ { M.icon("🤔 ") .. "Thought content:" } }))
table.insert(lines, Line:new({ { "" } }))
for _, text_line in ipairs(text_lines) do
local piece = { "> " .. text_line }
if hl then table.insert(piece, hl) end
table.insert(lines, Line:new({ piece }))
end
return lines
end
---@param item AvanteLLMMessageContentItem
---@param message avante.HistoryMessage
---@param messages avante.HistoryMessage[]
---@return avante.ui.Line[]
function M.message_content_item_to_lines(item, message, messages)
local Line = require("avante.ui.line")
local HistoryHelpers = require("avante.history.helpers")
if type(item) == "string" then return M.text_to_lines(item) end
if type(item) == "table" then
if item.type == "thinking" or item.type == "redacted_thinking" then
return M.thinking_to_lines(item.thinking or item.data or "")
end
if item.type == "text" then return M.text_to_lines(item.text) end
if item.type == "image" then
return { Line:new({ { "![image](" .. item.source.media_type .. ": " .. item.source.data .. ")" } }) }
end
if item.type == "tool_use" then
local tool_result_message = HistoryHelpers.get_tool_result_message(message, messages)
local lines = {}
local state = "generating"
local hl = "AvanteStateSpinnerToolCalling"
local ok, llm_tool = pcall(require, "avante.llm_tools." .. item.name)
if ok then
---@cast llm_tool AvanteLLMTool
if llm_tool.on_render then
return llm_tool.on_render(item.input, {
logs = message.tool_use_logs,
state = message.state,
store = message.tool_use_store,
result_message = tool_result_message,
})
end
end
if tool_result_message then
local tool_result = tool_result_message.message.content[1]
if tool_result.is_error then
state = "failed"
hl = "AvanteStateSpinnerFailed"
else
state = "succeeded"
hl = "AvanteStateSpinnerSucceeded"
end
end
table.insert(
lines,
Line:new({ { "╭─" }, { " " }, { string.format(" %s ", item.name), hl }, { string.format(" %s", state) } })
)
if message.tool_use_logs then
for idx, log in ipairs(message.tool_use_logs) do
local log_ = M.trim(log, { prefix = string.format("[%s]: ", item.name) })
local lines_ = vim.split(log_, "\n")
if idx ~= #(message.tool_use_logs or {}) then
for _, line_ in ipairs(lines_) do
table.insert(lines, Line:new({ { "" }, { string.format(" %s", line_) } }))
end
else
for idx_, line_ in ipairs(lines_) do
if idx_ ~= #lines_ then
table.insert(lines, Line:new({ { "" }, { string.format(" %s", line_) } }))
else
table.insert(lines, Line:new({ { "╰─" }, { string.format(" %s", line_) } }))
end
end
end
end
end
return lines
end
end
return {}
end
---@param message avante.HistoryMessage
---@param messages avante.HistoryMessage[]
---@return avante.ui.Line[]
function M.message_to_lines(message, messages)
if message.displayed_content then return M.text_to_lines(message.displayed_content) end
local content = message.message.content
if type(content) == "string" then return M.text_to_lines(content) end
if vim.islist(content) then
local lines = {}
for _, item in ipairs(content) do
local lines_ = M.message_content_item_to_lines(item, message, messages)
lines = vim.list_extend(lines, lines_)
end
return lines
end
return {}
end
---@param item AvanteLLMMessageContentItem
---@param message avante.HistoryMessage
---@param messages avante.HistoryMessage[]
---@return string
function M.message_content_item_to_text(item, message, messages)
local lines = M.message_content_item_to_lines(item, message, messages)
if #lines == 0 then return "" end
return table.concat(vim.tbl_map(function(line) return tostring(line) end, lines), "\n")
end
---@param message avante.HistoryMessage
---@param messages avante.HistoryMessage[]
---@return string
function M.message_to_text(message, messages)
local content = message.message.content
if type(content) == "string" then return content end
if vim.islist(content) then
local pieces = {}
for _, item in ipairs(content) do
local text = M.message_content_item_to_text(item, message, messages)
if text ~= "" then table.insert(pieces, text) end
end
return table.concat(pieces, "\n")
end
return ""
end
---Counts number of strings in text, accounting for possibility of a trailing newline
---@param str string | nil
---@return integer