From 7004e4be929b3a627f86f92f13e336c41761d336 Mon Sep 17 00:00:00 2001 From: Meisterlala <6453306+Meisterlala@users.noreply.github.com> Date: Sun, 7 Jun 2026 18:50:12 +0200 Subject: [PATCH] feat(ai-commit): streamline cold starts and enhance UI stream wrapping Remove unnecessary dummy preloads to reduce API overhead; rely on cache health checks before loading models. Add dynamic text line wrapping in spinner output based on terminal width and support explicit request start notifications via context callbacks. --- local/ai-commit/lua/ai-commit/generator.lua | 5 + local/ai-commit/lua/ai-commit/providers.lua | 6 ++ local/ai-commit/lua/ai-commit/spinner.lua | 100 +++++++++++++++++- local/ai-provider/lua/ai-provider/init.lua | 2 +- .../lua/ai-provider/providers/ollama.lua | 34 ++---- 5 files changed, 117 insertions(+), 30 deletions(-) diff --git a/local/ai-commit/lua/ai-commit/generator.lua b/local/ai-commit/lua/ai-commit/generator.lua index 8079f50..a8149d9 100644 --- a/local/ai-commit/lua/ai-commit/generator.lua +++ b/local/ai-commit/lua/ai-commit/generator.lua @@ -135,6 +135,11 @@ function M.insert() table.insert(http_jobs, job) end end, + on_request_start = function() + if not done and not aborted then + spinner_ui.start_stream_section(spinner) + end + end, on_chunk = function(chunk) if not done and not aborted then spinner_ui.append_stream(spinner, chunk) diff --git a/local/ai-commit/lua/ai-commit/providers.lua b/local/ai-commit/lua/ai-commit/providers.lua index df7a8f5..634ff5c 100644 --- a/local/ai-commit/lua/ai-commit/providers.lua +++ b/local/ai-commit/lua/ai-commit/providers.lua @@ -125,6 +125,8 @@ local function complete_ai_provider(source_id, full_prompt, callback, status_cal status_callback(action and (action .. ' with ' .. status_model) or ('Loading model ' .. status_model)) elseif status.phase == 'loaded' then status_callback('Loaded model ' .. status_model) + elseif status.phase == 'context' then + status_callback('Loading prompt context with ' .. status_model) elseif status.phase == 'thinking' then local suffix = status.tokens_per_second and string.format(' (%.1f t/s)', status.tokens_per_second) or '' status_callback((action or 'Thinking') .. ' with ' .. status_model .. suffix) @@ -136,6 +138,10 @@ local function complete_ai_provider(source_id, full_prompt, callback, status_cal end end + if request_context and request_context.on_request_start then + request_context.on_request_start(source_id, model) + end + ai_provider.chat(provider, { source_id = source_id, model = model, diff --git a/local/ai-commit/lua/ai-commit/spinner.lua b/local/ai-commit/lua/ai-commit/spinner.lua index 15c0d87..362c920 100644 --- a/local/ai-commit/lua/ai-commit/spinner.lua +++ b/local/ai-commit/lua/ai-commit/spinner.lua @@ -6,6 +6,72 @@ local M = {} local frames = { '⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏' } +local function preview_width(bufnr) + local win = vim.fn.bufwinid(bufnr) + if win ~= -1 then + return math.max(20, vim.api.nvim_win_get_width(win) - 4) + end + return 76 +end + +local function split_long_token(token, width) + local parts = {} + local current = '' + local index = 0 + local length = vim.fn.strchars(token) + + while index < length do + local char = vim.fn.strcharpart(token, index, 1) + if current ~= '' and vim.fn.strdisplaywidth(current .. char) > width then + table.insert(parts, current) + current = char + else + current = current .. char + end + index = index + 1 + end + + if current ~= '' then + table.insert(parts, current) + end + return parts +end + +local function wrap_line(line, width) + if line == '' or vim.fn.strdisplaywidth(line) <= width then + return { line } + end + + local wrapped = {} + local current = '' + + for token in line:gmatch '%S+%s*' do + local candidate = current .. token + if current ~= '' and vim.fn.strdisplaywidth(candidate) > width then + table.insert(wrapped, (current:gsub('%s+$', ''))) + current = token + else + current = candidate + end + + if vim.fn.strdisplaywidth(current) > width then + local parts = split_long_token(current:gsub('%s+$', ''), width) + for index, part in ipairs(parts) do + if index < #parts then + table.insert(wrapped, part) + else + current = part + end + end + end + end + + if current ~= '' then + table.insert(wrapped, (current:gsub('%s+$', ''))) + end + return #wrapped > 0 and wrapped or { line } +end + local function stop_timer_safe(spinner) local timer = spinner and spinner.timer if not timer then @@ -25,13 +91,26 @@ local function stop_timer_safe(spinner) end ---@param spinner table +---@param bufnr integer ---@return table -local function preview_virt_lines(spinner) +local function preview_virt_lines(spinner, bufnr) local lines = {} + local width = preview_width(bufnr) for _, line in ipairs(spinner.stream_preview) do - table.insert(lines, { { line, 'Comment' } }) + for _, wrapped in ipairs(wrap_line(line, width)) do + table.insert(lines, wrapped) + end + end + + while #lines > (config.values.preview_lines or 5) do + table.remove(lines, 1) end - return lines + + local virt_lines = {} + for _, line in ipairs(lines) do + table.insert(virt_lines, { { line, 'Comment' } }) + end + return virt_lines end ---@param bufnr integer @@ -77,7 +156,7 @@ function M.start(bufnr) virt_text = virt_text, virt_text_pos = 'eol', } - local virt_lines = preview_virt_lines(spinner) + local virt_lines = preview_virt_lines(spinner, bufnr) if #virt_lines > 0 then opts.virt_lines = virt_lines opts.virt_lines_above = false @@ -113,11 +192,22 @@ function M.append_stream(spinner, text) spinner.stream_preview[#spinner.stream_preview] = (spinner.stream_preview[#spinner.stream_preview] or '') .. chunk end - while #spinner.stream_preview > (config.values.preview_lines or 5) do + while #spinner.stream_preview > 200 do table.remove(spinner.stream_preview, 1) end end +---@param spinner table|nil +function M.start_stream_section(spinner) + if not spinner or #spinner.stream_preview == 0 then + return + end + + if spinner.stream_preview[#spinner.stream_preview] ~= '' then + table.insert(spinner.stream_preview, '') + end +end + ---@param spinner table|nil ---@param status_text string ---@param status_chunks table|nil diff --git a/local/ai-provider/lua/ai-provider/init.lua b/local/ai-provider/lua/ai-provider/init.lua index 1091783..a28607c 100644 --- a/local/ai-provider/lua/ai-provider/init.lua +++ b/local/ai-provider/lua/ai-provider/init.lua @@ -44,7 +44,7 @@ ---@class AiProviderStatus ---@field provider string Provider name. ----@field phase string Standard phase, for example `loading`, `loaded`, `thinking`, `generating`, `done`, or `error`. +---@field phase string Standard phase, for example `loading`, `loaded`, `context`, `thinking`, `generating`, `done`, or `error`. ---@field message string Human-readable status message. ---@field model? string Model/profile name. ---@field used_model? string Raw provider model name. diff --git a/local/ai-provider/lua/ai-provider/providers/ollama.lua b/local/ai-provider/lua/ai-provider/providers/ollama.lua index 8b7b60d..31f2c32 100644 --- a/local/ai-provider/lua/ai-provider/providers/ollama.lua +++ b/local/ai-provider/lua/ai-provider/providers/ollama.lua @@ -295,14 +295,7 @@ function M.chat(request) end if code ~= 0 then - log.error( - 'ollama request process failed code=' - .. tostring(code) - .. ' model=' - .. tostring(raw_model) - .. ' error=' - .. tostring(error_message) - ) + log.error('ollama request process failed code=' .. tostring(code) .. ' model=' .. tostring(raw_model) .. ' error=' .. tostring(error_message)) if request.callback then request.callback(nil, { requested_model = selected_model, @@ -354,12 +347,12 @@ function M.chat(request) if request.callback then request.callback(nil, meta) end - emit_status { - phase = 'error', - message = provider_error, - tokens = metrics.eval_count, - tokens_per_second = tokens_per_second(metrics.eval_count, metrics.eval_duration), - } + emit_status { + phase = 'error', + message = provider_error, + tokens = metrics.eval_count, + tokens_per_second = tokens_per_second(metrics.eval_count, metrics.eval_duration), + } return end if done_reason == 'length' then @@ -467,14 +460,7 @@ function M.chat(request) if response.status ~= 200 then local error_message = response.error or response.body or 'ollama preload failed' - log.error( - 'ollama preload failed status=' - .. tostring(response.status) - .. ' model=' - .. tostring(raw_model) - .. ' error=' - .. tostring(error_message) - ) + log.error('ollama preload failed status=' .. tostring(response.status) .. ' model=' .. tostring(raw_model) .. ' error=' .. tostring(error_message)) if request.callback then request.callback(nil, { requested_model = selected_model, @@ -506,8 +492,8 @@ function M.chat(request) ) ) emit_status { - phase = 'generating', - message = 'Generating response', + phase = 'context', + message = 'Loading prompt context', } job = run_chat() if request.register_http_job then -- 2.51.2