From 7fe5bfc9f3dce7ed40c9a8e7d8516562c409d87c Mon Sep 17 00:00:00 2001 From: theprimeagain Date: Mon, 23 Feb 2026 07:51:59 -0700 Subject: working lua window inflight --- AGENTS.md | 38 ++++++++++++++ TUTORIAL.md | 11 ---- lua/99/consts.lua | 6 +++ lua/99/init.lua | 79 ++++------------------------ lua/99/logger/logger.lua | 1 + lua/99/ops/throbber.lua | 19 ++++--- lua/99/state.lua | 16 +++--- lua/99/test/in_flight_spec.lua | 71 +++++++++++++++++++++++++ lua/99/test/test_utils.lua | 36 +++++++++---- lua/99/window/in-flight.lua | 114 +++++++++++++++++++++++++++++++++++++++++ lua/99/window/init.lua | 16 ++++++ 11 files changed, 299 insertions(+), 108 deletions(-) delete mode 100644 TUTORIAL.md create mode 100644 lua/99/consts.lua create mode 100644 lua/99/test/in_flight_spec.lua create mode 100644 lua/99/window/in-flight.lua diff --git a/AGENTS.md b/AGENTS.md index f285623..38a9d38 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,3 +5,41 @@ * make lua_test * make pr_ready +## e2e / Integration style testing +every testing file should have roughly the same setup. + +```lua +-- In this example we are testing the visual function for 99 requests +-- test utils has utilities to setup buffers and request context and +-- test providers that we can control when to resolve +-- test utils also has functions to schedule syncronously, very powerful +local _99 = require("99") +local test_utils = require("99.test.test_utils") +local visual_fn = require("99.ops.over-range") +-- ... imports that need to be tested + +describe("", function() + + it("specific test condition", function() + -- we setup the world with test provider, context, and state + local p, buffer, range = setup(content, 2, 1, 2, 23) + local state = _99.__get_state() + local context = Prompt.visual(state) + + -- now this test is simple, its just proving that we keep track + -- of inflight requests. + -- + -- all tests should have simple conditions we are testing for + -- and the logic should attempt to be as simple as possible. + eq(0, state:active_request_count()) + visual_fn(context, { + additional_prompt = "test prompt", + }) + eq(1, state:active_request_count()) + p:resolve("success", " return 'implemented!'") + test_utils.next_frame() + eq(0, state:active_request_count()) + + end) +end) +``` diff --git a/TUTORIAL.md b/TUTORIAL.md deleted file mode 100644 index 212fb4f..0000000 --- a/TUTORIAL.md +++ /dev/null @@ -1,11 +0,0 @@ -### The features i want. -- a prompt window - - a way to tag docs - - a crafted tutorial -- links back to the docs - - i want special remaps for this... back and forth should be amazing -- the tutorial re-navigable / recallable - - keep that information around for some amount of time... - - stacked? - - telescoped? - diff --git a/lua/99/consts.lua b/lua/99/consts.lua new file mode 100644 index 0000000..4763a6a --- /dev/null +++ b/lua/99/consts.lua @@ -0,0 +1,6 @@ +return { + show_in_flight_requests_loop_time = 1000, + throbber_throb_time = 1200, + throbber_cooldown_time = 100, + throbber_tick_time = 100, +} diff --git a/lua/99/init.lua b/lua/99/init.lua index 4dffdfc..9fee2d3 100644 --- a/lua/99/init.lua +++ b/lua/99/init.lua @@ -3,12 +3,12 @@ local Level = require("99.logger.level") local ops = require("99.ops") local Languages = require("99.language") local Window = require("99.window") +local show_in_flight_requests = require("99.window.in-flight") local Prompt = require("99.prompt") local State = require("99.state") local Extensions = require("99.extensions") local Agents = require("99.extensions.agents") local Providers = require("99.providers") -local Throbber = require("99.ops.throbber") ---@param path_or_rule string | _99.Agents.Rule ---@return _99.Agents.Rule | string @@ -38,20 +38,21 @@ local function process_opts(opts) end --- @class _99.Completion +--- @docs included --- @field source "cmp" | "blink" | nil --- @field custom_rules string[] --- @field files _99.Files.Config? --- @class _99.Options ---- @field logger _99.Logger.Options? ---- @field model string? ---- @field show_in_flight_requests boolean? ---- @field md_files string[]? ---- @field provider _99.Providers.BaseProvider? ---- @field debug_log_prefix string? +--- @docs base +--- @field logger? _99.Logger.Options +--- @field model? string +--- @field in_flight_options? _99.InFlight.Opts +--- @field md_files? string[] +--- @field provider? _99.Providers.BaseProvider --- @field display_errors? boolean --- @field auto_add_skills? boolean ---- @field completion _99.Completion? +--- @field completion? _99.Completion --- @field tmp_dir? string --- @type _99.State @@ -282,64 +283,6 @@ function _99.__get_state() return _99_state end -local function shut_down_in_flight_requests_window() - if _99_state.show_in_flight_requests_throbber then - _99_state.show_in_flight_requests_throbber:stop() - end - - local win = _99_state.show_in_flight_requests_window - if win ~= nil then - Window.close(win) - end - _99_state.show_in_flight_requests_window = nil - _99_state.show_in_flight_requests_throbber = nil -end - -local function show_in_flight_requests() - if _99_state.show_in_flight_requests == false then - return - end - vim.defer_fn(show_in_flight_requests, 1000) - - Window.refresh_active_windows() - local current_win = _99_state.show_in_flight_requests_window - if current_win ~= nil and not Window.is_active_window(current_win) then - shut_down_in_flight_requests_window() - end - - if Window.has_active_windows() or _99_state:active_request_count() == 0 then - return - end - - if _99_state.show_in_flight_requests_window == nil then - local win = Window.status_window() - local throb = Throbber.new(function(throb) - local count = _99_state:active_request_count() - if count == 0 or not Window.valid(win) then - return shut_down_in_flight_requests_window() - end - - --- @type string[] - local lines = { - throb .. " requests(" .. tostring(count) .. ") " .. throb, - } - - for _, c in pairs(_99_state.__request_by_id) do - if c.state == "requesting" then - table.insert(lines, c.operation) - end - end - - Window.resize(win, #lines[1], #lines) - vim.api.nvim_buf_set_lines(win.buf_id, 0, 1, false, lines) - end) - _99_state.show_in_flight_requests_window = win - _99_state.show_in_flight_requests_throbber = throb - - throb:start() - end -end - --- @param opts _99.Options? function _99.setup(opts) opts = opts or {} @@ -389,9 +332,7 @@ function _99.setup(opts) Extensions.init(_99_state) Extensions.capture_project_root() - if _99_state.show_in_flight_requests then - show_in_flight_requests() - end + show_in_flight_requests(_99_state, _99_state.in_flight_options) end --- @param md string diff --git a/lua/99/logger/logger.lua b/lua/99/logger/logger.lua index 611b016..003aab2 100644 --- a/lua/99/logger/logger.lua +++ b/lua/99/logger/logger.lua @@ -8,6 +8,7 @@ local logger_list = {} local max_requests_in_logger_cache = MAX_REQUEST_DEFAULT --- @class _99.Logger.Options +--- @docs included --- @field level number? --- @field type? "print" | "void" | "file" --- @field path string? diff --git a/lua/99/ops/throbber.lua b/lua/99/ops/throbber.lua index b08302a..4dd5867 100644 --- a/lua/99/ops/throbber.lua +++ b/lua/99/ops/throbber.lua @@ -1,4 +1,6 @@ local time = require("99.time") +local Consts = require("99.consts") + local throb_icons = { { "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" }, { "◐", "◓", "◑", "◒" }, @@ -55,10 +57,6 @@ local function ease_in_ease_out_cubic(percent) end end -local throb_time = 1200 -local cooldown_time = 100 -local tick_time = 100 - --- @class _99.Throbber --- @field start_time number --- @field section_time number @@ -72,16 +70,17 @@ Throbber.__index = Throbber --- @class _99.Throbber.Opts --- @field throb_time number --- @field cooldown_time number +--- @field tick_time number --- @param cb fun(str: string): nil --- @param opts _99.Throbber.Opts? --- @return _99.Throbber function Throbber.new(cb, opts) - opts = opts - or { - throb_time = throb_time, - cooldown_time = cooldown_time, - } + opts = opts or {} + opts.throb_time = opts.throb_time or Consts.throbber_throb_time + opts.cooldown_time = opts.cooldown_time or Consts.throbber_cooldown_time + opts.tick_time = opts.tick_time or Consts.throbber_tick_time + return setmetatable({ state = "init", start_time = 0, @@ -111,7 +110,7 @@ function Throbber:_run() self.cb(icon) vim.defer_fn(function() self:_run() - end, tick_time) + end, self.opts.tick_time) end function Throbber:start() diff --git a/lua/99/state.lua b/lua/99/state.lua index 1762b84..d472117 100644 --- a/lua/99/state.lua +++ b/lua/99/state.lua @@ -1,12 +1,15 @@ local Agents = require("99.extensions.agents") local Extensions = require("99.extensions") +local function default_completion() + return { source = nil, custom_rules = {} } +end + --- @class _99.StateProps --- @field model string --- @field md_files string[] --- @field prompts _99.Prompts --- @field ai_stdout_rows number ---- @field show_in_flight_requests boolean --- @field languages string[] --- @field display_errors boolean --- @field auto_add_skills boolean @@ -24,7 +27,7 @@ local Extensions = require("99.extensions") --- @field ai_stdout_rows number --- @field languages string[] --- @field display_errors boolean ---- @field show_in_flight_requests boolean +--- @field in_flight_options _99.InFlight.Opts | nil --- @field show_in_flight_requests_window _99.window.Window | nil --- @field show_in_flight_requests_throbber _99.Throbber | nil --- @field provider_override _99.Providers.BaseProvider? @@ -45,7 +48,6 @@ local function create() md_files = {}, prompts = require("99.prompt-settings"), ai_stdout_rows = 3, - show_in_flight_requests = false, languages = { "lua", "go", "java", "elixir", "cpp", "ruby" }, display_errors = false, provider_override = nil, @@ -63,13 +65,9 @@ function State.new(opts) local props = create() local _99_state = setmetatable(props, State) --[[@as _99.State]] - _99_state.show_in_flight_requests = opts.show_in_flight_requests or false + _99_state.in_flight_options = opts.in_flight_options or { enable = true } _99_state.provider_override = opts.provider - _99_state.completion = opts.completion - or { - source = nil, - custom_rules = {}, - } + _99_state.completion = opts.completion or default_completion() _99_state.completion.custom_rules = _99_state.completion.custom_rules or {} _99_state.auto_add_skills = opts.auto_add_skills or false _99_state.completion.files = _99_state.completion.files or {} diff --git a/lua/99/test/in_flight_spec.lua b/lua/99/test/in_flight_spec.lua new file mode 100644 index 0000000..c9ebc6b --- /dev/null +++ b/lua/99/test/in_flight_spec.lua @@ -0,0 +1,71 @@ +-- luacheck: globals describe it assert after_each +local _99 = require("99") +local Prompt = require("99.prompt") +local Window = require("99.window") +local test_utils = require("99.test.test_utils") +local eq = assert.are.same + +local content = { + "local function foo()", + " return 1", + "end", +} + +--- You have to override this or else things will crash since the ui itself +--- does not exist. this is a headless test so i fake it by returning a very +--- simple ui of 120x40 +local original_nvim_list_uis = vim.api.nvim_list_uis +local function nvim_list_uis() + return { + { width = 120, height = 40 }, + } +end + +describe("in_flight window", function() + local WAIT_TIME = 10 + before_each(function() + vim.api.nvim_list_uis = nvim_list_uis + end) + after_each(function() + vim.api.nvim_list_uis = original_nvim_list_uis + end) + it("shows active requests and clears when request completes", function() + local provider = test_utils.test_setup(content, 2, 4) + local state = _99.__get_state() + local context = Prompt.search(state) + + context:start_request() + vim.wait(WAIT_TIME * 2, function() end) + + eq(1, #Window.active_windows) + + local win = Window.active_windows[1] + vim.api.nvim_win_close(win.win_id, true) + + vim.wait(WAIT_TIME * 2, function() end) + local next_win = Window.active_windows[1] + + eq(true, win.win_id ~= next_win.win_id) + + provider:resolve("success", "results are in") + + vim.wait(WAIT_TIME * 2, function() end) + eq(0, #Window.active_windows) + end) + + it("enable false == do not show in flight", function() + local provider = test_utils.test_setup(content, 2, 4, "lua", { + in_flight_options = { enable = false }, + }) + local state = _99.__get_state() + local context = Prompt.search(state) + + context:start_request() + vim.wait(WAIT_TIME * 2, function() end) + + eq(0, #Window.active_windows) + provider:resolve("success", "results are in") + vim.wait(WAIT_TIME * 2, function() end) + eq(0, #Window.active_windows) + end) +end) diff --git a/lua/99/test/test_utils.lua b/lua/99/test/test_utils.lua index e5484f1..28fe1e4 100644 --- a/lua/99/test/test_utils.lua +++ b/lua/99/test/test_utils.lua @@ -108,21 +108,39 @@ function M.create_file(contents, file_type, row, col) return bufnr end +--- @param opts _99.Options | nil +--- @param provider _99.Providers.BaseProvider +--- @return _99.Options +function M.get_test_setup_options(opts, provider) + opts = opts or {} + opts.provider = provider + opts.logger = { + error_cache_level = Levels.ERROR, + type = "print", + } + opts.in_flight_options = opts.in_flight_options + or { + throbber_opts = { + tick_time = 10, + throb_time = 1000, + cooldown_time = 500, + }, + in_flight_interval = 10, + enable = true, + } + return opts +end + --- @param content string[] --- @param row number --- @param col number --- @param lang string? +--- @param opts _99.Options | nil --- @return _99.test.Provider, number -function M.test_setup(content, row, col, lang) - assert(lang, "lang must be provided") +function M.test_setup(content, row, col, lang, opts) + lang = lang or "lua" local provider = M.TestProvider.new() - require("99").setup({ - provider = provider, - logger = { - error_cache_level = Levels.ERROR, - type = "print", - }, - }) + require("99").setup(M.get_test_setup_options(opts, provider)) local buffer = M.create_file(content, lang, row, col) return provider, buffer diff --git a/lua/99/window/in-flight.lua b/lua/99/window/in-flight.lua new file mode 100644 index 0000000..9605688 --- /dev/null +++ b/lua/99/window/in-flight.lua @@ -0,0 +1,114 @@ +local Window = require("99.window") +local Consts = require("99.consts") +local Throbber = require("99.ops.throbber") + +--- @param opts _99.InFlight.Opts | nil +--- @return _99.InFlight.Opts +local function default_opts(opts) + opts = opts or {} + opts.throbber_opts = opts.throbber_opts + or { + throb_time = Consts.throbber_throb_time, + cooldown_time = Consts.throbber_cooldown_time, + tick_time = Consts.throbber_tick_time, + } + opts.in_flight_interval = opts.in_flight_interval + or Consts.show_in_flight_requests_loop_time + opts.enable = opts.enable == nil and true or opts.enable + return opts +end + +--- @param _99 _99.State +local function shut_down_in_flight_requests_window(_99) + if _99.show_in_flight_requests_throbber then + _99.show_in_flight_requests_throbber:stop() + end + + local win = _99.show_in_flight_requests_window + if win ~= nil then + Window.close(win) + end + _99.show_in_flight_requests_window = nil + _99.show_in_flight_requests_throbber = nil +end + +--- @class _99.InFlight.Opts +--- this is pure a class for testing. helps controls timings +--- @docs include +--- @field throbber_opts _99.Throbber.Opts | nil +--- options for the throbber in the top left +--- @field in_flight_interval number | nil +--- frequency in which the in-flight interval checks to see if it should be +--- displayed / removed +--- @field enable boolean | nil +--- defaults to true + +--- @param _99 _99.State +--- @param opts _99.InFlight.Opts | nil +local function show_in_flight_requests(_99, opts) + --- TODO: I dont like this. i dont like that i have to redo this every single + --- time i cycle, but its not a big deal right now. either way ill address this later + opts = default_opts(opts) + if opts.enable == false then + return + end + vim.defer_fn(function() + show_in_flight_requests(_99, opts) + end, opts.in_flight_interval) + + Window.refresh_active_windows() + local current_win = _99.show_in_flight_requests_window + if current_win ~= nil and not Window.is_active_window(current_win) then + shut_down_in_flight_requests_window(_99) + end + + local active_window = Window.has_active_status_window() + local active_other_window = Window.has_active_windows() + local active_requests = _99:active_request_count() + if + active_window == false and active_other_window + or active_window and active_requests > 0 + or active_window == false and active_requests == 0 + then + return + end + + if _99.show_in_flight_requests_window == nil then + local ok, win = pcall(Window.status_window) + if not ok then + --- TODO: There needs to be a way to display logs for "all active requests" + --- this is its own activity and should not be added to any work set + return + end + + local throb = Throbber.new(function(throb) + local count = _99:active_request_count() + local win_valid = Window.valid(win) + + if count == 0 or not win_valid then + return shut_down_in_flight_requests_window(_99) + end + + --- @type string[] + local lines = { + throb .. " requests(" .. tostring(count) .. ") " .. throb, + } + + for _, c in pairs(_99.__request_by_id) do + if c.state == "requesting" then + table.insert(lines, c.operation) + end + end + + Window.resize(win, #lines[1], #lines) + vim.api.nvim_buf_set_lines(win.buf_id, 0, -1, false, lines) + end, opts.throbber_opts) + + _99.show_in_flight_requests_window = win + _99.show_in_flight_requests_throbber = throb + + throb:start() + end +end + +return show_in_flight_requests diff --git a/lua/99/window/init.lua b/lua/99/window/init.lua index 8db5b2f..f546fac 100644 --- a/lua/99/window/init.lua +++ b/lua/99/window/init.lua @@ -26,6 +26,7 @@ local nvim_buf_is_valid = vim.api.nvim_buf_is_valid --- @field config _99.window.Config --- @field win_id number --- @field buf_id number +--- @field type "capture_input" | "status" --- @class _99.window.SplitWindow --- @field win number @@ -366,6 +367,8 @@ function M.capture_input(name, opts) local config = create_centered_window() local win = create_floating_window(config, string.format(" 99 %s ", name), true) + win.type = "capture_input" + set_defaul_win_options(win, "99-prompt") vim.api.nvim_set_current_win(win.win_id) @@ -456,6 +459,7 @@ function M.status_window() M.clear_active_popups() local config = create_transparent_top_right_config(100, " 99 - Status ") local window = create_floating_window(config, " 99 - Status ", false) + window.type = "status" return window end @@ -477,6 +481,18 @@ function M.has_active_windows() return #M.active_windows > 0 end +--- @return boolean +function M.has_active_status_window() + local has = false + for _, w in ipairs(M.active_windows) do + if w.type == "status" then + has = true + break + end + end + return has +end + function M.refresh_active_windows() --- @type _99.window.Window[] local actives = {} -- cgit v1.3-3-g829e