Files
avante.nvim/lua/avante/history/render.lua
cndoit18 44b594863c fix(history): improve parameter display in get_tool_display_name function (#2822)
Use vim.inspect() for better parameter formatting instead of direct string concatenation
2025-11-06 05:23:30 +08:00

621 lines
21 KiB
Lua

local Helpers = require("avante.history.helpers")
local Line = require("avante.ui.line")
local Utils = require("avante.utils")
local Highlights = require("avante.highlights")
local M = {}
---@diagnostic disable-next-line: deprecated
local islist = vim.islist or vim.tbl_islist
---Converts text into format suitable for UI
---@param text string
---@param decoration string | nil
---@param filter? fun(text_line: string, idx: integer, len: integer): boolean
---@return avante.ui.Line[]
local function text_to_lines(text, decoration, filter)
local text_lines = vim.split(text, "\n")
local lines = {}
for idx, text_line in ipairs(text_lines) do
if filter and not filter(text_line, idx, #text_lines) then goto continue end
if decoration then
table.insert(lines, Line:new({ { decoration }, { text_line } }))
else
table.insert(lines, Line:new({ { text_line } }))
end
::continue::
end
return lines
end
---Converts text into format suitable for UI
---@param text string
---@param decoration string | nil
---@param truncate boolean | nil
---@return avante.ui.Line[]
local function text_to_truncated_lines(text, decoration, truncate)
local text_lines = vim.split(text, "\n")
local lines = {}
for _, text_line in ipairs(text_lines) do
if truncate and #lines > 3 then
table.insert(
lines,
Line:new({
{ decoration },
{
string.format("... (Result truncated, remaining %d lines not shown)", #text_lines - #lines + 1),
Highlights.AVANTE_COMMENT_FG,
},
})
)
break
end
table.insert(lines, Line:new({ { decoration }, { text_line } }))
end
return lines
end
---@param lines avante.ui.Line[]
---@param decoration string | nil
---@param truncate boolean | nil
---@return avante.ui.Line[]
local function lines_to_truncated_lines(lines, decoration, truncate)
local truncated_lines = {}
for idx, line in ipairs(lines) do
if truncate and #truncated_lines > 3 then
table.insert(
truncated_lines,
Line:new({
{ decoration },
{
string.format("... (Result truncated, remaining %d lines not shown)", #lines - idx + 1),
Highlights.AVANTE_COMMENT_FG,
},
})
)
break
end
table.insert(truncated_lines, line)
end
return truncated_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")
--- trim prefix empty lines
while #text_lines > 0 and text_lines[1] == "" do
table.remove(text_lines, 1)
end
--- trim suffix empty lines
while #text_lines > 0 and text_lines[#text_lines] == "" do
table.remove(text_lines, #text_lines)
end
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[]
function M.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 = ""
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",
}
function M.get_diff_lines(old_str, new_str, decoration, truncate)
local lines = {}
local line_count = 0
local old_lines = vim.split(old_str, "\n")
local new_lines = vim.split(new_str, "\n")
---@diagnostic disable-next-line: assign-type-mismatch, missing-fields
local patch = vim.diff(old_str, new_str, { ---@type integer[][]
algorithm = "histogram",
result_type = "indices",
ctxlen = vim.o.scrolloff,
})
local prev_start_a = 0
local truncated_lines = 0
for _, hunk in ipairs(patch) do
local start_a, count_a, start_b, count_b = unpack(hunk)
local no_change_lines = vim.list_slice(old_lines, prev_start_a, start_a - 1)
if truncate then
local last_three_no_change_lines = vim.list_slice(no_change_lines, #no_change_lines - 3)
truncated_lines = truncated_lines + #no_change_lines - #last_three_no_change_lines
if #no_change_lines > 4 then
table.insert(lines, Line:new({ { decoration }, { "...", Highlights.AVANTE_COMMENT_FG } }))
end
no_change_lines = last_three_no_change_lines
end
for idx, line in ipairs(no_change_lines) do
if truncate and line_count > 10 then
truncated_lines = truncated_lines + #no_change_lines - idx
break
end
line_count = line_count + 1
table.insert(lines, Line:new({ { decoration }, { line } }))
end
prev_start_a = start_a + count_a
if count_a > 0 then
local delete_lines = vim.list_slice(old_lines, start_a, start_a + count_a - 1)
for idx, line in ipairs(delete_lines) do
if truncate and line_count > 10 then
truncated_lines = truncated_lines + #delete_lines - idx
break
end
line_count = line_count + 1
table.insert(lines, Line:new({ { decoration }, { line, Highlights.TO_BE_DELETED_WITHOUT_STRIKETHROUGH } }))
end
end
if count_b > 0 then
local create_lines = vim.list_slice(new_lines, start_b, start_b + count_b - 1)
for idx, line in ipairs(create_lines) do
if truncate and line_count > 10 then
truncated_lines = truncated_lines + #create_lines - idx
break
end
line_count = line_count + 1
table.insert(lines, Line:new({ { decoration }, { line, Highlights.INCOMING } }))
end
end
end
if prev_start_a < #old_lines then
-- Append remaining old_lines
local no_change_lines = vim.list_slice(old_lines, prev_start_a, #old_lines)
local first_three_no_change_lines = vim.list_slice(no_change_lines, 1, 3)
for idx, line in ipairs(first_three_no_change_lines) do
if truncate and line_count > 10 then
truncated_lines = truncated_lines + #first_three_no_change_lines - idx
break
end
line_count = line_count + 1
table.insert(lines, Line:new({ { decoration }, { line } }))
end
end
if truncate and truncated_lines > 0 then
table.insert(
lines,
Line:new({
{ decoration },
{
string.format("... (Result truncated, remaining %d lines not shown)", truncated_lines),
Highlights.AVANTE_COMMENT_FG,
},
})
)
end
return lines
end
---@param content any
---@param decoration string | nil
---@param truncate boolean | nil
function M.get_content_lines(content, decoration, truncate)
local lines = {}
local content_obj = content
if type(content) == "string" then
local ok, content_obj_ = pcall(vim.json.decode, content)
if ok then content_obj = content_obj_ end
end
if type(content_obj) == "table" then
if islist(content_obj) then
local all_lines = {}
for _, content_item in ipairs(content_obj) do
if type(content_item) == "string" then
local lines_ = text_to_lines(content_item, decoration)
all_lines = vim.list_extend(all_lines, lines_)
end
end
local lines_ = lines_to_truncated_lines(all_lines, decoration, truncate)
lines = vim.list_extend(lines, lines_)
end
if type(content_obj.content) == "string" then
local lines_ = text_to_truncated_lines(content_obj.content, decoration, truncate)
lines = vim.list_extend(lines, lines_)
end
if islist(content_obj.content) then
local all_lines = {}
for _, content_item in ipairs(content_obj.content) do
if type(content_item) == "string" then
local lines_ = text_to_lines(content_item, decoration)
all_lines = vim.list_extend(all_lines, lines_)
end
end
local lines_ = lines_to_truncated_lines(all_lines, decoration, truncate)
lines = vim.list_extend(lines, lines_)
end
if islist(content_obj.matches) then
local all_lines = {}
for _, content_item in ipairs(content_obj.matches) do
if type(content_item) == "string" then
local lines_ = text_to_lines(content_item, decoration)
all_lines = vim.list_extend(all_lines, lines_)
end
end
local lines_ = lines_to_truncated_lines(all_lines, decoration, truncate)
lines = vim.list_extend(lines, lines_)
end
end
if type(content_obj) == "string" then
local lines_ = text_to_lines(content_obj, decoration)
local line_count = 0
for idx, line in ipairs(lines_) do
if truncate and line_count > 3 then
table.insert(
lines,
Line:new({
{ decoration },
{
string.format("... (Result truncated, remaining %d lines not shown)", #lines_ - idx + 1),
Highlights.AVANTE_COMMENT_FG,
},
})
)
break
end
line_count = line_count + 1
table.insert(lines, line)
end
end
if type(content_obj) == "number" then
table.insert(lines, Line:new({ { decoration }, { tostring(content_obj) } }))
end
if islist(content) then
for _, content_item in ipairs(content) do
local line_count = 0
if content_item.type == "content" then
if content_item.content.type == "text" then
local lines_ = text_to_lines(content_item.content.text, decoration, function(text_line, idx, len)
if idx == 1 and text_line:match("^%s*```%s*$") then return false end
if idx == len and text_line:match("^%s*```%s*$") then return false end
return true
end)
for idx, line in ipairs(lines_) do
if truncate and line_count > 3 then
table.insert(
lines,
Line:new({
{ decoration },
{
string.format("... (Result truncated, remaining %d lines not shown)", #lines_ - idx + 1),
Highlights.AVANTE_COMMENT_FG,
},
})
)
break
end
line_count = line_count + 1
table.insert(lines, line)
end
end
elseif
content_item.type == "diff"
and content_item.oldText ~= nil
and content_item.newText ~= nil
and content_item.oldText ~= vim.NIL
and content_item.newText ~= vim.NIL
then
local relative_path = Utils.relative_path(content_item.path)
table.insert(lines, Line:new({ { decoration }, { "Path: " .. relative_path } }))
local lines_ = M.get_diff_lines(content_item.oldText, content_item.newText, decoration, truncate)
lines = vim.list_extend(lines, lines_)
end
end
end
return lines
end
---@param message avante.HistoryMessage
---@return string tool_name
---@return string | nil error
function M.get_tool_display_name(message)
local content = message.message.content
if type(content) ~= "table" then return "", "expected message content to be a table" end
---@cast content AvanteLLMMessageContentItem[]
if not islist(content) then return "", "expected message content to be a list" end
local item = message.message.content[1]
local native_tool_name = item.name
if native_tool_name == "other" and message.acp_tool_call then
native_tool_name = message.acp_tool_call.title or "Other"
end
if message.acp_tool_call and message.acp_tool_call.title then native_tool_name = message.acp_tool_call.title end
local tool_name = native_tool_name
if message.displayed_tool_name then
tool_name = message.displayed_tool_name
else
local param
if item.input and type(item.input) == "table" then
local path
if type(item.input.path) == "string" then path = item.input.path end
if type(item.input.rel_path) == "string" then path = item.input.rel_path end
if type(item.input.filepath) == "string" then path = item.input.filepath end
if type(item.input.file_path) == "string" then path = item.input.file_path end
if type(item.input.query) == "string" then param = item.input.query end
if type(item.input.pattern) == "string" then param = item.input.pattern end
if type(item.input.command) == "string" then
param = item.input.command
local pieces = vim.split(param, "\n")
if #pieces > 1 then param = pieces[1] .. "..." end
end
if native_tool_name == "execute" and not param then
if message.acp_tool_call and message.acp_tool_call.title then param = message.acp_tool_call.title end
end
if not param and path then
local relative_path = Utils.relative_path(path)
param = relative_path
end
end
if not param and message.acp_tool_call then
if message.acp_tool_call.locations then
for _, location in ipairs(message.acp_tool_call.locations) do
if location.path then
local relative_path = Utils.relative_path(location.path)
param = relative_path
break
end
end
end
end
if
not param
and message.acp_tool_call
and message.acp_tool_call.rawInput
and message.acp_tool_call.rawInput.command
then
param = message.acp_tool_call.rawInput.command
pcall(function()
local project_root = Utils.root.get()
param = param:gsub(project_root .. "/?", "")
end)
end
if param then tool_name = native_tool_name .. "(" .. vim.inspect(param) .. ")" end
end
---@cast tool_name string
return tool_name, nil
end
---Converts a tool invocation into format suitable for UI
---@param item AvanteLLMMessageContentItem
---@param message avante.HistoryMessage
---@param messages avante.HistoryMessage[]
---@param expanded boolean | nil
---@return avante.ui.Line[]
local function tool_to_lines(item, message, messages, expanded)
-- local logs = message.tool_use_logs
local lines = {}
local tool_name, error = M.get_tool_display_name(message)
if error then
table.insert(lines, Line:new({ { "" }, { error } }))
return lines
end
local rest_input_text_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({
{ "╭─ " },
{ " " .. tool_name .. " ", STATE_TO_HL[state] },
{ " " .. state },
})
)
-- if logs then vim.list_extend(lines, tool_logs_to_lines(item.name, logs)) end
local decoration = ""
if rest_input_text_lines and #rest_input_text_lines > 0 then
local lines_ = text_to_lines(table.concat(rest_input_text_lines, "\n"), decoration)
local line_count = 0
for idx, line in ipairs(lines_) do
if not expanded and line_count > 3 then
table.insert(
lines,
Line:new({
{ decoration },
{
string.format("... (Input truncated, remaining %d lines not shown)", #lines_ - idx + 1),
Highlights.AVANTE_COMMENT_FG,
},
})
)
break
end
line_count = line_count + 1
table.insert(lines, line)
end
table.insert(lines, Line:new({ { decoration }, { "" } }))
end
local add_diff_lines = false
if item.input and type(item.input) == "table" then
if type(item.input.old_str) == "string" and type(item.input.new_str) == "string" then
local diff_lines = M.get_diff_lines(item.input.old_str, item.input.new_str, decoration, not expanded)
add_diff_lines = true
vim.list_extend(lines, diff_lines)
end
end
if
not add_diff_lines
and message.acp_tool_call
and message.acp_tool_call.rawInput
and message.acp_tool_call.rawInput.oldString
then
local diff_lines = M.get_diff_lines(
message.acp_tool_call.rawInput.oldString,
message.acp_tool_call.rawInput.newString,
decoration,
not expanded
)
vim.list_extend(lines, diff_lines)
end
if
message.acp_tool_call
and message.acp_tool_call.rawOutput
and message.acp_tool_call.rawOutput.metadata
and message.acp_tool_call.rawOutput.metadata.preview
then
local preview = message.acp_tool_call.rawOutput.metadata.preview
if preview then
local content_lines = M.get_content_lines(preview, decoration, not expanded)
vim.list_extend(lines, content_lines)
end
else
if message.acp_tool_call and message.acp_tool_call.content then
local content = message.acp_tool_call.content
if content then
local content_lines = M.get_content_lines(content, decoration, not expanded)
vim.list_extend(lines, content_lines)
end
else
if result and result.content then
local result_content = result.content
if result_content then
local content_lines = M.get_content_lines(result_content, decoration, not expanded)
vim.list_extend(lines, content_lines)
end
end
end
end
if #lines <= 1 then
if state == "generating" then
table.insert(lines, Line:new({ { decoration }, { "...", Highlights.AVANTE_COMMENT_FG } }))
else
table.insert(lines, Line:new({ { decoration }, { "completed" } }))
end
end
--- remove last empty lines
while #lines > 0 and lines[#lines].sections[2] and lines[#lines].sections[2][1] == "" do
table.remove(lines, #lines)
end
local last_line = lines[#lines]
last_line.sections[1][1] = "╰─ "
return lines
end
---Converts a message item into representation suitable for UI
---@param item AvanteLLMMessageContentItem
---@param message avante.HistoryMessage
---@param messages avante.HistoryMessage[]
---@param expanded boolean | nil
---@return avante.ui.Line[]
local function message_content_item_to_lines(item, message, messages, expanded)
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)
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" and item.name then
local ok, llm_tool = pcall(require, "avante.llm_tools." .. item.name)
if ok then
local tool_result_message = Helpers.get_tool_result_message(item.id, 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
local lines = tool_to_lines(item, message, messages, expanded)
if message.tool_use_log_lines then lines = vim.list_extend(lines, message.tool_use_log_lines) end
return lines
end
end
return {}
end
---Converts a message into representation suitable for UI
---@param message avante.HistoryMessage
---@param messages avante.HistoryMessage[]
---@param expanded boolean | nil
---@return avante.ui.Line[]
function M.message_to_lines(message, messages, expanded)
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 islist(content) then
local lines = {}
for _, item in ipairs(content) do
local item_lines = message_content_item_to_lines(item, message, messages, expanded)
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 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