621 lines
21 KiB
Lua
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({ { "" } }) }
|
|
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
|