From 94b8094cbd57eb53717b136978651c98f685d2ae Mon Sep 17 00:00:00 2001 From: Anthony Poschen Date: Fri, 13 Feb 2026 21:45:03 +1100 Subject: feat: Add tmp directory which is configurable --- lua/99/request-context.lua | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'lua/99/request-context.lua') diff --git a/lua/99/request-context.lua b/lua/99/request-context.lua index 6671cab..609fe78 100644 --- a/lua/99/request-context.lua +++ b/lua/99/request-context.lua @@ -36,11 +36,16 @@ function RequestContext.from_current_buffer(_99, xid) table.insert(mds, md) end + local tmp_dir = _99.tmp_dir + if tmp_dir then + tmp_dir = vim.fn.expand(tmp_dir) + end + return setmetatable({ _99 = _99, md_file_names = mds, ai_context = {}, - tmp_file = random_file(), + tmp_file = random_file(tmp_dir), buffer = buffer, full_path = full_path, file_type = file_type, -- cgit v1.3-3-g829e From 3ade8bbbbcc4fe80a09f22eb59ce8539e4fe2987 Mon Sep 17 00:00:00 2001 From: theprimeagain Date: Sun, 15 Feb 2026 15:28:36 -0700 Subject: the final parts of tutorial --- lua/99/extensions/agents/init.lua | 1 - lua/99/init.lua | 92 ++++++++++++--------------------------- lua/99/ops/clean-up.lua | 56 ++++++++++++++++++++---- lua/99/ops/implement-fn.lua | 92 --------------------------------------- lua/99/ops/init.lua | 1 - lua/99/ops/make-prompt.lua | 10 ++++- lua/99/ops/over-range.lua | 18 ++++---- lua/99/ops/search.lua | 46 ++++++++------------ lua/99/ops/tutorial.lua | 48 +++++++++----------- lua/99/providers.lua | 3 ++ lua/99/request-context.lua | 36 +-------------- lua/99/request/init.lua | 9 +++- lua/99/test/request_spec.lua | 3 ++ 13 files changed, 147 insertions(+), 268 deletions(-) delete mode 100644 lua/99/ops/implement-fn.lua (limited to 'lua/99/request-context.lua') diff --git a/lua/99/extensions/agents/init.lua b/lua/99/extensions/agents/init.lua index 4002c75..b1fda0f 100644 --- a/lua/99/extensions/agents/init.lua +++ b/lua/99/extensions/agents/init.lua @@ -1,5 +1,4 @@ local helpers = require("99.extensions.agents.helpers") -local Logger = require("99.logger.logger") local M = {} --- @class _99.Agents.Rule diff --git a/lua/99/init.lua b/lua/99/init.lua index 6f6010f..43daed1 100644 --- a/lua/99/init.lua +++ b/lua/99/init.lua @@ -54,17 +54,18 @@ end --- @class _99.RequestEntry.Data.Visual --- @field type "visual" +--- --- @alias _99.RequestEntry.Data _99.RequestEntry.Data.Search | _99.RequestEntry.Data.Tutorial | _99.RequestEntry.Data.Visual --- @class _99.RequestEntry --- @field id number --- @field operation string ---- @field status "running" | "success" | "failed" | "cancelled" +--- @field status _99.Request.State --- @field filename string --- @field lnum number --- @field col number --- @field started_at number ---- @field operation_data _99.RequestEntry.Data +--- @field operation_data _99.RequestEntry.Data | nil --- @class _99.ActiveRequest --- @field clean_up _99.Cleanup @@ -81,9 +82,9 @@ end --- @field display_errors boolean --- @field auto_add_skills boolean --- @field provider_override _99.Providers.BaseProvider? ---- @field __active_requests table --- @field __view_log_idx number --- @field __tutorials _99.RequestEntry.Data.Tutorial[] +--- @field __searches _99.RequestEntry.Data.Search[] --- @field __request_history _99.RequestEntry[] --- @field __request_by_id table @@ -99,8 +100,8 @@ local function create_99_state() display_errors = false, provider_override = nil, auto_add_skills = false, - __active_requests = {}, __tutorials = {}, + __searches = {}, __view_log_idx = 1, __request_history = {}, __request_by_id = {}, @@ -139,10 +140,10 @@ end --- @field provider_override _99.Providers.BaseProvider? --- @field auto_add_skills boolean --- @field rules _99.Agents.Rules ---- @field __active_requests table --- @field __view_log_idx number --- @field __request_history _99.RequestEntry[] --- @field __tutorials _99.RequestEntry.Data.Tutorial[] +--- @field __searches _99.RequestEntry.Data.Search[] --- @field __request_by_id table --- @field __active_marks _99.Mark[] local _99_State = {} @@ -170,30 +171,42 @@ function _99_State:refresh_rules() end --- @param context _99.RequestContext +--- @param clean_up fun(): nil --- @return _99.RequestEntry -function _99_State:track_request(context) +function _99_State:track_request(context, clean_up) local point = context.range and context.range.start or Point:from_cursor() local entry = { id = context.xid, + clean_up = clean_up, operation = context.operation or "request", status = "running", filename = context.full_path, lnum = point.row, col = point.col, started_at = time.now(), + operation_data = nil, } table.insert(self.__request_history, entry) self.__request_by_id[entry.id] = entry return entry end ---- @param id number +--- @param context _99.RequestContext --- @param status "success" | "failed" | "cancelled" -function _99_State:finish_request(id, status) +function _99_State:finish_request(context, status) + local id = context.xid local entry = self.__request_by_id[id] if entry then entry.status = status end + if entry.operation == "success" and entry.operation_data then + local data = entry.operation_data + if data.type == "tutorial" then + table.insert(self.__tutorials, data) + elseif data.type == "search" then + table.insert(self.__searches, data) + end + end end --- @param id number @@ -211,17 +224,6 @@ function _99_State:add_data(id, data) entry.operation_data = data end ---- @param id number -function _99_State:remove_request(id) - for i, entry in ipairs(self.__request_history) do - if entry.id == id then - table.remove(self.__request_history, i) - break - end - end - self.__request_by_id[id] = nil -end - --- @return number function _99_State:previous_request_count() local count = 0 @@ -243,22 +245,8 @@ function _99_State:clear_previous_requests() end end self.__request_history = keep -end - -local _active_request_id = 0 ----@param clean_up _99.Cleanup ----@param request_id number ----@param name string ----@return number -function _99_State:add_active_request(clean_up, request_id, name) - _active_request_id = _active_request_id + 1 - Logger:debug("adding active request", "id", _active_request_id) - self.__active_requests[_active_request_id] = { - clean_up = clean_up, - request_id = request_id, - name = name, - } - return _active_request_id + self.__searches = {} + self.__tutorials = {} end --- @param mark _99.Mark @@ -268,31 +256,14 @@ end function _99_State:active_request_count() local count = 0 - for _ in pairs(self.__active_requests) do - count = count + 1 + for _, r in pairs(self.__request_history) do + if r.status == "running" then + count = count + 1 + end end return count end ----@param id number -function _99_State:remove_active_request(id) - local logger = Logger:set_id(id) - local r = self.__active_requests[id] - logger:assert(r, "there is no active request for id. implementation broken") - logger:debug("removing active request") - self.__active_requests[id] = nil - - local entry = self.__request_history[id] - if entry.operation == "tutorial" and entry.status == "success" then - local data = entry.operation_data - logger:assert( - data.type == "tutorial", - "data type tutorial expected for request tutorial" - ) - table.insert(self.__tutorials, data) - end -end - local _99_state = _99_State.new() --- @class _99 @@ -464,11 +435,7 @@ function _99.next_request_logs() end function _99.stop_all_requests() - for _, active in pairs(_99_state.__active_requests) do - _99_state:remove_request(active.request_id) - active.clean_up() - end - _99_state.__active_requests = {} + error("implement") end function _99.clear_all_marks() @@ -542,9 +509,6 @@ local function show_in_flight_requests() local lines = { throb .. " requests(" .. tostring(count) .. ") " .. throb, } - for _, r in pairs(_99_state.__active_requests) do - table.insert(lines, r.name) - end Window.resize(win, #lines[1], #lines) vim.api.nvim_buf_set_lines(win.buf_id, 0, 1, false, lines) diff --git a/lua/99/ops/clean-up.lua b/lua/99/ops/clean-up.lua index ba76f01..2ac099c 100644 --- a/lua/99/ops/clean-up.lua +++ b/lua/99/ops/clean-up.lua @@ -1,20 +1,60 @@ ----@param context _99.RequestContext ----@param name string +local M = {} + +--- @alias _99.Providers.on_complete fun(status: _99.Request.ResponseState, response: string): nil +--- @class _99.Providers.PartialObserver +--- @field on_complete _99.Providers.on_complete +--- @field on_stdout? fun(line: string): nil +--- @field on_stderr? fun(line: string): nil +--- @field on_start? fun(): nil + +--- @param context _99.RequestContext +--- @param clean_up fun(): nil +--- @param obs_or_fn _99.Providers.PartialObserver | _99.Providers.on_complete +--- @return _99.Providers.Observer +M.make_observer = function(context, clean_up, obs_or_fn) + --- @type _99.Providers.PartialObserver + local obs = type(obs_or_fn) == "table" and obs_or_fn + or { + on_complete = obs_or_fn, + } + + return { + on_start = function() + context._99:track_request(context, clean_up) + if obs.on_start then + obs.on_start() + end + end, + on_complete = function(status, res) + vim.schedule(clean_up) + context._99:finish_request(context, status) + obs.on_complete(status, res) + end, + on_stderr = function(line) + if obs.on_stderr then + obs.on_stderr(line) + end + end, + on_stdout = function(line) + if obs.on_stdout then + obs.on_stdout(line) + end + end, + } --[[@as _99.Providers.Observer ]] +end + ---@param clean_up_fn fun(): nil ---@return fun(): nil -return function(context, name, clean_up_fn) +M.make_clean_up = function(clean_up_fn) local called = false - local request_id = -1 local function clean_up() if called then return end - called = true clean_up_fn() - context._99:remove_active_request(request_id) end - request_id = context._99:add_active_request(clean_up, context.xid, name) - return clean_up end + +return M diff --git a/lua/99/ops/implement-fn.lua b/lua/99/ops/implement-fn.lua deleted file mode 100644 index efec8a3..0000000 --- a/lua/99/ops/implement-fn.lua +++ /dev/null @@ -1,92 +0,0 @@ -local Request = require("99.request") -local editor = require("99.editor") -local geo = require("99.geo") -local Range = geo.Range -local Point = geo.Point -local Mark = require("99.ops.marks") -local RequestStatus = require("99.ops.request_status") -local make_clean_up = require("99.ops.clean-up") - ---- @param context _99.RequestContext ---- @param response string -local function update_code(context, response) - local code_mark = context.marks.code_placement - local logger = context.logger:set_area("implement_fn#update_code") - local point = Point.from_mark(code_mark) - - logger:debug("setting text at mark", "Point", point) - code_mark:set_text_at_mark("\n" .. response) -end - ---- @param context _99.RequestContext -local function implement_fn(context) - local ts = editor.treesitter - local cursor = Point:from_cursor() - local buffer = vim.api.nvim_get_current_buf() - local fn_call = ts.fn_call(buffer, cursor) - local logger = context.logger:set_area("implement_fn") - - if not fn_call then - logger:fatal( - "cannot implement function, cursor was not on an identifier that is a function call" - ) - return - end - - local range = Range:from_ts_node(fn_call, buffer) - local request = Request.new(context) - - context.marks.end_of_fn_call = Mark.mark_end_of_range(buffer, range) - local func = ts.containing_function(buffer, cursor) - if func then - context.marks.code_placement = Mark.mark_above_func(buffer, func) - else - context.marks.code_placement = Mark.mark_above_range(range) - end - - local code_placement = RequestStatus.new( - 250, - context._99.ai_stdout_rows, - "Loading", - context.marks.code_placement - ) - local at_call_site = RequestStatus.new( - 250, - 1, - "Implementing Function", - context.marks.end_of_fn_call - ) - - code_placement:start() - at_call_site:start() - - local clean_up = make_clean_up(context, function() - context:clear_marks() - request:cancel() - code_placement:stop() - at_call_site:stop() - end) - - request:add_prompt_content(context._99.prompts.prompts.implement_function) - request:start({ - on_stdout = function(line) - code_placement:push(line) - end, - on_complete = function(status, response) - vim.schedule(clean_up) - if status ~= "success" then - logger:fatal( - "unable to implement function, enable and check logger for more details" - ) - end - pcall(update_code, context, response) - end, - on_stderr = function(line) - logger:error("stderr", "line", line) - end, - }) - - return request -end - -return implement_fn diff --git a/lua/99/ops/init.lua b/lua/99/ops/init.lua index 247c903..1f8e9f8 100644 --- a/lua/99/ops/init.lua +++ b/lua/99/ops/init.lua @@ -9,6 +9,5 @@ return { search = require("99.ops.search"), tutorial = require("99.ops.tutorial"), - implement_fn = require("99.ops.implement-fn"), over_range = require("99.ops.over-range"), } diff --git a/lua/99/ops/make-prompt.lua b/lua/99/ops/make-prompt.lua index 8b77256..4a17687 100644 --- a/lua/99/ops/make-prompt.lua +++ b/lua/99/ops/make-prompt.lua @@ -1,9 +1,10 @@ local Completions = require("99.extensions.completions") +local Agents = require("99.extensions.agents") --- @param context _99.RequestContext --- @param prompt string --- @param opts _99.ops.Opts ---- @return string, _99.Completion +--- @return string, _99.Reference[] return function(context, prompt, opts) local user_prompt = opts.additional_prompt assert( @@ -18,7 +19,12 @@ return function(context, prompt, opts) local additional_rules = opts.additional_rules if additional_rules then for _, r in ipairs(additional_rules) do - table.insert(refs, r) + local content = Agents.get_rule_content(r) + if content then + table.insert(refs, { + content = content, + }) + end end end diff --git a/lua/99/ops/over-range.lua b/lua/99/ops/over-range.lua index ba4a7d3..a37aaf8 100644 --- a/lua/99/ops/over-range.lua +++ b/lua/99/ops/over-range.lua @@ -2,8 +2,11 @@ local Request = require("99.request") local RequestStatus = require("99.ops.request_status") local Mark = require("99.ops.marks") local geo = require("99.geo") -local make_clean_up = require("99.ops.clean-up") local make_prompt = require("99.ops.make-prompt") +local CleanUp = require("99.ops.clean-up") + +local make_clean_up = CleanUp.make_clean_up +local make_observer = CleanUp.make_observer local Range = geo.Range local Point = geo.Point @@ -37,7 +40,7 @@ local function over_range(context, range, opts) top_mark ) local bottom_status = RequestStatus.new(250, 1, "Implementing", bottom_mark) - local clean_up = make_clean_up(context, "Visual", function() + local clean_up = make_clean_up(function() top_status:stop() bottom_status:stop() context:clear_marks() @@ -45,17 +48,15 @@ local function over_range(context, range, opts) end) local system_cmd = context._99.prompts.prompts.visual_selection(range) - local prompt, rules, refs = make_prompt(context, system_cmd, opts) + local prompt, refs = make_prompt(context, system_cmd, opts) - context:add_agent_rules(rules) request:add_prompt_content(prompt) context:add_references(refs) top_status:start() bottom_status:start() - request:start({ + request:start(make_observer(context, clean_up, { on_complete = function(status, response) - vim.schedule(clean_up) if status == "cancelled" then logger:debug("request cancelled for visual selection, removing marks") elseif status == "failed" then @@ -90,10 +91,7 @@ local function over_range(context, range, opts) top_status:push(line) end end, - on_stderr = function(line) - logger:debug("visual_selection#on_stderr received", "line", line) - end, - }) + })) end return over_range diff --git a/lua/99/ops/search.lua b/lua/99/ops/search.lua index cc6752d..cde8f64 100644 --- a/lua/99/ops/search.lua +++ b/lua/99/ops/search.lua @@ -1,6 +1,9 @@ local Request = require("99.request") -local make_clean_up = require("99.ops.clean-up") local make_prompt = require("99.ops.make-prompt") +local CleanUp = require("99.ops.clean-up") + +local make_clean_up = CleanUp.make_clean_up +local make_observer = CleanUp.make_observer --- @class _99.Search.Result --- @field filename string @@ -65,39 +68,28 @@ local function search(context, opts) logger:debug("search", "with opts", opts.additional_prompt) - local clean_up = make_clean_up(context, "Search", function() + local clean_up = make_clean_up(function() request:cancel() end) - local prompt, rules, refs = + local prompt, refs = make_prompt(context, context._99.prompts.prompts.semantic_search(), opts) - context:add_agent_rules(rules) request:add_prompt_content(prompt) context:add_references(refs) - request:start({ - on_complete = function(status, response) - vim.schedule(clean_up) - if status == "cancelled" then - logger:debug("request cancelled for search") - elseif status == "failed" then - logger:error( - "request failed for search", - "error response", - response or "no response provided" - ) - elseif status == "success" then - create_search_locations(context._99, response) - end - end, - on_stdout = function(line) - --- TODO: i need to figure out how to surface this information - _ = line - end, - on_stderr = function(line) - logger:debug("visual_selection#on_stderr received", "line", line) - end, - }) + request:start(make_observer(context, clean_up, function(status, response) + if status == "cancelled" then + logger:debug("request cancelled for search") + elseif status == "failed" then + logger:error( + "request failed for search", + "error response", + response or "no response provided" + ) + elseif status == "success" then + create_search_locations(context._99, response) + end + end)) end return search diff --git a/lua/99/ops/tutorial.lua b/lua/99/ops/tutorial.lua index 54e7804..bba56e6 100644 --- a/lua/99/ops/tutorial.lua +++ b/lua/99/ops/tutorial.lua @@ -1,7 +1,9 @@ local Request = require("99.request") -local make_clean_up = require("99.ops.clean-up") +local CleanUp = require("99.ops.clean-up") local make_prompt = require("99.ops.make-prompt") +local make_clean_up = CleanUp.make_clean_up +local make_observer = CleanUp.make_observer --- @class _99.Tutorial.Result --- @param context _99.RequestContext @@ -14,37 +16,29 @@ local function tutorial(context, opts) local request = Request.new(context) - local clean_up = make_clean_up(context, "Search", function() + local clean_up = make_clean_up(function() request:cancel() end) - local prompt, rules = + local prompt, refs = make_prompt(context, context._99.prompts.prompts.tutorial(), opts) - context:add_agent_rules(rules) + context:add_references(refs) request:add_prompt_content(prompt) - request:start({ - on_complete = function(status, response) - vim.schedule(clean_up) - if status == "cancelled" then - logger:debug("cancelled") - elseif status == "failed" then - logger:error( - "failed", - "error response", - response or "no response provided" - ) - elseif status == "success" then - error("what the hell") - end - end, - on_stdout = function(line) - --- TODO: i need to figure out how to surface this information - _ = line - end, - on_stderr = function(line) - logger:debug("on_stderr", "line", line) - end, - }) + request:start(make_observer(context, clean_up, function(status, response) + vim.schedule(clean_up) + if status == "cancelled" then + logger:debug("cancelled") + elseif status == "failed" then + logger:error( + "failed", + "error response", + response or "no response provided" + ) + elseif status == "success" then + error("what the hell") + end + end)) + end return tutorial diff --git a/lua/99/providers.lua b/lua/99/providers.lua index ff1188b..1b5c4d9 100644 --- a/lua/99/providers.lua +++ b/lua/99/providers.lua @@ -2,6 +2,7 @@ --- @field on_stdout fun(line: string): nil --- @field on_stderr fun(line: string): nil --- @field on_complete fun(status: _99.Request.ResponseState, res: string): nil +--- @field on_start fun(): nil --- @param fn fun(...: any): nil --- @return fun(...: any): nil @@ -50,6 +51,8 @@ end --- @param request _99.Request --- @param observer _99.Providers.Observer function BaseProvider:make_request(query, request, observer) + observer.on_start() + local logger = request.logger:set_area(self:_get_provider_name()) logger:debug("make_request", "tmp_file", request.context.tmp_file) diff --git a/lua/99/request-context.lua b/lua/99/request-context.lua index 6671cab..ef10aff 100644 --- a/lua/99/request-context.lua +++ b/lua/99/request-context.lua @@ -15,6 +15,7 @@ local random_file = utils.random_file --- @field xid number --- @field range _99.Range? --- @field operation string? +--- @field clean_ups (fun(): nil)[] --- @field _99 _99.State local RequestContext = {} RequestContext.__index = RequestContext @@ -38,6 +39,7 @@ function RequestContext.from_current_buffer(_99, xid) return setmetatable({ _99 = _99, + clean_ups = {}, md_file_names = mds, ai_context = {}, tmp_file = random_file(), @@ -58,40 +60,6 @@ function RequestContext:add_md_file_name(md_file_name) return self end ---- TODO: Dedupe any rules that have already been added ---- @param rules (_99.Agents.Rule | string)[] -function RequestContext:add_agent_rules(rules) - for _, rule in ipairs(rules) do - -- Handle both string paths and rule objects - self.logger:debug("adding custom rule to agent", "rule", rule) - local file_path = rule.absolute_path or rule.path - local ok, file = pcall(io.open, file_path, "r") - if ok and file then - local content = file:read("*a") - file:close() - self.logger:info( - "Context#adding agent file to the context", - "agent_path", - rule.path - ) - table.insert( - self.ai_context, - string.format( - [[ -<%s> -%s -]], - rule.name, - content, - rule.name - ) - ) - else - self.logger:debug("unable to read agent rule", "rule", rule) - end - end -end - --- @param refs _99.Reference[] function RequestContext:add_references(refs) for _, ref in ipairs(refs) do diff --git a/lua/99/request/init.lua b/lua/99/request/init.lua index 327aa0f..3cff512 100644 --- a/lua/99/request/init.lua +++ b/lua/99/request/init.lua @@ -81,8 +81,10 @@ end --- @param obs _99.Providers.Observer | nil local function observer_from_request(r, obs) return { + on_start = obs and obs.on_start or function() end, on_complete = function(status, res) r.state = status + r.context._99:finish_request(r.context, status) if obs then obs.on_complete(status, res) end @@ -108,7 +110,6 @@ function Request:start(observer) ) self.state = "requesting" - self.context._99:track_request(self.context) self.context:finalize() for _, content in ipairs(self.context.ai_context) do self:add_prompt_content(content) @@ -117,7 +118,11 @@ function Request:start(observer) local prompt = table.concat(self._content, "\n") self.context:save_prompt(prompt) self.logger:debug("start", "prompt", prompt) - self.provider:make_request(prompt, self, observer_from_request(self, observer)) + self.provider:make_request( + prompt, + self, + observer_from_request(self, observer) + ) end return Request diff --git a/lua/99/test/request_spec.lua b/lua/99/test/request_spec.lua index d2e0885..f6ce34d 100644 --- a/lua/99/test/request_spec.lua +++ b/lua/99/test/request_spec.lua @@ -26,6 +26,7 @@ describe("request test", function() eq("ready", request.state) + eq(0, state:active_request_count()) request:start({ on_complete = function(status, _) finished_called = true @@ -34,12 +35,14 @@ describe("request test", function() on_stdout = function() end, on_stderr = function() end, }) + eq(1, state:active_request_count()) eq("requesting", request.state) p:resolve("success", " return 'implemented!'") assert.is_true(finished_called) + eq(0, state:active_request_count()) eq("success", request.state) eq("success", finished_status) end) -- cgit v1.3-3-g829e From a5130c1ec63c4b6bbceedbc61906cd9803bfabf8 Mon Sep 17 00:00:00 2001 From: theprimeagain Date: Sun, 15 Feb 2026 16:00:54 -0700 Subject: better entry point. success still needs work --- lua/99/init.lua | 121 ++++++++++++++++++++++++--------------------- lua/99/ops/clean-up.lua | 6 +-- lua/99/ops/over-range.lua | 3 +- lua/99/ops/search.lua | 4 +- lua/99/ops/tutorial.lua | 3 +- lua/99/prompt-settings.lua | 3 +- lua/99/request-context.lua | 11 +++++ lua/99/request/init.lua | 16 ++++-- 8 files changed, 98 insertions(+), 69 deletions(-) (limited to 'lua/99/request-context.lua') diff --git a/lua/99/init.lua b/lua/99/init.lua index 43daed1..35b1444 100644 --- a/lua/99/init.lua +++ b/lua/99/init.lua @@ -58,20 +58,12 @@ end --- @alias _99.RequestEntry.Data _99.RequestEntry.Data.Search | _99.RequestEntry.Data.Tutorial | _99.RequestEntry.Data.Visual --- @class _99.RequestEntry ---- @field id number ---- @field operation string +--- @field context _99.RequestContext --- @field status _99.Request.State ---- @field filename string ---- @field lnum number ---- @field col number +--- @field point _99.Point --- @field started_at number --- @field operation_data _99.RequestEntry.Data | nil ---- @class _99.ActiveRequest ---- @field clean_up _99.Cleanup ---- @field request_id number ---- @field name string - --- @class _99.StateProps --- @field model string --- @field md_files string[] @@ -171,36 +163,38 @@ function _99_State:refresh_rules() end --- @param context _99.RequestContext ---- @param clean_up fun(): nil --- @return _99.RequestEntry -function _99_State:track_request(context, clean_up) +function _99_State:track_request(context) + assert( + context.operation, + "must have an operation defined to track the request" + ) + local point = context.range and context.range.start or Point:from_cursor() local entry = { - id = context.xid, - clean_up = clean_up, - operation = context.operation or "request", - status = "running", - filename = context.full_path, - lnum = point.row, - col = point.col, + context = context, + status = "requesting", + point = point, started_at = time.now(), operation_data = nil, } table.insert(self.__request_history, entry) - self.__request_by_id[entry.id] = entry + self.__request_by_id[context.xid] = entry return entry end --- @param context _99.RequestContext ---- @param status "success" | "failed" | "cancelled" +--- @param status _99.Request.ResponseState function _99_State:finish_request(context, status) local id = context.xid local entry = self.__request_by_id[id] - if entry then - entry.status = status + if not entry then + return end - if entry.operation == "success" and entry.operation_data then - local data = entry.operation_data + + entry.status = status + local data = entry.operation_data + if entry.status == "success" and data then if data.type == "tutorial" then table.insert(self.__tutorials, data) elseif data.type == "search" then @@ -218,7 +212,7 @@ function _99_State:add_data(id, data) end local logger = Logger:set_id(id) logger:assert( - entry.operation == data.type, + entry.context.operation == data.type, "the data type is not the same as the operation" ) entry.operation_data = data @@ -228,7 +222,7 @@ end function _99_State:previous_request_count() local count = 0 for _, entry in ipairs(self.__request_history) do - if entry.status ~= "running" then + if entry.status ~= "requesting" then count = count + 1 end end @@ -238,10 +232,10 @@ end function _99_State:clear_previous_requests() local keep = {} for _, entry in ipairs(self.__request_history) do - if entry.status == "running" then + if entry.status == "requesting" then table.insert(keep, entry) else - self.__request_by_id[entry.id] = nil + self.__request_by_id[entry.context.xid] = nil end end self.__request_history = keep @@ -257,7 +251,7 @@ end function _99_State:active_request_count() local count = 0 for _, r in pairs(self.__request_history) do - if r.status == "running" then + if r.status == "requesting" then count = count + 1 end end @@ -366,24 +360,14 @@ function _99.search(opts) end --- @param opts _99.ops.Opts -function _99.visual_prompt(opts) - vim.notify( - "use visual, visual_prompt has been deprecated", - vim.log.levels.WARN - ) - _99.visual(opts) -end - -function _99.fill_in_function() - error( - "function has been removed. Just use visual. I really hate fill in function, sorry :)" - ) -end - -function _99.fill_in_function_prompt() - error( - "function has been removed. Just use visual. I really hate fill in function, sorry :)" - ) +function _99.tutorial(opts) + opts = process_opts(opts) + local context = get_context("tutorial") + if opts.additional_prompt then + ops.tutorial(context, opts) + else + capture_prompt(ops.tutorial, "Tutorial", context, opts) + end end --- @param opts _99.ops.Opts? @@ -434,8 +418,33 @@ function _99.next_request_logs() Window.display_full_screen_message(logs[_99_state.__view_log_idx]) end +--- @class _99.QFixEntry +--- @field filename string +--- @field lnum number +--- @field col number +--- @field text string + +--- @param entry _99.RequestEntry +--- @return _99.QFixEntry +local function request_entry_to_qfix_item(entry) + local context = entry.context + local point = entry.point + local text = string.format("[%s] %s", entry.status, entry.context.operation) + + return { + filename = context and context.full_path or "", + lnum = point and point.row or 0, + col = point and point.col or 0, + text = text, + } +end + function _99.stop_all_requests() - error("implement") + for _, request in pairs(_99_state.__request_by_id) do + if request.status == "requesting" then + request.context:stop() + end + end end function _99.clear_all_marks() @@ -448,12 +457,7 @@ end function _99.previous_requests_to_qfix() local items = {} for _, entry in ipairs(_99_state.__request_history) do - table.insert(items, { - filename = entry.filename, - lnum = entry.lnum, - col = entry.col, - text = string.format("[%s] %s", entry.status, entry.operation), - }) + table.insert(items, request_entry_to_qfix_item(entry)) end vim.fn.setqflist({}, "r", { title = "99 Requests", items = items }) vim.cmd("copen") @@ -506,10 +510,17 @@ local function show_in_flight_requests() return shut_down_in_flight_requests_window() end + --- @type string[] local lines = { throb .. " requests(" .. tostring(count) .. ") " .. throb, } + for _, r in pairs(_99_state.__request_by_id) do + if r.status == "requesting" then + table.insert(lines, r.context.operation) + end + end + Window.resize(win, #lines[1], #lines) vim.api.nvim_buf_set_lines(win.buf_id, 0, 1, false, lines) end) diff --git a/lua/99/ops/clean-up.lua b/lua/99/ops/clean-up.lua index 2ac099c..cbf077a 100644 --- a/lua/99/ops/clean-up.lua +++ b/lua/99/ops/clean-up.lua @@ -7,27 +7,23 @@ local M = {} --- @field on_stderr? fun(line: string): nil --- @field on_start? fun(): nil ---- @param context _99.RequestContext --- @param clean_up fun(): nil --- @param obs_or_fn _99.Providers.PartialObserver | _99.Providers.on_complete --- @return _99.Providers.Observer -M.make_observer = function(context, clean_up, obs_or_fn) +M.make_observer = function(clean_up, obs_or_fn) --- @type _99.Providers.PartialObserver local obs = type(obs_or_fn) == "table" and obs_or_fn or { on_complete = obs_or_fn, } - return { on_start = function() - context._99:track_request(context, clean_up) if obs.on_start then obs.on_start() end end, on_complete = function(status, res) vim.schedule(clean_up) - context._99:finish_request(context, status) obs.on_complete(status, res) end, on_stderr = function(line) diff --git a/lua/99/ops/over-range.lua b/lua/99/ops/over-range.lua index a37aaf8..7070857 100644 --- a/lua/99/ops/over-range.lua +++ b/lua/99/ops/over-range.lua @@ -52,10 +52,11 @@ local function over_range(context, range, opts) request:add_prompt_content(prompt) context:add_references(refs) + context:add_clean_up(clean_up) top_status:start() bottom_status:start() - request:start(make_observer(context, clean_up, { + request:start(make_observer(clean_up, { on_complete = function(status, response) if status == "cancelled" then logger:debug("request cancelled for visual selection, removing marks") diff --git a/lua/99/ops/search.lua b/lua/99/ops/search.lua index cde8f64..44dd29c 100644 --- a/lua/99/ops/search.lua +++ b/lua/99/ops/search.lua @@ -75,10 +75,12 @@ local function search(context, opts) local prompt, refs = make_prompt(context, context._99.prompts.prompts.semantic_search(), opts) + request:add_prompt_content(prompt) context:add_references(refs) + context:add_clean_up(clean_up) - request:start(make_observer(context, clean_up, function(status, response) + request:start(make_observer(clean_up, function(status, response) if status == "cancelled" then logger:debug("request cancelled for search") elseif status == "failed" then diff --git a/lua/99/ops/tutorial.lua b/lua/99/ops/tutorial.lua index bba56e6..10d3530 100644 --- a/lua/99/ops/tutorial.lua +++ b/lua/99/ops/tutorial.lua @@ -24,8 +24,9 @@ local function tutorial(context, opts) make_prompt(context, context._99.prompts.prompts.tutorial(), opts) context:add_references(refs) request:add_prompt_content(prompt) + context:add_clean_up(clean_up) - request:start(make_observer(context, clean_up, function(status, response) + request:start(make_observer(clean_up, function(status, response) vim.schedule(clean_up) if status == "cancelled" then logger:debug("cancelled") diff --git a/lua/99/prompt-settings.lua b/lua/99/prompt-settings.lua index deb3368..9950a86 100644 --- a/lua/99/prompt-settings.lua +++ b/lua/99/prompt-settings.lua @@ -20,7 +20,8 @@ local prompts = { return [[ You are given a prompt and context and you must craft a tutorial. If a set of context has links, read through them thoroughly and decide which ones to retrieve. -Once you get the content, review it thoroughly before crafting the tutorial +Once you have fetched all the relavent content, review it thoroughly before +crafting the tutorial The response format must be valid Markdown ]] diff --git a/lua/99/request-context.lua b/lua/99/request-context.lua index ef10aff..ba76519 100644 --- a/lua/99/request-context.lua +++ b/lua/99/request-context.lua @@ -53,6 +53,17 @@ function RequestContext.from_current_buffer(_99, xid) }, RequestContext) end +function RequestContext:stop() + for _, cb in ipairs(self.clean_ups) do + cb() + end +end + +--- @param clean_up fun(): nil +function RequestContext:add_clean_up(clean_up) + table.insert(self.clean_ups, clean_up) +end + --- @param md_file_name string --- @return self function RequestContext:add_md_file_name(md_file_name) diff --git a/lua/99/request/init.lua b/lua/99/request/init.lua index 3cff512..d057879 100644 --- a/lua/99/request/init.lua +++ b/lua/99/request/init.lua @@ -47,7 +47,7 @@ function Request:_set_process(proc) end function Request:cancel() - if self.state == "success" then + if self.state == "success" or self.state == "failed" then return end @@ -80,11 +80,19 @@ end --- @param r _99.Request --- @param obs _99.Providers.Observer | nil local function observer_from_request(r, obs) + + local context = r.context return { - on_start = obs and obs.on_start or function() end, + on_start = function() + r.state = "requesting" + context._99:track_request(context) + if obs then + obs.on_start() + end + end, on_complete = function(status, res) r.state = status - r.context._99:finish_request(r.context, status) + context._99:finish_request(context, status) if obs then obs.on_complete(status, res) end @@ -108,8 +116,6 @@ function Request:start(observer) self.state == "ready", "request is not in state ready when attempting to start a request" ) - self.state = "requesting" - self.context:finalize() for _, content in ipairs(self.context.ai_context) do self:add_prompt_content(content) -- cgit v1.3-3-g829e From 5f678044e81af6525558260f8e2aa017f0b88377 Mon Sep 17 00:00:00 2001 From: Anthony Poschen Date: Tue, 17 Feb 2026 14:45:32 +1100 Subject: feat: Replace mkdir command to support recursive folder creation --- lua/99/request-context.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lua/99/request-context.lua') diff --git a/lua/99/request-context.lua b/lua/99/request-context.lua index 609fe78..987590d 100644 --- a/lua/99/request-context.lua +++ b/lua/99/request-context.lua @@ -145,7 +145,7 @@ function RequestContext:save_prompt(prompt) local dir = vim.fs.dirname(prompt_file) if dir and not vim.uv.fs_stat(dir) then - pcall(vim.uv.fs_mkdir, dir, 493) + vim.fn.mkdir(dir, "p") end local file = io.open(prompt_file, "w") -- cgit v1.3-3-g829e From 3e334a0e08cd6618d7ee50fdf7810712265a3a77 Mon Sep 17 00:00:00 2001 From: theprimeagain Date: Mon, 16 Feb 2026 12:12:35 -0700 Subject: Tutorials are ready (ish) --- lua/99/init.lua | 99 ++++++++++++++++++++++++++++++++++------------ lua/99/ops/tutorial.lua | 33 ++++++++++++++-- lua/99/prompt-settings.lua | 1 + lua/99/request-context.lua | 29 ++++++++++++-- lua/99/request/init.lua | 9 +++-- lua/99/window/init.lua | 65 ++++++++++++++++++++++++++++++ 6 files changed, 200 insertions(+), 36 deletions(-) (limited to 'lua/99/request-context.lua') diff --git a/lua/99/init.lua b/lua/99/init.lua index d5d74d1..f8755dd 100644 --- a/lua/99/init.lua +++ b/lua/99/init.lua @@ -43,18 +43,13 @@ end --- @alias _99.Cleanup fun(): nil ---- @class _99.RequestEntry.Data.Tutorial ---- @field type "tutorial" ---- @field title string ---- @field content string[] - --- @class _99.RequestEntry.Data.Search --- @field type "search" --- @class _99.RequestEntry.Data.Visual --- @field type "visual" ---- +-- luacheck: ignore --- @alias _99.RequestEntry.Data _99.RequestEntry.Data.Search | _99.RequestEntry.Data.Tutorial | _99.RequestEntry.Data.Visual --- @class _99.RequestEntry @@ -75,8 +70,6 @@ end --- @field auto_add_skills boolean --- @field provider_override _99.Providers.BaseProvider? --- @field __view_log_idx number ---- @field __tutorials _99.RequestEntry.Data.Tutorial[] ---- @field __searches _99.RequestEntry.Data.Search[] --- @field __request_history _99.RequestEntry[] --- @field __request_by_id table @@ -92,8 +85,6 @@ local function create_99_state() display_errors = false, provider_override = nil, auto_add_skills = false, - __tutorials = {}, - __searches = {}, __view_log_idx = 1, __request_history = {}, __request_by_id = {}, @@ -134,8 +125,6 @@ end --- @field rules _99.Agents.Rules --- @field __view_log_idx number --- @field __request_history _99.RequestEntry[] ---- @field __tutorials _99.RequestEntry.Data.Tutorial[] ---- @field __searches _99.RequestEntry.Data.Search[] --- @field __request_by_id table --- @field __active_marks _99.Mark[] local _99_State = {} @@ -162,6 +151,9 @@ function _99_State:refresh_rules() Extensions.refresh(self) end +--- @param tutorial _99.RequestEntry.Data.Tutorial +function _99_State:open_tutorial(tutorial) end + --- @param context _99.RequestContext --- @return _99.RequestEntry function _99_State:track_request(context) @@ -193,19 +185,12 @@ function _99_State:finish_request(context, status) end entry.status = status - local data = entry.operation_data - if entry.status == "success" and data then - if data.type == "tutorial" then - table.insert(self.__tutorials, data) - elseif data.type == "search" then - table.insert(self.__searches, data) - end - end end ---- @param id number +--- @param context _99.RequestContext ---@param data _99.RequestEntry.Data -function _99_State:add_data(id, data) +function _99_State:add_data(context, data) + local id = context.xid local entry = self.__request_by_id[id] if not entry then return @@ -239,8 +224,6 @@ function _99_State:clear_previous_requests() end end self.__request_history = keep - self.__searches = {} - self.__tutorials = {} end --- @param mark _99.Mark @@ -258,6 +241,19 @@ function _99_State:active_request_count() return count end +--- @param type "search" | "visual" | "tutorial" +--- @return _99.RequestEntry.Data +function _99_State:get_request_data_by_type(type) + local out = {} + for _, r in ipairs(self.__request_history) do + local data = r.operation_data + if data and data.type == type then + table.insert(out, data) + end + end + return out +end + local _99_state = _99_State.new() --- @class _99 @@ -282,8 +278,11 @@ end --- @param name string --- @param context _99.RequestContext --- @param opts _99.ops.Opts -local function capture_prompt(cb, name, context, opts) +--- @param capture_content string[] | nil +local function capture_prompt(cb, name, context, opts, capture_content) Window.capture_input(name, { + content = capture_content, + --- @param ok boolean --- @param response string cb = function(ok, response) @@ -340,6 +339,56 @@ function _99.info() Window.display_centered_message(info) end +--- @param tutorials _99.RequestEntry.Data.Tutorial[] +--- @return string[] +local function tutorial_to_string(tutorials) + local out = {} + for _, t in ipairs(tutorials) do + table.insert(out, string.format("%d: %s", t.xid, t.tutorial[1])) + end + return out +end + +--- @param xid number | nil +--- @param opts? _99.window.SplitWindowOpts +function _99.open_tutorial(xid, opts) + opts = opts or { split_direction = "vertical" } + if xid == nil then + local tutorials = _99_state:get_request_data_by_type("tutorial") + if #tutorials == 0 then + print("no tutorials available") + elseif #tutorials == 1 then + local data = tutorials[1].operation_data + assert(data, "tutorial is malformed") + Window.create_split(data.tutorial, data.buffer, opts) + else + local context = get_context("tutorial-lookup") + capture_prompt(function(_, o) + local response = o.additional_prompt + local lines = vim.split(response, "\n") + for _, l in ipairs(lines) do + local id = tonumber(vim.split(l, ":")[1]) + if not id then + error( + "do not alter the tutoria lines, just delete the ones you dont want" + ) + end + local tut = _99_state.__request_by_id[id] + local data = tut and tut.operation_data + assert(data and data.type == "tutorial", "invalid tutorial selected") + Window.create_split(data.tutorial, data.buffer, opts) + end + end, "Select Tutorial", context, {}, tutorial_to_string(tutorials)) + end + return + end + + local tutorial = _99_state.__request_by_id[xid] + local data = tutorial and tutorial.operation_data + assert(data and data.type == "tutorial", "cannot open a non tutorial") + Window.create_split(data.tutorial, data.buffer, opts) +end + --- @param path string function _99:rule_from_path(path) _ = self diff --git a/lua/99/ops/tutorial.lua b/lua/99/ops/tutorial.lua index 10d3530..320366f 100644 --- a/lua/99/ops/tutorial.lua +++ b/lua/99/ops/tutorial.lua @@ -1,10 +1,36 @@ local Request = require("99.request") local CleanUp = require("99.ops.clean-up") +local Window = require("99.window") local make_prompt = require("99.ops.make-prompt") local make_clean_up = CleanUp.make_clean_up local make_observer = CleanUp.make_observer ---- @class _99.Tutorial.Result + +--- @class _99.RequestEntry.Data.Tutorial +--- @field type "tutorial" +--- @field buffer number +--- @field window number +--- @field xid number +--- @field tutorial string[] + +--- @param context _99.RequestContext +---@param response string +---@return _99.RequestEntry.Data.Tutorial +local function open_tutorial(context, response) + local content = vim.split(response, "\n") + local win = Window.create_split(content) + + --- @type _99.RequestEntry.Data.Tutorial + local data = { + type = "tutorial", + buffer = win.buffer, + window = win.win, + xid = context.xid, + tutorial = content, + } + context._99:add_data(context, data) + return data +end --- @param context _99.RequestContext ---@param opts _99.ops.Opts @@ -22,6 +48,7 @@ local function tutorial(context, opts) local prompt, refs = make_prompt(context, context._99.prompts.prompts.tutorial(), opts) + context:add_references(refs) request:add_prompt_content(prompt) context:add_clean_up(clean_up) @@ -37,9 +64,9 @@ local function tutorial(context, opts) response or "no response provided" ) elseif status == "success" then - error("what the hell") + local data = open_tutorial(context, response) + context._99:open_tutorial(data) end end)) - end return tutorial diff --git a/lua/99/prompt-settings.lua b/lua/99/prompt-settings.lua index 9950a86..03f82a2 100644 --- a/lua/99/prompt-settings.lua +++ b/lua/99/prompt-settings.lua @@ -24,6 +24,7 @@ Once you have fetched all the relavent content, review it thoroughly before crafting the tutorial The response format must be valid Markdown +The first line of the response must be the title of the tutorial ]] end, semantic_search = function() diff --git a/lua/99/request-context.lua b/lua/99/request-context.lua index ba76519..02ac0e1 100644 --- a/lua/99/request-context.lua +++ b/lua/99/request-context.lua @@ -112,8 +112,9 @@ function RequestContext:content() return self.ai_context end ---- @param prompt string -function RequestContext:save_prompt(prompt) +--- @return boolean +function RequestContext:_ready_request_files() + local response_file = self.tmp_file local prompt_file = self.tmp_file .. "-prompt" local dir = vim.fs.dirname(prompt_file) @@ -122,6 +123,23 @@ function RequestContext:save_prompt(prompt) pcall(vim.uv.fs_mkdir, dir, 493) end + local files = { prompt_file, response_file } + for _, f in ipairs(files) do + local file = io.open(f, "w") + if file then + file:write("") + file:close() + else + self.logger:error("unable to create prompt file") + return false + end + end + return true +end + +--- @param prompt string +function RequestContext:save_prompt(prompt) + local prompt_file = self.tmp_file .. "-prompt" local file = io.open(prompt_file, "w") if file then file:write(prompt) @@ -132,8 +150,11 @@ function RequestContext:save_prompt(prompt) end end ---- @return self +--- @return boolean, self function RequestContext:finalize() + if self:_ready_request_files() == false then + return false, self + end self:_read_md_files() if self.range then table.insert(self.ai_context, self._99.prompts.get_file_location(self)) @@ -143,7 +164,7 @@ function RequestContext:finalize() self.ai_context, self._99.prompts.tmp_file_location(self.tmp_file) ) - return self + return true, self end function RequestContext:clear_marks() diff --git a/lua/99/request/init.lua b/lua/99/request/init.lua index d057879..f8000ae 100644 --- a/lua/99/request/init.lua +++ b/lua/99/request/init.lua @@ -80,8 +80,7 @@ end --- @param r _99.Request --- @param obs _99.Providers.Observer | nil local function observer_from_request(r, obs) - - local context = r.context + local context = r.context return { on_start = function() r.state = "requesting" @@ -116,12 +115,14 @@ function Request:start(observer) self.state == "ready", "request is not in state ready when attempting to start a request" ) - self.context:finalize() + local ok = self.context:finalize() + self.logger:assert(ok, "request has failed due to context finalization: check logs for more details") + for _, content in ipairs(self.context.ai_context) do self:add_prompt_content(content) end - local prompt = table.concat(self._content, "\n") + self.context:save_prompt(prompt) self.logger:debug("start", "prompt", prompt) self.provider:make_request( diff --git a/lua/99/window/init.lua b/lua/99/window/init.lua index b4b1cb4..d7018c8 100644 --- a/lua/99/window/init.lua +++ b/lua/99/window/init.lua @@ -27,6 +27,10 @@ local nvim_buf_is_valid = vim.api.nvim_buf_is_valid --- @field win_id number --- @field buf_id number +--- @class _99.window.SplitWindow +--- @field win number +--- @field buffer number + --- @param lines string[] --- @return string[] local function ensure_no_new_lines(lines) @@ -347,6 +351,7 @@ end --- @class _99.window.CaptureInputOpts --- @field cb fun(success: boolean, result: string): nil --- @field on_load? fun(): nil +--- @field content? string[] --- @field rules _99.Agents.Rules --- @param name string @@ -423,6 +428,16 @@ function M.capture_input(name, opts) if opts.on_load then vim.schedule(opts.on_load) end + + if opts.content then + vim.api.nvim_buf_set_lines( + win.buf_id, + 0, + -1, + false, + ensure_no_new_lines(opts.content) + ) + end end function M.clear_active_popups() @@ -493,4 +508,54 @@ function M.close(win) end end end + +--- @class _99.window.SplitWindowOpts +--- @field split_direction "vertical" | "horizontal" | nil +--- @field filetype string + +--- @param content string[] +---@param buffer number | nil +---@param opts _99.window.SplitWindowOpts | nil +--- @return _99.window.SplitWindow +function M.create_split(content, buffer, opts) + opts = opts or { split_direction = "vertical" } + + opts.split_direction = opts.split_direction or "vertical" + opts.filetype = opts.filetype or "markdown" + + local split_direction = opts.split_direction + assert( + split_direction == "vertical" or split_direction == "horizontal", + "unknown split direction: " + .. vim.inspect(split_direction) + .. " : must be horizontal or vertical" + ) + + if split_direction == "horizontal" then + vim.cmd("split") + else + vim.cmd("vsplit") + end + + local win_id = vim.api.nvim_get_current_win() + local buf_id = buffer + if not buf_id or not nvim_buf_is_valid(buf_id) then + buf_id = vim.api.nvim_create_buf(false, false) + vim.api.nvim_buf_set_lines( + buf_id, + 0, + -1, + false, + ensure_no_new_lines(content) + ) + end + + vim.api.nvim_win_set_buf(win_id, buf_id) + vim.bo[buf_id].filetype = opts.filetype + + return { + win = win_id, + buffer = buf_id, + } +end return M -- cgit v1.3-3-g829e