From 966eef743bbf8e9ef4a2664747dbfbd093c2355c Mon Sep 17 00:00:00 2001 From: Dmitry Torokhov Date: Tue, 15 Jul 2025 14:30:00 -0700 Subject: [PATCH] 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. --- lua/avante/history/render.lua | 171 ++++++++++++++++++++++++++++++++++ lua/avante/llm.lua | 3 +- lua/avante/sidebar.lua | 7 +- lua/avante/utils/init.lua | 149 ----------------------------- 4 files changed, 177 insertions(+), 153 deletions(-) create mode 100644 lua/avante/history/render.lua diff --git a/lua/avante/history/render.lua b/lua/avante/history/render.lua new file mode 100644 index 0000000..76b5e02 --- /dev/null +++ b/lua/avante/history/render.lua @@ -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 diff --git a/lua/avante/llm.lua b/lua/avante/llm.lua index e291aa5..4378dbf 100644 --- a/lua/avante/llm.lua +++ b/lua/avante/llm.lua @@ -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" diff --git a/lua/avante/sidebar.lua b/lua/avante/sidebar.lua index e709469..7ca1617 100644 --- a/lua/avante/sidebar.lua +++ b/lua/avante/sidebar.lua @@ -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] diff --git a/lua/avante/utils/init.lua b/lua/avante/utils/init.lua index 42644b5..6e6bfee 100644 --- a/lua/avante/utils/init.lua +++ b/lua/avante/utils/init.lua @@ -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