summaryrefslogtreecommitdiff
path: root/lua
diff options
context:
space:
mode:
authorWayne-Cole <77279425+Wacky404@users.noreply.github.com>2026-06-07 17:45:22 -0500
committerWayne-Cole <77279425+Wacky404@users.noreply.github.com>2026-06-07 17:45:22 -0500
commit53c60a9213f4e899bc411373ea2f0c20cf716826 (patch)
tree63001d83197db89f61890467740db940645fb2ca /lua
parent4d229141546290746c82ac90f5afc2786865b5f3 (diff)
downloada4-53c60a9213f4e899bc411373ea2f0c20cf716826.tar.xz
a4-53c60a9213f4e899bc411373ea2f0c20cf716826.zip
feat: huge refactor, still broken requests
Diffstat (limited to 'lua')
-rw-r--r--lua/99/example_response.json36
-rw-r--r--lua/99/extensions/completions.lua76
-rw-r--r--lua/99/init.lua469
-rw-r--r--lua/99/logger/logger.lua373
-rw-r--r--lua/99/ops/clean-up.lua74
-rw-r--r--lua/99/ops/init.lua10
-rw-r--r--lua/99/ops/make-prompt.lua44
-rw-r--r--lua/99/ops/over-range.lua155
-rw-r--r--lua/99/prompt-settings.lua151
-rw-r--r--lua/99/prompt.lua568
-rw-r--r--lua/99/providers.lua498
-rw-r--r--lua/99/state.lua94
12 files changed, 1359 insertions, 1189 deletions
diff --git a/lua/99/example_response.json b/lua/99/example_response.json
new file mode 100644
index 0000000..7497461
--- /dev/null
+++ b/lua/99/example_response.json
@@ -0,0 +1,36 @@
+{
+ "choices": [
+ {
+ "finish_reason": "stop",
+ "index": 0,
+ "message": {
+ "role": "assistant",
+ "content": "```go\nfunc reverseString(s string) string {\n runes := []rune(s)\n for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {\n runes[i], runes[j] = runes[j], runes[i]\n }\n return string(runes)\n}\n```"
+ }
+ }
+ ],
+ "created": 1780788059,
+ "model": "qwen2.5-coder-3b-instruct-q8_0.gguf",
+ "system_fingerprint": "b9381-91eb8ffa",
+ "object": "chat.completion",
+ "usage": {
+ "completion_tokens": 75,
+ "prompt_tokens": 128,
+ "total_tokens": 203,
+ "prompt_tokens_details": {
+ "cached_tokens": 127
+ }
+ },
+ "id": "chatcmpl-O7YWbG9gzzlOtN1NXT955UIkhTJ8X1Y6",
+ "timings": {
+ "cache_n": 127,
+ "prompt_n": 1,
+ "prompt_ms": 138.134,
+ "prompt_per_token_ms": 138.134,
+ "prompt_per_second": 7.239347300447393,
+ "predicted_n": 75,
+ "predicted_ms": 10140.874,
+ "predicted_per_token_ms": 135.21165333333332,
+ "predicted_per_second": 7.395812234724541
+ }
+}
diff --git a/lua/99/extensions/completions.lua b/lua/99/extensions/completions.lua
index 4eeec08..742f6e3 100644
--- a/lua/99/extensions/completions.lua
+++ b/lua/99/extensions/completions.lua
@@ -18,70 +18,74 @@ local M = {}
--- @param str string The string to escape
--- @return string The escaped string safe for use in Lua patterns
function M.escape_pattern(str)
- return str:gsub("([%%%^%$%(%)%.%[%]%*%+%-%?])", "%%%1")
+ return str:gsub("([%%%^%$%(%)%.%[%]%*%+%-%?])", "%%%1")
end
--- @param provider _99.CompletionProvider
function M.register(provider)
- for i, p in ipairs(providers) do
- if p.trigger == provider.trigger then
- providers[i] = provider
- return
+ for i, p in ipairs(providers) do
+ if p.trigger == provider.trigger then
+ providers[i] = provider
+ return
+ end
end
- end
- table.insert(providers, provider)
+ table.insert(providers, provider)
end
--- @return string[]
function M.get_trigger_characters()
- local chars = {}
- for _, p in ipairs(providers) do
- table.insert(chars, p.trigger)
- end
- return chars
+ local chars = {}
+ for _, p in ipairs(providers) do
+ table.insert(chars, p.trigger)
+ end
+ return chars
end
--- @return string
function M.get_keyword_pattern()
- local triggers = {}
- for _, p in ipairs(providers) do
- table.insert(triggers, p.trigger)
- end
- return "[" .. table.concat(triggers) .. "]\\k*"
+ local triggers = {}
+ for _, p in ipairs(providers) do
+ table.insert(triggers, p.trigger)
+ end
+ return "[" .. table.concat(triggers) .. "]\\k*"
end
---- @param prompt_text string
+--- @param prompt_text string|boolean|nil
--- @return _99.Reference[]
function M.parse(prompt_text)
- local refs = {}
- for _, provider in ipairs(providers) do
- local pattern = M.escape_pattern(provider.trigger) .. "%S+"
- for word in prompt_text:gmatch(pattern) do
- local token = word:sub(#provider.trigger + 1)
- if provider.is_valid(token) then
- local content = provider.resolve(token)
- if content then
- table.insert(refs, { content = content })
+ local refs = {}
+ if type(prompt_text) ~= "string" then
+ return refs
+ end
+
+ for _, provider in ipairs(providers) do
+ local pattern = M.escape_pattern(provider.trigger) .. "%S+"
+ for word in prompt_text:gmatch(pattern) do
+ local token = word:sub(#provider.trigger + 1)
+ if provider.is_valid(token) then
+ local content = provider.resolve(token)
+ if content then
+ table.insert(refs, { content = content })
+ end
+ end
end
- end
end
- end
- return refs
+ return refs
end
--- @param trigger_char string
--- @return CompletionItem[]
function M.get_completions(trigger_char)
- for _, provider in ipairs(providers) do
- if provider.trigger == trigger_char then
- return provider.get_items()
+ for _, provider in ipairs(providers) do
+ if provider.trigger == trigger_char then
+ return provider.get_items()
+ end
end
- end
- return {}
+ return {}
end
function M._reset()
- providers = {}
+ providers = {}
end
return M
diff --git a/lua/99/init.lua b/lua/99/init.lua
index 884250f..35ba93f 100644
--- a/lua/99/init.lua
+++ b/lua/99/init.lua
@@ -17,28 +17,29 @@ local Providers = require("99.providers")
---@param path_or_rule string | _99.Agents.Rule
---@return _99.Agents.Rule | string
local function expand(path_or_rule)
- if type(path_or_rule) == "string" then
- return vim.fn.expand(path_or_rule)
- end
- return {
- name = path_or_rule.name,
- path = vim.fn.expand(path_or_rule.path),
- }
+ if type(path_or_rule) == "string" then
+ return vim.fn.expand(path_or_rule)
+ end
+ return {
+ name = path_or_rule.name,
+ path = vim.fn.expand(path_or_rule.path),
+ }
end
--- @param opts _99.ops.Opts?
--- @return _99.ops.Opts
local function process_opts(opts)
- opts = opts or {}
- for i, rule in ipairs(opts.additional_rules or {}) do
- local r = expand(rule)
- assert(
- type(r) ~= "string",
- "broken configuration. additional_rules must never be a string"
- )
- opts.additional_rules[i] = r
- end
- return opts
+ opts = opts or {}
+ for i, rule in ipairs(opts.additional_rules or {}) do
+ local r = expand(rule)
+ assert(
+ type(r) ~= "string",
+ "broken configuration. additional_rules must never be a string"
+ )
+ opts.additional_rules[i] = r
+ end
+
+ return opts
end
--- @class _99.Completion
@@ -55,6 +56,7 @@ end
--- @field md_files? string[]
--- @field provider? _99.Providers.BaseProvider
--- @field provider_extra_args? string[]
+--- @field endpoint? string
--- @field display_errors? boolean
--- @field auto_add_skills? boolean
--- @field completion? _99.Completion
@@ -66,14 +68,13 @@ local _99_state
--- @alias _99.TraceID number
--- @class _99
---- 99 is an agentic workflow that is meant to meld the current programmers ability
---- with the amazing powers of LLMs. Instead of being a replacement, its meant to
---- augment the programmer.
+--- A4 is an agentic / trigger based completion workflow that is meant to meld the
+--- current programmers ability with the amazing powers of LLMs. Instead of being
+--- a replacement, its meant to augment the programmer. The power is the intentionality
+--- of the programmer, not the clanker.
---
---- As of now, the direction of 99 is to progress into agentic programming and surfacing
---- of information. In the beginning and the original youtube video was about replacing
---- specific pieces of code. The more i use 99 the more i realize the better use is
---- through `search` and `work`
+--- A4 is a fork of 99, the direction of A4 is to progress into "ghost" programming and surfacing
+--- of language information to the programmer.
---
--- ### Basic Setup
--- ```lua
@@ -213,11 +214,11 @@ local _99_state
--- @field Extensions _99.Extensions
--- check out Worker for cool abstraction on search and vibe
local _99 = {
- DEBUG = Level.DEBUG,
- INFO = Level.INFO,
- WARN = Level.WARN,
- ERROR = Level.ERROR,
- FATAL = Level.FATAL,
+ DEBUG = Level.DEBUG,
+ INFO = Level.INFO,
+ WARN = Level.WARN,
+ ERROR = Level.ERROR,
+ FATAL = Level.FATAL,
}
--- @param cb fun(context: _99.Prompt, o: _99.ops.Opts?): nil
@@ -226,56 +227,56 @@ local _99 = {
--- @param opts _99.ops.Opts
--- @param capture_content string[] | nil
local function capture_prompt(cb, name, context, opts, capture_content)
- Window.capture_input(name, {
- keymap = {
- [":w"] = "submit",
- },
- content = capture_content,
+ Window.capture_input(name, {
+ keymap = {
+ [":w"] = "submit",
+ },
+ content = capture_content,
- --- @param ok boolean
- --- @param response string
- cb = function(ok, response)
- context.logger:debug(
- "capture_prompt",
- "success",
- ok,
- "response",
- response
- )
- if not ok then
- return
- end
- local rules_and_names = Agents.by_name(_99_state.rules, response)
- opts.additional_rules = opts.additional_rules or {}
- for _, r in ipairs(rules_and_names.rules) do
- table.insert(opts.additional_rules, r)
- end
- opts.additional_prompt = response
- context.user_prompt = response
- cb(context, opts)
- end,
- on_load = function()
- Extensions.setup_buffer(_99_state)
- end,
- rules = _99_state.rules,
- })
+ --- @param ok boolean
+ --- @param response string
+ cb = function(ok, response)
+ context.logger:debug(
+ "capture_prompt",
+ "success",
+ ok,
+ "response",
+ response
+ )
+ if not ok then
+ return
+ end
+ local rules_and_names = Agents.by_name(_99_state.rules, response)
+ opts.additional_rules = opts.additional_rules or {}
+ for _, r in ipairs(rules_and_names.rules) do
+ table.insert(opts.additional_rules, r)
+ end
+ opts.additional_prompt = response
+ context.user_prompt = response
+ cb(context, opts)
+ end,
+ on_load = function()
+ Extensions.setup_buffer(_99_state)
+ end,
+ rules = _99_state.rules,
+ })
end
function _99.info()
- local info = {}
- _99_state:refresh_rules()
- table.insert(
- info,
- string.format("Previous Requests: %d", _99_state.tracking:completed())
- )
- table.insert(
- info,
- string.format("custom rules(%d):", #(_99_state.rules.custom or {}))
- )
- for _, rule in ipairs(_99_state.rules.custom or {}) do
- table.insert(info, string.format("* %s", rule.name))
- end
- Window.display_centered_message(info)
+ local info = {}
+ _99_state:refresh_rules()
+ table.insert(
+ info,
+ string.format("Previous Requests: %d", _99_state.tracking:completed())
+ )
+ table.insert(
+ info,
+ string.format("custom rules(%d):", #(_99_state.rules.custom or {}))
+ )
+ for _, rule in ipairs(_99_state.rules.custom or {}) do
+ table.insert(info, string.format("* %s", rule.name))
+ end
+ Window.display_centered_message(info)
end
-- elseif #tutorials == 1 then
@@ -286,238 +287,268 @@ end
--- @param context _99.Prompt
function _99.open_tutorial(context)
- local tutorial = context:tutorial_data()
- Window.create_split(tutorial.tutorial, tutorial.buffer, {
- split_direction = "vertical",
- window_opts = {
- wrap = true,
- },
- })
+ local tutorial = context:tutorial_data()
+ Window.create_split(tutorial.tutorial, tutorial.buffer, {
+ split_direction = "vertical",
+ window_opts = {
+ wrap = true,
+ },
+ })
end
function _99.open()
- local requests = _99_state.tracking:successful()
- local str_requests = Tracking.to_selectable_list(requests)
- select_window(str_requests, function(idx)
- local r = requests[idx]
- assert(r:valid(), "encountered unexpected issue. malformated data")
- if r.operation == "visual" then
- --- TODO: this is its own work item for being able to have a global mark
- --- section in which i keep track of marks for the lifetime of the
- --- editor and when you close the editor, then it should lose them
- print("visual not supported: i will figure this out... at some point")
- elseif r.operation == "search" or r.operation == "vibe" then
- _99.open_qfix_for_request(r)
- elseif r.operation == "tutorial" then
- _99.open_tutorial(r)
- end
- end)
+ local requests = _99_state.tracking:successful()
+ local str_requests = Tracking.to_selectable_list(requests)
+ select_window(str_requests, function(idx)
+ local r = requests[idx]
+ assert(r:valid(), "encountered unexpected issue. malformated data")
+ if r.operation == "visual" then
+ --- TODO: this is its own work item for being able to have a global mark
+ --- section in which i keep track of marks for the lifetime of the
+ --- editor and when you close the editor, then it should lose them
+ print(
+ "visual not supported: i will figure this out... at some point"
+ )
+ elseif r.operation == "search" or r.operation == "vibe" then
+ _99.open_qfix_for_request(r)
+ elseif r.operation == "tutorial" then
+ _99.open_tutorial(r)
+ end
+ end)
end
--- @param opts? _99.ops.Opts
--- @return _99.TraceID
function _99.vibe(opts)
- local o = process_opts(opts)
- local context = Prompt.vibe(_99_state)
- if o.additional_prompt then
- context.user_prompt = o.additional_prompt
- ops.vibe(context, o)
- else
- capture_prompt(ops.vibe, "Vibe", context, o)
- end
- return context.xid
+ local o = process_opts(opts)
+ local context = Prompt.vibe(_99_state)
+ if o.additional_prompt then
+ context.user_prompt = o.additional_prompt
+ ops.vibe(context, o)
+ else
+ capture_prompt(ops.vibe, "Vibe", context, o)
+ end
+ return context.xid
end
--- @param opts? _99.ops.SearchOpts
--- @return _99.TraceID
function _99.search(opts)
- local o = process_opts(opts) --[[ @as _99.ops.SearchOpts ]]
- local context = Prompt.search(_99_state)
- if o.additional_prompt then
- context.user_prompt = o.additional_prompt
- ops.search(context, o)
- else
- capture_prompt(ops.search, "Search", context, o)
- end
- return context.xid
+ local o = process_opts(opts) --[[ @as _99.ops.SearchOpts ]]
+ local context = Prompt.search(_99_state)
+ if o.additional_prompt then
+ context.user_prompt = o.additional_prompt
+ ops.search(context, o)
+ else
+ capture_prompt(ops.search, "Search", context, o)
+ end
+ return context.xid
end
--- @param opts _99.ops.Opts
function _99.tutorial(opts)
- opts = process_opts(opts)
- local context = Prompt.tutorial(_99_state)
- if opts.additional_prompt then
- context.user_prompt = opts.additional_prompt
- ops.tutorial(context, opts)
- else
- capture_prompt(ops.tutorial, "Tutorial", context, opts)
- end
+ opts = process_opts(opts)
+ local context = Prompt.tutorial(_99_state)
+ if opts.additional_prompt then
+ context.user_prompt = opts.additional_prompt
+ ops.tutorial(context, opts)
+ else
+ capture_prompt(ops.tutorial, "Tutorial", context, opts)
+ end
end
--- @param opts _99.ops.Opts?
--- @return _99.TraceID
function _99.visual(opts)
- opts = process_opts(opts)
- local context = Prompt.visual(_99_state)
- if opts.additional_prompt then
- context.user_prompt = opts.additional_prompt
- ops.over_range(context, opts)
- else
- capture_prompt(ops.over_range, "Visual", context, opts)
- end
- return context.xid
+ opts = process_opts(opts)
+ local context = Prompt.visual(_99_state) --[[ @as _99.Prompt ]]
+
+ if opts.additional_prompt then
+ if type(opts.additional_prompt) == "string" then
+ context.user_prompt = string(opts.additional_prompt)
+ elseif type(opts.additional_prompt) == "boolean" then
+ capture_prompt(ops.over_range, "Visual", context, opts)
+ end
+ else
+ ops.over_range(context, opts)
+ end
+
+ return context.xid
end
function _99.view_logs()
- local requests = _99_state.tracking.history
- local str_requests = Tracking.to_selectable_list(requests)
- select_window(str_requests, function(idx)
- local r = requests[idx]
- local logs = Logger.logs_by_id(r.xid)
- if logs == nil then
- logs = { "No logs found for request: " .. r.xid }
- end
- Window.display_full_screen_message(logs)
- end)
+ local requests = _99_state.tracking.history
+ local str_requests = Tracking.to_selectable_list(requests)
+ select_window(str_requests, function(idx)
+ local r = requests[idx]
+ local logs = Logger.logs_by_id(r.xid)
+ if logs == nil then
+ logs = { "No logs found for request: " .. r.xid }
+ end
+ Window.display_full_screen_message(logs)
+ end)
end
--- @param request _99.Prompt
function _99.open_qfix_for_request(request)
- local items = request:qfix_data()
- if #items == 0 then
- print("there are no quickfix items to show")
- return
- end
+ local items = request:qfix_data()
+ if #items == 0 then
+ print("there are no quickfix items to show")
+ return
+ end
- vim.fn.setqflist({}, "r", { title = "99 Results", items = items })
- vim.cmd("copen")
+ vim.fn.setqflist({}, "r", { title = "99 Results", items = items })
+ vim.cmd("copen")
end
function _99.stop_all_requests()
- _99_state.tracking:stop_all_requests()
+ _99_state.tracking:stop_all_requests()
end
function _99.clear_previous_requests()
- _99_state.tracking:clear_history()
+ _99_state.tracking:clear_history()
end
--- if you touch this function you will be fired
--- @return _99.State
function _99.__get_state()
- return _99_state
+ return _99_state
end
--- @param opts _99.Options?
function _99.setup(opts)
- opts = opts or {}
+ opts = opts or {}
- _99_state = State.new(opts)
+ _99_state = State.new(opts)
- local crules = _99_state.completion.custom_rules
- for i, rule in ipairs(crules) do
- local str = expand(rule)
- assert(type(str) == "string", "error parsing rule: path must be a string")
- crules[i] = str
- end
+ local crules = _99_state.completion.custom_rules
+ for i, rule in ipairs(crules) do
+ local str = expand(rule)
+ assert(
+ type(str) == "string",
+ "error parsing rule: path must be a string"
+ )
+ crules[i] = str
+ end
- vim.api.nvim_create_autocmd("VimLeavePre", {
- callback = function()
- _99.stop_all_requests()
- _99_state:sync()
- end,
- })
+ vim.api.nvim_create_autocmd("VimLeavePre", {
+ callback = function()
+ _99.stop_all_requests()
+ _99_state:sync()
+ end,
+ })
- Logger:configure(opts.logger)
+ Logger:configure(opts.logger)
- if opts.model then
- assert(type(opts.model) == "string", "opts.model is not a string")
- _99_state.model = opts.model
- else
- local provider = opts.provider or Providers.OpenCodeProvider
- if provider._get_default_model then
- _99_state.model = provider._get_default_model()
+ if opts.model then
+ assert(type(opts.model) == "string", "opts.model is not a string")
+ _99_state.model = opts.model
+ else
+ local provider = opts.provider or Providers.BareMetalProvider
+ if provider._get_default_model then
+ _99_state.model = provider._get_default_model()
+ end
end
- end
- if opts.provider_extra_args then
- assert(
- type(opts.provider_extra_args) == "table",
- "opts.provider_extra_args must be a table"
- )
- end
+ if opts.endpoint then
+ if type(opts.endpoint) ~= "string" then
+ vim.notify(
+ "config error: opts.endpoint must be a string",
+ vim.log.levels.ERROR
+ )
+ return false
+ end
- if opts.md_files then
- assert(type(opts.md_files) == "table", "opts.md_files is not a table")
- for _, md in ipairs(opts.md_files) do
- _99.add_md_file(md)
+ _99_state.endpoint = opts.endpoint
+ else
+ if opts.provider == _99.Providers.BareMetalProvider then
+ vim.notify(
+ "config error: opts.endpoint must be set for BareMetalProvider",
+ vim.log.levels.ERROR
+ )
+ return false
+ end
end
- end
- if opts.tmp_dir then
- assert(type(opts.tmp_dir) == "string", "opts.tmp_dir must be a string")
- end
- _99_state.__tmp_dir = opts.tmp_dir
+ if opts.provider_extra_args then
+ assert(
+ type(opts.provider_extra_args) == "table",
+ "opts.provider_extra_args must be a table"
+ )
+ end
+
+ if opts.md_files then
+ assert(type(opts.md_files) == "table", "opts.md_files is not a table")
+ for _, md in ipairs(opts.md_files) do
+ _99.add_md_file(md)
+ end
+ end
- _99_state.display_errors = opts.display_errors or false
- _99_state:refresh_rules()
- Extensions.init(_99_state)
- Extensions.capture_project_root()
+ if opts.tmp_dir then
+ assert(type(opts.tmp_dir) == "string", "opts.tmp_dir must be a string")
+ end
+ _99_state.__tmp_dir = opts.tmp_dir
- local sw = StatusWindow.new(_99_state, opts.in_flight_options)
- sw:start()
+ _99_state.display_errors = opts.display_errors or false
+ _99_state:refresh_rules()
+ Extensions.init(_99_state)
+ Extensions.capture_project_root()
+
+ local sw = StatusWindow.new(_99_state, opts.in_flight_options)
+ sw:start()
end
--- @param md string
--- @return _99
function _99.add_md_file(md)
- table.insert(_99_state.md_files, md)
- return _99
+ table.insert(_99_state.md_files, md)
+ return _99
end
--- @param md string
--- @return _99
function _99.rm_md_file(md)
- for i, name in ipairs(_99_state.md_files) do
- if name == md then
- table.remove(_99_state.md_files, i)
- break
+ for i, name in ipairs(_99_state.md_files) do
+ if name == md then
+ table.remove(_99_state.md_files, i)
+ break
+ end
end
- end
- return _99
+ return _99
end
--- @param model string
--- @return _99
function _99.set_model(model)
- _99_state.model = model
- return _99
+ _99_state.model = model
+ return _99
end
--- @return string
function _99.get_model()
- return _99_state.model
+ return _99_state.model
end
--- @return _99.Providers.BaseProvider
function _99.get_provider()
- return _99_state.provider_override or Providers.OpenCodeProvider
+ return _99_state.provider_override or Providers.OpenCodeProvider
end
--- @param provider _99.Providers.BaseProvider
--- @return _99
function _99.set_provider(provider)
- _99_state.provider_override = provider
- if provider._get_default_model then
- _99_state.model = provider._get_default_model()
- end
- return _99
+ _99_state.provider_override = provider
+ if provider._get_default_model then
+ _99_state.model = provider._get_default_model()
+ end
+ return _99
end
function _99.__debug()
- Logger:configure({
- path = nil,
- level = Level.DEBUG,
- })
+ Logger:configure({
+ path = nil,
+ level = Level.DEBUG,
+ })
end
_99.Providers = Providers
@@ -525,6 +556,6 @@ _99.Providers = Providers
--- @class _99.Extensions
--- @field Worker _99.Extensions.Worker
_99.Extensions = {
- Worker = require("99.extensions.work.worker"),
+ Worker = require("99.extensions.work.worker"),
}
return _99
diff --git a/lua/99/logger/logger.lua b/lua/99/logger/logger.lua
index daf65fa..f5901bf 100644
--- a/lua/99/logger/logger.lua
+++ b/lua/99/logger/logger.lua
@@ -1,6 +1,6 @@
local levels = require("99.logger.level")
local time = require("99.time")
-local MAX_REQUEST_DEFAULT = 5
+local MAX_REQUEST_DEFAULT = 3
--- @type table<number, _99.Logger.RequestLogs>
local logger_cache = {}
@@ -18,29 +18,29 @@ local max_requests_in_logger_cache = MAX_REQUEST_DEFAULT
--- @param ... any
--- @return table<string, any>
local function to_args(...)
- local count = select("#", ...)
- local out = {}
- assert(
- count % 2 == 0,
- "you cannot call logging with an odd number of args. e.g: msg, [k, v]..."
- )
- for i = 1, count, 2 do
- local key = select(i, ...)
- local value = select(i + 1, ...)
- assert(type(key) == "string", "keys in logging must be strings")
- assert(out[key] == nil, "key collision in logs: " .. key)
- out[key] = value
- end
- return out
+ local count = select("#", ...)
+ local out = {}
+ assert(
+ count % 2 == 0,
+ "you cannot call logging with an odd number of args. e.g: msg, [k, v]..."
+ )
+ for i = 1, count, 2 do
+ local key = select(i, ...)
+ local value = select(i + 1, ...)
+ assert(type(key) == "string", "keys in logging must be strings")
+ assert(out[key] == nil, "key collision in logs: " .. key)
+ out[key] = value
+ end
+ return out
end
--- @param log_statement table<string, any>
--- @param args table<string, any>
local function put_args(log_statement, args)
- for k, v in pairs(args) do
- assert(log_statement[k] == nil, "key collision in logs: " .. k)
- log_statement[k] = v
- end
+ for k, v in pairs(args) do
+ assert(log_statement[k] == nil, "key collision in logs: " .. k)
+ log_statement[k] = v
+ end
end
--- @class LoggerSink
@@ -51,12 +51,12 @@ local VoidSink = {}
VoidSink.__index = VoidSink
function VoidSink.new()
- return setmetatable({}, VoidSink)
+ return setmetatable({}, VoidSink)
end
--- @param _ string
function VoidSink:write_line(_)
- _ = self
+ _ = self
end
--- @class FileSink : LoggerSink
@@ -67,27 +67,29 @@ FileSink.__index = FileSink
--- @param path string
--- @return LoggerSink
function FileSink:new(path)
- -- Ensure the directory is already there (*thanks Windows*)
- vim.fn.mkdir(vim.fn.fnamemodify(path, ":h"), "p")
+ -- Ensure the directory is already there (*thanks Windows*)
+ -- normalize the path for cases of ~ and \\ etc
+ local normpath = vim.fs.normalize(path)
+ vim.fn.mkdir(vim.fn.fnamemodify(normpath, ":h"), "p")
- -- 420 decimal == 644 octal (rw-r--r--)
- local fd, err = vim.uv.fs_open(path, "w", 420)
- if not fd then
- error("unable to file sink: " .. err)
- end
+ -- 420 decimal == 644 octal (rw-r--r--)
+ local fd, err = vim.uv.fs_open(normpath, "w", 420)
+ if not fd then
+ error("unable to file sink: " .. err)
+ end
- return setmetatable({
- fd = fd,
- }, self)
+ return setmetatable({
+ fd = fd,
+ }, self)
end
--- @param str string
function FileSink:write_line(str)
- local success, err = vim.uv.fs_write(self.fd, str .. "\n")
- if not success then
- error("unable to write to file sink", err)
- end
- vim.uv.fs_fsync(self.fd)
+ local success, err = vim.uv.fs_write(self.fd, str .. "\n")
+ if not success then
+ error("unable to write to file sink", err)
+ end
+ vim.uv.fs_fsync(self.fd)
end
--- @class PrintSink : LoggerSink
@@ -96,13 +98,13 @@ PrintSink.__index = PrintSink
--- @return LoggerSink
function PrintSink:new()
- return setmetatable({}, self)
+ return setmetatable({}, self)
end
--- @param str string
function PrintSink:write_line(str)
- local _ = self
- print(str)
+ local _ = self
+ print(str)
end
--- @class _99.Logger.RequestLogs
@@ -120,265 +122,274 @@ Logger.__index = Logger
--- @param level number?
--- @return _99.Logger
function Logger:new(level)
- level = level or levels.FATAL
- return setmetatable({
- sink = VoidSink:new(),
- level = level,
- print_on_error = false,
- extra_params = {},
- }, self)
+ level = level or levels.FATAL
+
+ return setmetatable({
+ sink = VoidSink:new(),
+ level = level,
+ print_on_error = false,
+ extra_params = {},
+ }, self)
end
--- @return _99.Logger
function Logger:clone()
- local params = {}
- for k, v in pairs(self.extra_params) do
- params[k] = v
- end
- return setmetatable({
- sink = self.sink,
- level = self.level,
- print_on_error = self.print_on_error,
- extra_params = params,
- }, Logger)
+ local params = {}
+ for k, v in pairs(self.extra_params) do
+ params[k] = v
+ end
+ return setmetatable({
+ sink = self.sink,
+ level = self.level,
+ print_on_error = self.print_on_error,
+ extra_params = params,
+ }, Logger)
end
--- @param path string
--- @return _99.Logger
function Logger:file_sink(path)
- self.sink = FileSink:new(path)
- return self
+ self.sink = FileSink:new(path)
+ return self
end
--- @return _99.Logger
function Logger:void_sink()
- self.sink = VoidSink:new()
- return self
+ self.sink = VoidSink:new()
+ return self
end
--- @return _99.Logger
function Logger:print_sink()
- self.sink = PrintSink:new()
- return self
+ self.sink = PrintSink:new()
+ return self
end
--- @param area string
--- @return _99.Logger
function Logger:set_area(area)
- local new_logger = self:clone()
- new_logger.extra_params["Area"] = area
- return new_logger
+ local new_logger = self:clone()
+ new_logger.extra_params["Area"] = area
+ return new_logger
end
--- @param xid number
--- @return _99.Logger
function Logger:set_id(xid)
- local new_logger = self:clone()
- new_logger.extra_params["id"] = xid
- return new_logger
+ local new_logger = self:clone()
+ new_logger.extra_params["id"] = xid
+ return new_logger
end
--- @param level number
--- @return _99.Logger
function Logger:set_level(level)
- self.level = level
- return self
+ self.level = level
+ return self
end
--- @return _99.Logger
function Logger:on_error_print_message()
- self.print_on_error = true
- return self
+ self.print_on_error = true
+ return self
end
--- @param opts _99.Logger.Options?
function Logger:configure(opts)
- if not opts then
- return
- end
+ if not opts then
+ return
+ end
+
+ if opts.level then
+ self:set_level(opts.level)
+ end
- if opts.level then
- self:set_level(opts.level)
- end
+ if opts.type == "print" then
+ self:print_sink()
+ elseif opts.type == "file" then
+ assert(
+ opts.path,
+ "if you choose file for logger, you must have a path specified"
+ )
+ self:file_sink(opts.path)
+ else
+ if opts.path then
+ vim.notify(
+ "config error: opts.path set but no opts.type, defaulting to void logger",
+ vim.log.levels.ERROR
+ )
+ end
- if opts.type == "print" then
- self:print_sink()
- elseif opts.type == "file" then
- assert(
- opts.path,
- "if you choose file for logger, you must have a path specified"
- )
- self:file_sink(opts.path)
- else
- self:void_sink()
- end
+ self:void_sink()
+ end
- if opts.print_on_error then
- self:on_error_print_message()
- end
+ if opts.print_on_error then
+ self:on_error_print_message()
+ end
- max_requests_in_logger_cache = opts.max_requests_cached or MAX_REQUEST_DEFAULT
+ max_requests_in_logger_cache = opts.max_requests_cached
+ or MAX_REQUEST_DEFAULT
end
--- @param line string
function Logger:_cache_log(line)
- local id = self.extra_params.id
- if not id then
- return
- end
+ local id = self.extra_params.id
+ if not id then
+ return
+ end
- local cache = logger_cache[id]
- local new_cache = false
- if not cache then
- cache = {
- last_access = time.now(),
- logs = {},
- }
- logger_cache[id] = cache
- table.insert(logger_list, id)
- new_cache = true
- end
- cache.last_access = time.now()
- table.insert(cache.logs, line)
- table.sort(logger_list, function(a, b)
- assert(
- logger_cache[a] and logger_cache[b],
- "logger list is out of sync with logger cache: "
- .. tostring(a)
- .. " and "
- .. tostring(b)
- )
- local a_time = logger_cache[a].last_access
- local b_time = logger_cache[b].last_access
- return a_time > b_time
- end)
+ local cache = logger_cache[id]
+ local new_cache = false
+ if not cache then
+ cache = {
+ last_access = time.now(),
+ logs = {},
+ }
+ logger_cache[id] = cache
+ table.insert(logger_list, id)
+ new_cache = true
+ end
+ cache.last_access = time.now()
+ table.insert(cache.logs, line)
+ table.sort(logger_list, function(a, b)
+ assert(
+ logger_cache[a] and logger_cache[b],
+ "logger list is out of sync with logger cache: "
+ .. tostring(a)
+ .. " and "
+ .. tostring(b)
+ )
+ local a_time = logger_cache[a].last_access
+ local b_time = logger_cache[b].last_access
+ return a_time > b_time
+ end)
- if not new_cache then
- return
- end
+ if not new_cache then
+ return
+ end
- Logger._trim_cache()
+ Logger._trim_cache()
end
--- This is a _TEST ONLY_ function. you should not call this function outside
--- of unit tests
function Logger.reset()
- logger_cache = {}
- max_requests_in_logger_cache = MAX_REQUEST_DEFAULT
+ logger_cache = {}
+ max_requests_in_logger_cache = MAX_REQUEST_DEFAULT
end
--- @return string[][]
function Logger.logs()
- local out = {}
- for _, id in ipairs(logger_list) do
- local request_logs = logger_cache[id]
- table.insert(out, request_logs.logs)
- end
- return out
+ local out = {}
+ for _, id in ipairs(logger_list) do
+ local request_logs = logger_cache[id]
+ table.insert(out, request_logs.logs)
+ end
+ return out
end
--- @param xid number
--- @return string[] | nil
function Logger.logs_by_id(xid)
- local logs = logger_cache[xid]
- return logs and logs.logs
+ local logs = logger_cache[xid]
+ return logs and logs.logs
end
--- @param level number
---@param msg string
---@param ... any
function Logger:_log(level, msg, ...)
- if self.level > level then
- return
- end
+ if self.level > level then
+ return
+ end
- local log_statement = {
- level = levels.levelToString(level),
- msg = msg,
- }
+ local log_statement = {
+ level = levels.levelToString(level),
+ msg = msg,
+ }
- put_args(log_statement, to_args(...))
- put_args(log_statement, self.extra_params)
+ put_args(log_statement, to_args(...))
+ put_args(log_statement, self.extra_params)
- assert(log_statement["id"], "every log must have an id associated with it")
+ assert(log_statement["id"], "every log must have an id associated with it")
- local json_string = vim.json.encode(log_statement)
- if self.print_on_error and level == levels.ERROR then
- print(json_string)
- end
+ local json_string = vim.json.encode(log_statement)
+ if self.print_on_error and level == levels.ERROR then
+ print(json_string)
+ end
- self:_cache_log(json_string)
- self.sink:write_line(json_string)
+ self:_cache_log(json_string)
+ self.sink:write_line(json_string)
end
--- @param msg string
--- @param ... any
function Logger:info(msg, ...)
- self:_log(levels.INFO, msg, ...)
+ self:_log(levels.INFO, msg, ...)
end
--- @param msg string
--- @param ... any
function Logger:warn(msg, ...)
- self:_log(levels.WARN, msg, ...)
+ self:_log(levels.WARN, msg, ...)
end
--- @param msg string
--- @param ... any
function Logger:debug(msg, ...)
- self:_log(levels.DEBUG, msg, ...)
+ self:_log(levels.DEBUG, msg, ...)
end
--- @param msg string
--- @param ... any
function Logger:error(msg, ...)
- self:_log(levels.ERROR, msg, ...)
+ self:_log(levels.ERROR, msg, ...)
end
--- @param msg string
--- @param ... any
function Logger:fatal(msg, ...)
- self:_log(levels.FATAL, msg, ...)
- assert(false, "fatal msg recieved: " .. msg, ...)
+ self:_log(levels.FATAL, msg, ...)
+ assert(false, "fatal msg recieved: " .. msg, ...)
end
--- @param test any
---@param msg string
---@param ... any
function Logger:assert(test, msg, ...)
- if not test then
- self:fatal(msg, ...)
- end
+ if not test then
+ self:fatal(msg, ...)
+ end
end
function Logger._trim_cache()
- local count = 0
- local oldest = nil
- local oldest_key = nil
- for k, log in pairs(logger_cache) do
- if oldest == nil or log.last_access < oldest.last_access then
- oldest = log
- oldest_key = k
+ local count = 0
+ local oldest = nil
+ local oldest_key = nil
+ for k, log in pairs(logger_cache) do
+ if oldest == nil or log.last_access < oldest.last_access then
+ oldest = log
+ oldest_key = k
+ end
+ count = count + 1
end
- count = count + 1
- end
- if count > max_requests_in_logger_cache then
- assert(oldest_key, "oldest key must exist")
- logger_cache[oldest_key] = nil
+ if count > max_requests_in_logger_cache then
+ assert(oldest_key, "oldest key must exist")
+ logger_cache[oldest_key] = nil
- for i, id in ipairs(logger_list) do
- if id == oldest_key then
- table.remove(logger_list, i)
- break
- end
+ for i, id in ipairs(logger_list) do
+ if id == oldest_key then
+ table.remove(logger_list, i)
+ break
+ end
+ end
end
- end
end
function Logger.set_max_cached_requests(count)
- max_requests_in_logger_cache = count
- Logger._trim_cache()
+ max_requests_in_logger_cache = count
+ Logger._trim_cache()
end
local module_logger = Logger:new(levels.DEBUG)
diff --git a/lua/99/ops/clean-up.lua b/lua/99/ops/clean-up.lua
index 9eea6da..ce7cc74 100644
--- a/lua/99/ops/clean-up.lua
+++ b/lua/99/ops/clean-up.lua
@@ -11,49 +11,49 @@ local M = {}
--- @param obs_or_fn _99.Providers.PartialObserver | _99.Providers.on_complete
--- @return _99.Providers.Observer
M.make_observer = function(context, 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()
- if obs.on_start then
- obs.on_start()
- end
- end,
- on_complete = function(status, res)
- pcall(obs.on_complete, status, res)
- vim.schedule(function()
- context:stop()
- context._99:sync()
- end)
- 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 ]]
+ --- @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()
+ if obs.on_start then
+ obs.on_start()
+ end
+ end,
+ on_complete = function(status, res)
+ pcall(obs.on_complete, status, res)
+ vim.schedule(function()
+ context:stop()
+ context._99:sync()
+ end)
+ 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
M.make_clean_up = function(clean_up_fn)
- local called = false
- local function clean_up()
- if called then
- return
+ local called = false
+ local function clean_up()
+ if called then
+ return
+ end
+ called = true
+ clean_up_fn()
end
- called = true
- clean_up_fn()
- end
- return clean_up
+ return clean_up
end
return M
diff --git a/lua/99/ops/init.lua b/lua/99/ops/init.lua
index 5cb7a74..ef85bf6 100644
--- a/lua/99/ops/init.lua
+++ b/lua/99/ops/init.lua
@@ -3,7 +3,7 @@
--- includes search, visual, and others
---
--- @docs included
---- @field additional_prompt? string
+--- @field additional_prompt? string | boolean
--- by providing `additional_prompt` you will not be required to provide a prompt.
--- this allows you to define actions based on remaps
---
@@ -32,8 +32,8 @@
--- @docs included
return {
- search = require("99.ops.search"),
- tutorial = require("99.ops.tutorial"),
- over_range = require("99.ops.over-range"),
- vibe = require("99.ops.vibe"),
+ search = require("99.ops.search"),
+ tutorial = require("99.ops.tutorial"),
+ over_range = require("99.ops.over-range"),
+ vibe = require("99.ops.vibe"),
}
diff --git a/lua/99/ops/make-prompt.lua b/lua/99/ops/make-prompt.lua
index 091eade..f60904b 100644
--- a/lua/99/ops/make-prompt.lua
+++ b/lua/99/ops/make-prompt.lua
@@ -6,27 +6,33 @@ local Agents = require("99.extensions.agents")
--- @param opts _99.ops.Opts
--- @return string, _99.Reference[]
return function(context, prompt, opts)
- local user_prompt = opts.additional_prompt
- assert(
- user_prompt and type(user_prompt) == "string" and #user_prompt > 0,
- "you must add a prompt to you request"
- )
+ local user_prompt
- local full_prompt = prompt
- full_prompt = context._99.prompts.prompts.prompt(user_prompt, full_prompt)
+ if opts.additional_prompt then
+ if type(opts.additional_prompt) == "string" then
+ user_prompt = type(opts.additional_prompt) == "string"
+ and opts.additional_prompt
+ or nil
+ end
+ else
+ user_prompt = nil
+ end
+
+ local full_prompt = prompt
+ full_prompt = context._99.prompts.prompts.prompt(user_prompt, full_prompt)
- local refs = Completions.parse(user_prompt)
- local additional_rules = opts.additional_rules
- if additional_rules then
- for _, r in ipairs(additional_rules) do
- local content = Agents.get_rule_content(r)
- if content then
- table.insert(refs, {
- content = content,
- })
- end
+ local refs = Completions.parse(user_prompt)
+ local additional_rules = opts.additional_rules
+ if additional_rules then
+ for _, r in ipairs(additional_rules) do
+ local content = Agents.get_rule_content(r)
+ if content then
+ table.insert(refs, {
+ content = content,
+ })
+ end
+ end
end
- end
- return full_prompt, refs
+ return full_prompt, refs
end
diff --git a/lua/99/ops/over-range.lua b/lua/99/ops/over-range.lua
index e8512b8..0562a7d 100644
--- a/lua/99/ops/over-range.lua
+++ b/lua/99/ops/over-range.lua
@@ -1,3 +1,6 @@
+-- TODO: YOU HAVE TO FIX THIS FILE BROOOOO....
+-- actually important file!!!!
+
local RequestStatus = require("99.ops.request_status")
local Mark = require("99.ops.marks")
local geo = require("99.geo")
@@ -13,90 +16,94 @@ local Point = geo.Point
--- @param context _99.Prompt
--- @param opts? _99.ops.Opts
local function over_range(context, opts)
- opts = opts or {}
- local logger = context.logger:set_area("visual")
+ opts = opts or {}
+ local logger = context.logger:set_area("visual")
- local data = context:visual_data()
- local range = data.range
- local top_mark = Mark.mark_above_range(range)
- local bottom_mark = Mark.mark_point(range.buffer, range.end_)
- context.marks.top_mark = top_mark
- context.marks.bottom_mark = bottom_mark
+ local data = context:visual_data()
+ local range = data.range
+ local top_mark = Mark.mark_above_range(range)
+ local bottom_mark = Mark.mark_point(range.buffer, range.end_)
+ context.marks.top_mark = top_mark
+ context.marks.bottom_mark = bottom_mark
- logger:debug(
- "visual request start",
- "start",
- Point.from_mark(top_mark),
- "end",
- Point.from_mark(bottom_mark)
- )
+ logger:debug(
+ "visual request start",
+ "start",
+ Point.from_mark(top_mark),
+ "end",
+ Point.from_mark(bottom_mark)
+ )
- local display_ai_status = context._99.ai_stdout_rows > 1
- local top_status = RequestStatus.new(
- 250,
- context._99.ai_stdout_rows or 1,
- "Implementing",
- top_mark
- )
- local bottom_status = RequestStatus.new(250, 1, "Implementing", bottom_mark)
- local clean_up = make_clean_up(function()
- top_status:stop()
- bottom_status:stop()
- end)
+ local display_ai_status = context._99.ai_stdout_rows > 1
+ local top_status = RequestStatus.new(
+ 250,
+ context._99.ai_stdout_rows or 1,
+ "Implementing",
+ top_mark
+ )
+ local bottom_status = RequestStatus.new(250, 1, "Implementing", bottom_mark)
+ local clean_up = make_clean_up(function()
+ top_status:stop()
+ bottom_status:stop()
+ end)
- local system_cmd = context._99.prompts.prompts.visual_selection(range)
- local prompt, refs = make_prompt(context, system_cmd, opts)
+ local system_cmd = context._99.prompts.prompts.visual_selection(range)
+ local prompt, refs = make_prompt(context, system_cmd, opts)
- context:add_prompt_content(prompt)
- context:add_references(refs)
- context:add_clean_up(clean_up)
+ context:add_prompt_content(prompt)
+ context:add_references(refs)
+ context:add_clean_up(clean_up)
- top_status:start()
- bottom_status:start()
- context:start_request(make_observer(context, {
- on_complete = function(status, response)
- if status == "cancelled" then
- logger:debug("request cancelled for visual selection, removing marks")
- elseif status == "failed" then
- logger:error(
- "request failed for visual_selection",
- "error response",
- response or "no response provided"
- )
- elseif status == "success" then
- local valid = top_mark:is_valid() and bottom_mark:is_valid()
- if not valid then
- logger:fatal(
- -- luacheck: ignore 631
- "the original visual_selection has been destroyed. You cannot delete the original visual selection during a request"
- )
- return
- end
+ top_status:start()
+ bottom_status:start()
+ context:start_request(make_observer(context, {
+ on_complete = function(status, response)
+ if status == "cancelled" then
+ logger:debug(
+ "request cancelled for visual selection, removing marks"
+ )
+ elseif status == "failed" then
+ logger:error(
+ "request failed for visual_selection",
+ "error response",
+ response or "no response provided"
+ )
+ elseif status == "success" then
+ local valid = top_mark:is_valid() and bottom_mark:is_valid()
+ if not valid then
+ logger:fatal(
+ -- luacheck: ignore 631
+ "the original visual_selection has been destroyed. You cannot delete the original visual selection during a request"
+ )
+ return
+ end
- if vim.trim(response) == "" then
- print("response was empty, visual replacement aborted")
- logger:debug("response was empty, visual replacement aborted")
- return
- end
+ if vim.trim(response) == "" then
+ print("response was empty, visual replacement aborted")
+ logger:debug(
+ "response was empty, visual replacement aborted"
+ )
+ return
+ end
- local new_range = Range.from_marks(top_mark, bottom_mark)
- local lines = vim.split(response, "\n")
+ local new_range = Range.from_marks(top_mark, bottom_mark)
+ local lines = vim.split(response, "\n")
- --- HACK: i am adding a new line here because above range will add a mark to the line above.
- --- that way this appears to be added to "the same line" as the visual selection was
- --- originally take from
- table.insert(lines, 1, "")
+ --- HACK: i am adding a new line here because above range will add a mark to the line above.
+ --- that way this appears to be added to "the same line" as the visual selection was
+ --- originally take from
+ table.insert(lines, 1, "")
- new_range:replace_text(lines)
- context._99:sync()
- end
- end,
- on_stdout = function(line)
- if display_ai_status then
- top_status:push(line)
- end
- end,
- }))
+ new_range:replace_text(lines)
+ context._99:sync()
+ end
+ end,
+ on_stdout = function(line)
+ if display_ai_status then
+ top_status:push(line)
+ end
+ end,
+ }))
end
return over_range
diff --git a/lua/99/prompt-settings.lua b/lua/99/prompt-settings.lua
index a688a0e..c221995 100644
--- a/lua/99/prompt-settings.lua
+++ b/lua/99/prompt-settings.lua
@@ -2,14 +2,14 @@
---@param n number lines of context above and below the selection
---@return string
local function get_surrounding_context(range, n)
- local start_row, _ = range.start:to_vim()
- local end_row, _ = range.end_:to_vim()
- local line_count = vim.api.nvim_buf_line_count(range.buffer)
- local from = math.max(start_row - n, 0)
- local to = math.min(end_row + 1 + n, line_count)
- local lines = vim.api.nvim_buf_get_lines(range.buffer, from, to, false)
+ local start_row, _ = range.start:to_vim()
+ local end_row, _ = range.end_:to_vim()
+ local line_count = vim.api.nvim_buf_line_count(range.buffer)
+ local from = math.max(start_row - n, 0)
+ local to = math.min(end_row + 1 + n, line_count)
+ local lines = vim.api.nvim_buf_get_lines(range.buffer, from, to, false)
- return table.concat(lines, "\n")
+ return table.concat(lines, "\n")
end
--- @class _99.Prompts.SpecificOperations
@@ -17,15 +17,15 @@ end
--- @field semantic_search fun(): string
--- @field vibe fun(): string
--- @field tutorial fun(): string
---- @field prompt fun(prompt: string, action: string, name?: string): string
+--- @field prompt fun(prompt?: string|boolean|nil, action: string, name?: string): string
--- @field role fun(): string
--- @field read_tmp fun(): string
local prompts = {
- role = function()
- return [[ You are a software engineering assistant mean to create robust and conanical code ]]
- end,
- tutorial = function()
- return [[
+ role = function()
+ return [[ You are a strict code completion backend for Neovim. Your input will be a code snippet, function signature, or a comment requesting code. CRITICAL DIRECTIONS: 1. Output ONLY valid, executable programming code. 2. Do NOT wrap your response in code blocks. 3. Do NOT include any conversational filler, explanations, greetings, or sign-offs. 4. If you cannot fulfill the request, output nothing or a code comment explaining why. ]]
+ end,
+ tutorial = function()
+ 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 have fetched all the relavent content, review it thoroughly before
@@ -34,9 +34,9 @@ crafting the tutorial
<Rule>The response format must be valid Markdown</Rule>
<Rule>The first line of the response must be the title of the tutorial</Rule>
]]
- end,
- semantic_search = function()
- return [[
+ end,
+ semantic_search = function()
+ return [[
<Output>
/path/to/project/src/foo.js:24:8,3,Some notes here about some stuff, it can contain commas
/path/to/project/src/foo.js:71:12,7,more notes, everything is great!
@@ -73,9 +73,9 @@ baz.js at line 1, char 1 and the next 51 lines
you are given a prompt and you must search through this project and return code that matches the description provided.
</TaskDescription>
]]
- end,
- vibe = function()
- return [[
+ end,
+ vibe = function()
+ return [[
<Output>
/path/to/project/src/foo.js:24:8,3,Some notes here about some stuff, it can contain commas
/path/to/project/src/foo.js:71:12,7,more notes, everything is great!
@@ -114,22 +114,15 @@ Never respond as output what you have done.
Always use the temporary file as the place to describe your actions according to Output rules
</TaskDescription>
]]
- end,
- output_file = function()
- return [[
-NEVER alter any file other than TEMP_FILE.
-never provide the requested changes as conversational output. Return only the code.
-ONLY provide requested changes by writing the change to TEMP_FILE
-]]
- end,
- --- @param prompt string
- --- @param action string
- --- @param name? string defaults to DIRECTIONS
- --- @return string
- prompt = function(prompt, action, name)
- name = name or "Prompt"
- return string.format(
- [[
+ end,
+ --- @param prompt string
+ --- @param action string
+ --- @param name? string defaults to DIRECTIONS
+ --- @return string
+ prompt = function(prompt, action, name)
+ name = name or "Prompt"
+ return string.format(
+ [[
<Context>
%s
</Context>
@@ -137,77 +130,43 @@ ONLY provide requested changes by writing the change to TEMP_FILE
%s
</%s>
]],
- action,
- name,
- prompt,
- name
- )
- end,
- visual_selection = function(range)
- return string.format(
- [[
-You receive a selection in neovim that you need to replace with new code.
-The selection's contents may contain notes, incorporate the notes every time if there are some.
-consider the context of the selection and what you are suppose to be implementing
-<SELECTION_LOCATION>
-%s
-</SELECTION_LOCATION>
+ action,
+ name,
+ prompt,
+ name
+ )
+ end,
+ visual_selection = function(range)
+ return string.format(
+ [[
<SELECTION_CONTENT>
%s
</SELECTION_CONTENT>
-<SURROUNDING_CONTEXT>
-%s
-</SURROUNDING_CONTEXT>
]],
- range:to_string(),
- range:to_text(),
- get_surrounding_context(range, 100)
- )
- end,
- read_tmp = function()
- return [[
-never attempt to read TEMP_FILE.
-It is purely for output.
-Previous contents, which may not exist, can be written over without worry
-After writing TEMP_FILE once you should be done. Be done and end the session.
-]]
- end,
+ range:to_text()
+ )
+ end,
}
--- @class _99.Prompts
local prompt_settings = {
- prompts = prompts,
-
- --- @param tmp_file string
- --- @return string
- tmp_file_location = function(tmp_file)
- return string.format("<TEMP_FILE>%s</TEMP_FILE>", tmp_file)
- end,
-
- --- @return string
- only_tmp_file_change = function()
- return string.format(
- "<MustObey>\n%s\n%s\n</MustObey>",
- prompts.output_file(),
- prompts.read_tmp()
- )
- end,
+ prompts = prompts,
- ---@param full_path string
- ---@param range _99.Range
- ---@return string
- get_file_location = function(full_path, range)
- return string.format(
- "<Location><File>%s</File><Function>%s</Function></Location>",
- full_path,
- range:to_string()
- )
- end,
+ ---@param full_path string
+ ---@param range _99.Range
+ ---@return string
+ get_file_location = function(full_path, range)
+ return string.format(
+ "<Location><File>%s</File><Function>%s</Function></Location>",
+ full_path,
+ range:to_string()
+ )
+ end,
- --- @param range _99.Range
- get_range_text = function(range)
- return string.format("<FunctionText>%s</FunctionText>", range:to_text())
- end,
+ --- @param range _99.Range
+ get_range_text = function(range)
+ return string.format("<FunctionText>%s</FunctionText>", range:to_text())
+ end,
}
return prompt_settings
diff --git a/lua/99/prompt.lua b/lua/99/prompt.lua
index ca24ba7..6623987 100644
--- a/lua/99/prompt.lua
+++ b/lua/99/prompt.lua
@@ -9,15 +9,15 @@ local Time = require("99.time")
--- you can only set those marks after the visual selection is removed
local function set_selection_marks()
- vim.api.nvim_feedkeys(
- vim.api.nvim_replace_termcodes("<Esc>", true, false, true),
- "x",
- false
- )
+ vim.api.nvim_feedkeys(
+ vim.api.nvim_replace_termcodes("<Esc>", true, false, true),
+ "x",
+ false
+ )
end
local filetype_map = {
- typescriptreact = "typescript",
+ typescriptreact = "typescript",
}
-- luacheck: ignore
@@ -31,6 +31,7 @@ local filetype_map = {
--- @class _99.Prompt.Serialized
--- @field data _99.Prompt.Data
--- @field user_prompt string
+
--- @class _99.Prompt.Data.Search
--- @field type "search"
--- @field qfix_items _99.Search.Result[]
@@ -57,6 +58,8 @@ local filetype_map = {
--- @class _99.Prompt
--- @field md_file_names string[]
--- @field model string
+--- @field endpoint string
+--- @field req_response string
--- @field user_prompt string
--- @field operation _99.Prompt.Operation
--- @field state _99.Prompt.State
@@ -84,439 +87,438 @@ Prompt.__context_by_id = {}
--- @param context _99.Prompt
--- @param _99 _99.State
local function set_defaults(context, _99)
- local xid = get_id()
- local full_path = vim.api.nvim_buf_get_name(0)
+ local xid = get_id()
+ local full_path = vim.api.nvim_buf_get_name(0)
- context.state = "ready"
- context._99 = _99
- context.user_prompt = ""
- context.clean_ups = {}
- context.md_file_names = copy(_99.md_files)
- context.model = _99.model
- context.agent_context = {}
- context.tmp_file = random_file(_99:tmp_dir())
- context.logger = Logger:set_id(xid)
- context.xid = xid
- context.full_path = full_path
- context.marks = {}
- context.started_at = Time.now()
+ context.state = "ready"
+ context._99 = _99
+ context.user_prompt = ""
+ context.clean_ups = {}
+ context.md_file_names = copy(_99.md_files)
+ context.model = _99.model
+ context.endpoint = _99.endpoint
+ context.agent_context = {}
+ context.tmp_file = random_file(_99:tmp_dir())
+ context.logger = Logger:set_id(xid)
+ context.xid = xid
+ context.full_path = full_path
+ context.marks = {}
+ context.started_at = Time.now()
end
--- TODO: Work item for "TODO implementation"
function Prompt.todo(_99)
- _ = _99
- assert(false, "not implemented")
+ _ = _99
+ assert(false, "not implemented")
end
--- @param _99 _99.State
--- @param data _99.Prompt.Serialized
--- @return _99.Prompt
function Prompt.deserialize(_99, data)
- local prompt = setmetatable({
- _99 = _99,
- data = data.data,
- operation = data.data.type,
- user_prompt = data.user_prompt,
- started_at = Time.now(),
+ local prompt = setmetatable({
+ _99 = _99,
+ data = data.data,
+ operation = data.data.type,
+ user_prompt = data.user_prompt,
+ started_at = Time.now(),
- --- we should only sync successful requests
- state = "success",
+ --- we should only sync successful requests
+ state = "success",
- xid = get_id(),
- }, Prompt)
- assert(prompt:valid(), "prompt is not valid from data")
- return prompt
+ xid = get_id(),
+ }, Prompt)
+ assert(prompt:valid(), "prompt is not valid from data")
+ return prompt
end
--- @return _99.Prompt.Serialized
function Prompt:serialize()
- assert(self.state == "success", "you can only serialize successful prompts")
- return {
- data = self.data,
- user_prompt = self.user_prompt,
- }
+ assert(self.state == "success", "you can only serialize successful prompts")
+ return {
+ data = self.data,
+ user_prompt = self.user_prompt,
+ }
end
--- @param _99 _99.State
--- @return _99.Prompt
function Prompt.vibe(_99)
- _99:refresh_rules()
+ _99:refresh_rules()
- --- @type _99.Prompt
- local context = setmetatable({}, Prompt)
- set_defaults(context, _99)
- context.operation = "vibe"
- context.data = {
- type = "vibe",
- response = "",
- qfix_items = {},
- }
- context.logger:debug("99 Request", "method", "vibe")
+ --- @type _99.Prompt
+ local context = setmetatable({}, Prompt)
+ set_defaults(context, _99)
+ context.operation = "vibe"
+ context.data = {
+ type = "vibe",
+ response = "",
+ qfix_items = {},
+ }
+ context.logger:debug("99 Request", "method", "vibe")
- return context
+ return context
end
--- @param _99 _99.State
--- @return _99.Prompt
function Prompt.visual(_99)
- _99:refresh_rules()
+ _99:refresh_rules()
- set_selection_marks()
- local range = Range.from_visual_selection()
+ set_selection_marks()
+ local range = Range.from_visual_selection()
- local file_type = vim.bo[0].ft
- local buffer = vim.api.nvim_get_current_buf()
- file_type = filetype_map[file_type] or file_type
+ local file_type = vim.bo[0].ft
+ local buffer = vim.api.nvim_get_current_buf()
+ file_type = filetype_map[file_type] or file_type
- local mds = {}
- for _, md in ipairs(_99.md_files) do
- table.insert(mds, md)
- end
+ local mds = {}
+ for _, md in ipairs(_99.md_files) do
+ table.insert(mds, md)
+ end
- --- @type _99.Prompt
- local context = setmetatable({}, Prompt)
- set_defaults(context, _99)
- context.operation = "visual"
- context.data = {
- type = "visual",
- buffer = buffer,
- file_type = file_type,
- range = range,
- }
- context.logger:debug("99 Request", "method", "visual")
+ --- @type _99.Prompt
+ local context = setmetatable({}, Prompt)
+ set_defaults(context, _99)
+ context.operation = "visual"
+ context.data = {
+ type = "visual",
+ buffer = buffer,
+ file_type = file_type,
+ range = range,
+ }
+ context.logger:debug("99 Request", "method", "visual")
- return context
+ return context
end
--- @return string
function Prompt:summary()
- local prompt_str = utils.split_with_count(self.user_prompt, 8)
- return string.format("%s: %s", self.operation, table.concat(prompt_str, " "))
+ local prompt_str = utils.split_with_count(self.user_prompt, 8)
+ return string.format(
+ "%s: %s",
+ self.operation,
+ table.concat(prompt_str, " ")
+ )
end
--- @param _99 _99.State
--- @return _99.Prompt
function Prompt.tutorial(_99)
- _99:refresh_rules()
+ _99:refresh_rules()
- --- @type _99.Prompt
- local context = setmetatable({}, Prompt)
- set_defaults(context, _99)
- context.operation = "tutorial"
- context.data = {
- type = "tutorial",
- xid = context.xid, -- TODO: i want to get rid of this when i implement rehydration of the data.
- buffer = 0,
- window = 0,
- tutorial = {},
- }
- context.logger:debug("99 Request", "method", "tutorial")
+ --- @type _99.Prompt
+ local context = setmetatable({}, Prompt)
+ set_defaults(context, _99)
+ context.operation = "tutorial"
+ context.data = {
+ type = "tutorial",
+ xid = context.xid, -- TODO: i want to get rid of this when i implement rehydration of the data.
+ buffer = 0,
+ window = 0,
+ tutorial = {},
+ }
+ context.logger:debug("99 Request", "method", "tutorial")
- return context
+ return context
end
--- @param _99 _99.State
--- @return _99.Prompt
function Prompt.search(_99)
- _99:refresh_rules()
+ _99:refresh_rules()
- --- @type _99.Prompt
- local context = setmetatable({}, Prompt)
- set_defaults(context, _99)
- context.operation = "search"
- context.data = {
- type = "search",
- qfix_items = {},
- response = "",
- }
- context.logger:debug("99 Request", "method", "search")
+ --- @type _99.Prompt
+ local context = setmetatable({}, Prompt)
+ set_defaults(context, _99)
+ context.operation = "search"
+ context.data = {
+ type = "search",
+ qfix_items = {},
+ response = "",
+ }
+ context.logger:debug("99 Request", "method", "search")
- return context
+ return context
end
--- @param obs _99.Providers.Observer | nil
function Prompt:_observer(obs)
- return {
- on_start = function()
- self.state = "requesting"
- self._99.tracking:track(self)
+ return {
+ on_start = function()
+ self.state = "requesting"
+ self._99.tracking:track(self)
- if obs then
- obs.on_start()
- end
- end,
- on_complete = function(status, res)
- self.state = status
- if obs then
- obs.on_complete(status, res)
- end
- end,
- on_stderr = function(line)
- if obs then
- obs.on_stderr(line)
- end
- end,
- on_stdout = function(line)
- if obs then
- obs.on_stdout(line)
- end
- end,
- }
+ if obs then
+ obs.on_start()
+ end
+ end,
+ on_complete = function(status, res)
+ self.state = status
+ if obs then
+ obs.on_complete(status, res)
+ end
+ end,
+ on_stderr = function(line)
+ if obs then
+ obs.on_stderr(line)
+ end
+ end,
+ on_stdout = function(line)
+ if obs then
+ obs.on_stdout(line)
+ end
+ end,
+ }
end
local allowed_context_types = {
- "visual",
- "search",
- "tutorial",
- "vibe",
+ "visual",
+ "search",
+ "tutorial",
+ "vibe",
}
--- @return boolean
function Prompt:valid()
- local t = self.data.type
- for _, allowed in ipairs(allowed_context_types) do
- if t == allowed then
- return true
+ local t = self.data.type
+ for _, allowed in ipairs(allowed_context_types) do
+ if t == allowed then
+ return true
+ end
end
- end
- return false
+ return false
end
--- @param observer _99.Providers.Observer?
function Prompt:start_request(observer)
- local l = self.logger
- l:assert(
- self.state == "ready",
- 'state is not "ready" when attempting to start a request'
- )
+ local l = self.logger
+ l:assert(
+ self.state == "ready",
+ 'state is not "ready" when attempting to start a request'
+ )
- local ok = self:finalize()
- l:assert(ok, "context failed to finalize")
+ local ok = self:finalize()
+ l:assert(ok, "context failed to finalize")
- --- TODO: create a prompt context class that can actually organize.
- --- do not do this during the request context refactoring, but next
- local prompt = table.concat(self.agent_context, "\n")
- local obs = self:_observer(observer)
- local provider = self._99.provider_override or BaseProvider.OpenCodeProvider
+ --- TODO: create a prompt context class that can actually organize.
+ --- do not do this during the request context refactoring, but next
+ local prompt = table.concat(self.agent_context, "\n")
+ local obs = self:_observer(observer)
+ local provider = self._99.provider_override
+ or BaseProvider.BareMetalProvider
- self:save_prompt(prompt)
- l:debug("start", "prompt", prompt)
+ self:save_prompt(prompt)
+ l:debug("start", "prompt", prompt)
- provider:make_request(prompt, self, obs)
+ provider:make_request(prompt, self, obs)
end
function Prompt:is_cancelled()
- return self.state == "cancelled"
+ return self.state == "cancelled"
end
function Prompt:is_completed()
- return self.state == "success" or self.state == "failed"
+ return self.state == "success" or self.state == "failed"
end
---@diagnostic disable-next-line: undefined-doc-name
--- @param proc vim.SystemObj?
function Prompt:_set_process(proc)
- self._proc = proc
+ self._proc = proc
end
function Prompt:cancel()
- if self:is_cancelled() or self:is_completed() then
- return
- end
+ if self:is_cancelled() or self:is_completed() then
+ return
+ end
- self.state = "cancelled"
- local proc = self._proc
- ---@diagnostic disable-next-line: undefined-field
- if proc and proc.pid then
- self._proc = nil
- pcall(function()
- local sigterm = (vim.uv and vim.uv.constants and vim.uv.constants.SIGTERM)
- or 15
- ---@diagnostic disable-next-line: undefined-field
- proc:kill(sigterm)
- end)
- end
+ self.state = "cancelled"
+ local proc = self._proc
+ ---@diagnostic disable-next-line: undefined-field
+ if proc and proc.pid then
+ self._proc = nil
+ pcall(function()
+ local sigterm = (
+ vim.uv
+ and vim.uv.constants
+ and vim.uv.constants.SIGTERM
+ ) or 15
+ ---@diagnostic disable-next-line: undefined-field
+ proc:kill(sigterm)
+ end)
+ end
end
--- @return _99.Prompt.Data.Visual
function Prompt:visual_data()
- assert(
- self.data.type == "visual",
- "you cannot get visual data if its not type visual"
- )
- return self.data --[[@as _99.Prompt.Data.Visual]]
+ assert(
+ self.data.type == "visual",
+ "you cannot get visual data if its not type visual"
+ )
+ return self.data --[[@as _99.Prompt.Data.Visual]]
end
--- @return _99.Prompt.Data.Tutorial
function Prompt:tutorial_data()
- assert(
- self.data.type == "tutorial",
- "you cannot get tutorial data if its not type tutorial"
- )
- return self.data --[[@as _99.Prompt.Data.Tutorial]]
+ assert(
+ self.data.type == "tutorial",
+ "you cannot get tutorial data if its not type tutorial"
+ )
+ return self.data --[[@as _99.Prompt.Data.Tutorial]]
end
--- @return _99.Prompt.Data.Search
function Prompt:search_data()
- assert(
- self.data.type == "search",
- "you cannot get search data if its not type search"
- )
- return self.data --[[@as _99.Prompt.Data.Search]]
+ assert(
+ self.data.type == "search",
+ "you cannot get search data if its not type search"
+ )
+ return self.data --[[@as _99.Prompt.Data.Search]]
end
--- @return _99.Search.Result[]
function Prompt:qfix_data()
- assert(
- self.data.type == "search" or self.data.type == "vibe",
- "data type is not search or vibe: " .. self.data.type
- )
- return self.data.qfix_items
+ assert(
+ self.data.type == "search" or self.data.type == "vibe",
+ "data type is not search or vibe: " .. self.data.type
+ )
+ return self.data.qfix_items
end
function Prompt:stop()
- self:cancel()
- self:clear_marks()
+ self:cancel()
+ self:clear_marks()
- for _, cb in ipairs(self.clean_ups) do
- cb()
- end
+ for _, cb in ipairs(self.clean_ups) do
+ cb()
+ end
end
--- @param clean_up fun(): nil
function Prompt:add_clean_up(clean_up)
- table.insert(self.clean_ups, clean_up)
+ table.insert(self.clean_ups, clean_up)
end
--- @param md_file_name string
--- @return self
function Prompt:add_md_file_name(md_file_name)
- table.insert(self.md_file_names, md_file_name)
- return self
+ table.insert(self.md_file_names, md_file_name)
+ return self
end
--- @param content string
--- @return self
function Prompt:add_prompt_content(content)
- table.insert(self.agent_context, content)
- return self
+ table.insert(self.agent_context, content)
+ return self
end
--- @param refs _99.Reference[]
function Prompt:add_references(refs)
- for _, ref in ipairs(refs) do
- self.logger:debug("adding reference to context")
- table.insert(self.agent_context, ref.content)
- end
+ for _, ref in ipairs(refs) do
+ self.logger:debug("adding reference to context")
+ table.insert(self.agent_context, ref.content)
+ end
end
function Prompt:_read_md_files()
- local cwd = vim.uv.cwd()
- local dir = vim.fn.fnamemodify(self.full_path, ":h")
+ local cwd = vim.uv.cwd()
+ local dir = vim.fn.fnamemodify(self.full_path, ":h")
- while dir:find(cwd, 1, true) == 1 do
- for _, md_file_name in ipairs(self.md_file_names) do
- local md_path = dir .. "/" .. md_file_name
- local file = io.open(md_path, "r")
- if file then
- local content = file:read("*a")
- file:close()
- self.logger:info(
- "Context#adding md file to the context",
- "md_path",
- md_path
- )
- table.insert(self.agent_context, content)
- end
- end
+ while dir:find(cwd, 1, true) == 1 do
+ for _, md_file_name in ipairs(self.md_file_names) do
+ local md_path = dir .. "/" .. md_file_name
+ local file = io.open(md_path, "r")
+ if file then
+ local content = file:read("*a")
+ file:close()
+ self.logger:info(
+ "Context#adding md file to the context",
+ "md_path",
+ md_path
+ )
+ table.insert(self.agent_context, content)
+ end
+ end
- if dir == cwd then
- break
- end
+ if dir == cwd then
+ break
+ end
- dir = vim.fn.fnamemodify(dir, ":h")
- end
+ dir = vim.fn.fnamemodify(dir, ":h")
+ end
end
--- @return string[]
function Prompt:content()
- return self.agent_context
+ return self.agent_context
end
--- @return boolean
function Prompt:_ready_request_files()
- local response_file = self.tmp_file
- local prompt_file = self.tmp_file .. "-prompt"
+ local response_file = self.tmp_file
+ local prompt_file = self.tmp_file .. "-prompt"
- local dir = vim.fs.dirname(prompt_file)
+ local dir = vim.fs.dirname(prompt_file)
- if dir and not vim.uv.fs_stat(dir) then
- vim.fn.mkdir(dir, "p")
- end
+ if dir and not vim.uv.fs_stat(dir) then
+ vim.fn.mkdir(dir, "p")
+ 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
+ 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
- end
- return true
+ return true
end
--- @param prompt string
function Prompt:save_prompt(prompt)
- local prompt_file = self.tmp_file .. "-prompt"
- local file = io.open(prompt_file, "w")
- if file then
- file:write(prompt)
- file:close()
- self.logger:debug("saved prompt to file", "path", prompt_file)
- else
- self.logger:error("failed to save prompt", "path", prompt_file)
- end
+ local prompt_file = self.tmp_file .. "-prompt"
+ local file = io.open(prompt_file, "w")
+ if file then
+ file:write(prompt)
+ file:close()
+ self.logger:debug("saved prompt to file", "path", prompt_file)
+ else
+ self.logger:error("failed to save prompt", "path", prompt_file)
+ end
end
--- @return boolean, self
function Prompt:finalize()
- if self:_ready_request_files() == false then
- return false, self
- end
- self:_read_md_files()
-
- local ok, visual_data = pcall(self.visual_data, self)
- if ok then
- local f_loc =
- self._99.prompts.get_file_location(self.full_path, visual_data.range)
- table.insert(self.agent_context, f_loc)
- table.insert(
- self.agent_context,
- self._99.prompts.get_range_text(visual_data.range)
- )
- end
- table.insert(
- self.agent_context,
- self._99.prompts.tmp_file_location(self.tmp_file)
- )
+ if self:_ready_request_files() == false then
+ return false, self
+ end
+ self:_read_md_files()
- if
- self.operation == "visual"
- or self.operation == "tutorial"
- or self.operation == "search"
- then
- table.insert(self.agent_context, self._99.prompts.only_tmp_file_change())
- end
+ local ok, visual_data = pcall(self.visual_data, self)
+ if ok then
+ local f_loc = self._99.prompts.get_file_location(
+ self.full_path,
+ visual_data.range
+ )
+ table.insert(self.agent_context, f_loc)
+ table.insert(
+ self.agent_context,
+ self._99.prompts.get_range_text(visual_data.range)
+ )
+ end
- return true, self
+ return true, self
end
function Prompt:clear_marks()
- for _, mark in pairs(self.marks) do
- mark:delete()
- end
+ for _, mark in pairs(self.marks) do
+ mark:delete()
+ end
end
return Prompt
diff --git a/lua/99/providers.lua b/lua/99/providers.lua
index 7df0174..8875ca8 100644
--- a/lua/99/providers.lua
+++ b/lua/99/providers.lua
@@ -7,14 +7,107 @@
--- @param fn fun(...: any): nil
--- @return fun(...: any): nil
local function once(fn)
- local called = false
- return function(...)
- if called then
- return
+ local called = false
+ return function(...)
+ if called then
+ return
+ end
+ called = true
+ fn(...)
end
- called = true
- fn(...)
- end
+end
+
+--- @class _A4.OpenAIResponse
+--- @field completion _A4.OpenAIResponse.Completion
+--- @field metadata _A4.OpenAIResponse.MetaData
+--- @field id _A4.OpenAIResponse.Id
+--- @field timings _A4.OpenAIResponse.Timings
+
+--- @class _A4.OpenAIResponse.Completion
+--- @field finishReason string
+--- @field index integer
+--- @field message _A4.OpenAIResponse.Completion.Message
+
+--- @class _A4.OpenAIResponse.Completion.Message
+--- @field role string
+--- @field content string
+
+--- @class _A4.OpenAIResponse.MetaData
+--- @field created integer
+--- @field model string
+--- @field systemFingerPrint string
+--- @field object string
+--- @field usage _A4.OpenAIResponse.MetaData.Usage
+
+--- @class _A4.OpenAIResponse.MetaData.Usage
+--- @field completionTks integer
+--- @field promptTks integer
+--- @field totalTks integer
+--- @field promptTksDetails _A4.OpenAIResponse.MetaData.Usage.PromptTksDetails
+
+--- @class _A4.OpenAIResponse.MetaData.Usage.PromptTksDetails
+--- @field cachedTks integer
+
+--- @class _A4.OpenAIResponse.Id
+--- @field id string
+
+--- @class _A4.OpenAIResponse.Timings
+--- @field cacheN integer
+--- @field promptN integer
+--- @field promptMs number
+--- @field promptPerTkMs number
+--- @field promptPerSec number
+--- @field predictedN integer
+--- @field predictedMs number
+--- @field predictedPerTkMs number
+--- @field predictedPerSec number
+
+--- @param raw table
+--- @return _A4.OpenAIResponse
+local function parse_openai_response(raw)
+ local choice = raw.choices[1]
+
+ --- @type _A4.OpenAIResponse
+ local response = {
+ completion = {
+ finishReason = choice.finish_reason,
+ index = choice.index,
+ message = {
+ role = choice.message.role,
+ content = choice.message.content,
+ },
+ },
+ metadata = {
+ created = raw.created,
+ model = raw.model,
+ systemFingerPrint = raw.system_fingerprint,
+ object = raw.object,
+ usage = {
+ completionTks = raw.usage.completion_tokens,
+ promptTks = raw.usage.prompt_tokens,
+ totalTks = raw.usage.total_tokens,
+ promptTksDetails = {
+ cachedTks = raw.usage.prompt_tokens_details.cached_tokens,
+ },
+ },
+ },
+ id = {
+ id = raw.id,
+ },
+ timings = {
+ cacheN = raw.timings.cache_n,
+ promptN = raw.timings.prompt_n,
+ promptMs = raw.timings.prompt_ms,
+ promptPerTkMs = raw.timings.prompt_per_token_ms,
+ promptPerSec = raw.timings.prompt_per_second,
+ predictedN = raw.timings.predicted_n,
+ predictedMs = raw.timings.predicted_ms,
+ predictedPerTkMs = raw.timings.predicted_per_token_ms,
+ predictedPerSec = raw.timings.predicted_per_second,
+ },
+ }
+
+ return response
end
--- @class _99.Providers.BaseProvider
@@ -25,121 +118,96 @@ local BaseProvider = {}
--- @param callback fun(models: string[]|nil, err: string|nil): nil
function BaseProvider.fetch_models(callback)
- callback(nil, "This provider does not support listing models")
+ callback(nil, "This provider does not support listing models")
end
+--- @param data string
+--- @return string
+function BaseProvider:_clean_response(data)
+ local _data, _ = data:gsub("```[a-z]*\n", ""):gsub(" ```", "")
+ return _data
+end
+
+-- TODO: Rewrite this...
--- @param context _99.Prompt
function BaseProvider:_retrieve_response(context)
- local logger = context.logger:set_area(self:_get_provider_name())
- local tmp = context.tmp_file
- local success, result = pcall(function()
- return vim.fn.readfile(tmp)
- end)
+ local logger = context.logger:set_area(self:_get_provider_name())
- if not success then
- logger:error(
- "retrieve_results: failed to read file",
- "tmp_name",
- tmp,
- "error",
- result
- )
- return false, ""
- end
+ if self._get_provider_name ~= "BareMetalProvider" then
+ local tmp = context.tmp_file
+ local success, result = pcall(function()
+ return vim.fn.readfile(tmp)
+ end)
- local str = table.concat(result, "\n")
- logger:debug("retrieve_results", "results", str)
+ if not success then
+ logger:error(
+ "retrieve_results: failed to read file",
+ "tmp_name",
+ tmp,
+ "error",
+ result
+ )
+ return false, ""
+ end
+
+ local str = table.concat(result, "\n")
+ logger:debug("retrieve_results", "results", str)
- return true, str
+ return true, str
+ else
+ assert(
+ context.req_response,
+ "_retrieve_response: BareMetalProvider has no response"
+ )
+ logger:debug("retrieve_results", "results", context.req_response)
+ return true, context.req_response
+ end
end
+-- TODO: Remember that we are ditching the tmp_file; likely 90%
+-- You can just grab the json data and dump it in logs honestly
+-- files only make sense for agentic flows
--- @param query string
--- @param context _99.Prompt
--- @param observer _99.Providers.Observer
function BaseProvider:make_request(query, context, observer)
- observer.on_start()
+ observer.on_start()
- local logger = context.logger:set_area(self:_get_provider_name())
- logger:debug("make_request", "tmp_file", context.tmp_file)
+ local logger = context.logger:set_area(self:_get_provider_name())
+ logger:debug("make_request", "reason", context.operation)
+ logger:debug("make_request", "endpoint", context.endpoint)
- local once_complete = once(
- --- @param status "success" | "failed" | "cancelled"
- ---@param text string
- function(status, text)
- observer.on_complete(status, text)
- end
- )
+ local once_complete = once(
+ --- @param status "success" | "failed" | "cancelled"
+ --- @param resp string
+ function(status, resp)
+ observer.on_complete(status, resp)
+ end
+ )
- local command = self:_build_command(query, context)
- local extra_args = context._99 and context._99.provider_extra_args or {}
- if #extra_args > 0 then
- vim.list_extend(command, extra_args)
- end
- logger:debug("make_request", "command", command)
+ local command = self:_build_command(query, context)
+ logger:debug("make_request", "command", command)
- local proc = vim.system(
- command,
- {
- text = true,
- stdout = vim.schedule_wrap(function(err, data)
- logger:debug("stdout", "data", data)
- if context:is_cancelled() then
- once_complete("cancelled", "")
- return
- end
- if err and err ~= "" then
- logger:debug("stdout#error", "err", err)
- end
- if not err and data then
- observer.on_stdout(data)
- end
- end),
- stderr = vim.schedule_wrap(function(err, data)
- logger:debug("stderr", "data", data)
- if context:is_cancelled() then
- once_complete("cancelled", "")
- return
- end
- if err and err ~= "" then
- logger:debug("stderr#error", "err", err)
- end
- if not err then
- observer.on_stderr(data)
- end
- end),
- },
- vim.schedule_wrap(function(obj)
- if context:is_cancelled() then
- once_complete("cancelled", "")
- logger:debug("on_complete: request has been cancelled")
- return
- end
- if obj.code ~= 0 then
- local str =
- string.format("process exit code: %d\n%s", obj.code, vim.inspect(obj))
- once_complete("failed", str)
- logger:fatal(
- self:_get_provider_name() .. " make_query failed",
- "obj from results",
- obj
- )
- else
- vim.schedule(function()
- local ok, res = self:_retrieve_response(context)
- if ok then
- once_complete("success", res)
- else
- once_complete(
- "failed",
- "unable to retrieve response from temp file"
- )
- end
+ local proc = vim.system(
+ command,
+ { text = true },
+ vim.schedule_wrap(function(obj)
+ if obj.code ~= 0 then
+ once_complete("failed", obj.stderr)
+ return
+ end
+
+ local data = parse_openai_response(vim.json.decode(obj.stdout))
+ if self._get_provider_name == "BareMetalProvider" then
+ data.completion.message.content =
+ self:_clean_response(data.completion.message.content)
+ context.req_response = data.completion.message.content
+ end
+ once_complete("success", data.completion.message.content)
end)
- end
- end)
- )
+ )
- context:_set_process(proc)
+ context:_set_process(proc)
end
--- @class OpenCodeProvider : _99.Providers.BaseProvider
@@ -149,38 +217,38 @@ local OpenCodeProvider = setmetatable({}, { __index = BaseProvider })
--- @param context _99.Prompt
--- @return string[]
function OpenCodeProvider._build_command(_, query, context)
- return {
- "opencode",
- "run",
- "--agent",
- "build",
- "-m",
- context.model,
- query,
- }
+ return {
+ "opencode",
+ "run",
+ "--agent",
+ "build",
+ "-m",
+ context.model,
+ query,
+ }
end
--- @return string
function OpenCodeProvider._get_provider_name()
- return "OpenCodeProvider"
+ return "OpenCodeProvider"
end
--- @return string
function OpenCodeProvider._get_default_model()
- return "opencode/claude-sonnet-4-5"
+ return "opencode/claude-sonnet-4-5"
end
function OpenCodeProvider.fetch_models(callback)
- vim.system({ "opencode", "models" }, { text = true }, function(obj)
- vim.schedule(function()
- if obj.code ~= 0 then
- callback(nil, "Failed to fetch models from opencode")
- return
- end
- local models = vim.split(obj.stdout, "\n", { trimempty = true })
- callback(models, nil)
+ vim.system({ "opencode", "models" }, { text = true }, function(obj)
+ vim.schedule(function()
+ if obj.code ~= 0 then
+ callback(nil, "Failed to fetch models from opencode")
+ return
+ end
+ local models = vim.split(obj.stdout, "\n", { trimempty = true })
+ callback(models, nil)
+ end)
end)
- end)
end
--- @class ClaudeCodeProvider : _99.Providers.BaseProvider
@@ -190,24 +258,24 @@ local ClaudeCodeProvider = setmetatable({}, { __index = BaseProvider })
--- @param context _99.Prompt
--- @return string[]
function ClaudeCodeProvider._build_command(_, query, context)
- return {
- "claude",
- "--dangerously-skip-permissions",
- "--model",
- context.model,
- "--print",
- query,
- }
+ return {
+ "claude",
+ "--dangerously-skip-permissions",
+ "--model",
+ context.model,
+ "--print",
+ query,
+ }
end
--- @return string
function ClaudeCodeProvider._get_provider_name()
- return "ClaudeCodeProvider"
+ return "ClaudeCodeProvider"
end
--- @return string
function ClaudeCodeProvider._get_default_model()
- return "claude-sonnet-4-5"
+ return "claude-sonnet-4-5"
end
-- TODO: the claude CLI has no way to list available models.
@@ -216,16 +284,16 @@ end
-- Until Anthropic adds a CLI command for this, we have to hardcode the list here.
-- See https://github.com/anthropics/claude-code/issues/12612
function ClaudeCodeProvider.fetch_models(callback)
- callback({
- "claude-opus-4-6",
- "claude-sonnet-4-5",
- "claude-haiku-4-5",
- "claude-opus-4-5",
- "claude-opus-4-1",
- "claude-sonnet-4-0",
- "claude-opus-4-0",
- "claude-3-7-sonnet-latest",
- }, nil)
+ callback({
+ "claude-opus-4-6",
+ "claude-sonnet-4-5",
+ "claude-haiku-4-5",
+ "claude-opus-4-5",
+ "claude-opus-4-1",
+ "claude-sonnet-4-0",
+ "claude-opus-4-0",
+ "claude-3-7-sonnet-latest",
+ }, nil)
end
--- @class CursorAgentProvider : _99.Providers.BaseProvider
@@ -235,38 +303,40 @@ local CursorAgentProvider = setmetatable({}, { __index = BaseProvider })
--- @param context _99.Prompt
--- @return string[]
function CursorAgentProvider._build_command(_, query, context)
- return { "cursor-agent", "--model", context.model, "--print", query }
+ return { "cursor-agent", "--model", context.model, "--print", query }
end
--- @return string
function CursorAgentProvider._get_provider_name()
- return "CursorAgentProvider"
+ return "CursorAgentProvider"
end
--- @return string
function CursorAgentProvider._get_default_model()
- return "sonnet-4.5"
+ return "sonnet-4.5"
end
function CursorAgentProvider.fetch_models(callback)
- vim.system({ "cursor-agent", "models" }, { text = true }, function(obj)
- vim.schedule(function()
- if obj.code ~= 0 then
- callback(nil, "Failed to fetch models from cursor-agent")
- return
- end
- local models = {}
- for _, line in ipairs(vim.split(obj.stdout, "\n", { trimempty = true })) do
- -- `cursor-agent models` outputs lines like "model-id - description",
- -- so we grab everything before the first " - " separator
- local id = line:match("^(%S+)%s+%-")
- if id then
- table.insert(models, id)
- end
- end
- callback(models, nil)
+ vim.system({ "cursor-agent", "models" }, { text = true }, function(obj)
+ vim.schedule(function()
+ if obj.code ~= 0 then
+ callback(nil, "Failed to fetch models from cursor-agent")
+ return
+ end
+ local models = {}
+ for _, line in
+ ipairs(vim.split(obj.stdout, "\n", { trimempty = true }))
+ do
+ -- `cursor-agent models` outputs lines like "model-id - description",
+ -- so we grab everything before the first " - " separator
+ local id = line:match("^(%S+)%s+%-")
+ if id then
+ table.insert(models, id)
+ end
+ end
+ callback(models, nil)
+ end)
end)
- end)
end
--- @class KiroProvider : _99.Providers.BaseProvider
@@ -276,25 +346,25 @@ local KiroProvider = setmetatable({}, { __index = BaseProvider })
--- @param context _99.Prompt
--- @return string[]
function KiroProvider._build_command(_, query, context)
- return {
- "kiro-cli",
- "chat",
- "--no-interactive",
- "--model",
- context.model,
- "--trust-all-tools",
- query,
- }
+ return {
+ "kiro-cli",
+ "chat",
+ "--no-interactive",
+ "--model",
+ context.model,
+ "--trust-all-tools",
+ query,
+ }
end
--- @return string
function KiroProvider._get_provider_name()
- return "KiroProvider"
+ return "KiroProvider"
end
--- @return string
function KiroProvider._get_default_model()
- return "claude-sonnet-4.5"
+ return "claude-sonnet-4.5"
end
--- @class GeminiCLIProvider : _99.Providers.BaseProvider
@@ -304,36 +374,74 @@ local GeminiCLIProvider = setmetatable({}, { __index = BaseProvider })
--- @param context _99.Prompt
--- @return string[]
function GeminiCLIProvider._build_command(_, query, context)
- return {
- "gemini",
- "--approval-mode",
- -- Allow writing to temp files by default. See:
- -- https://geminicli.com/docs/core/policy-engine/#default-policies
- "auto_edit",
- "--model",
- context.model,
- "--prompt",
- query,
- }
+ return {
+ "gemini",
+ "--approval-mode",
+ -- Allow writing to temp files by default. See:
+ -- https://geminicli.com/docs/core/policy-engine/#default-policies
+ "auto_edit",
+ "--model",
+ context.model,
+ "--prompt",
+ query,
+ }
end
--- @return string
function GeminiCLIProvider._get_provider_name()
- return "GeminiCLIProvider"
+ return "GeminiCLIProvider"
end
--- @return string
function GeminiCLIProvider._get_default_model()
- -- Default to auto-routing between pro and flash. See:
- -- https://geminicli.com/docs/cli/model/
- return "auto"
+ -- Default to auto-routing between pro and flash. See:
+ -- https://geminicli.com/docs/cli/model/
+ return "auto"
+end
+
+--- @class BareMetalProvider : _99.Providers.BaseProvider
+--- @field _clean_response fun(self: _99.Providers.BaseProvider, data : string): string
+local BareMetalProvider = setmetatable({}, { __index = BaseProvider })
+
+--- @param query string
+--- @param context _99.Prompt
+--- @return string[]
+function BareMetalProvider._build_command(_, query, context)
+ return {
+ "curl",
+ "-s",
+ context.endpoint,
+ "-H",
+ '"Content-Type: applications/json"',
+ "-d",
+ '"{"messages":[{"role":"system","content":"You are a strict code completion backend for Neovim. Your input will be a code snippet, function signature, or a comment requesting code. CRITICAL DIRECTIONS: 1. Output ONLY valid, executable programming code. 2. Do NOT wrap your response in markdown code blocks. 3. Do NOT include any conversational filler, explanations, greetings, or sign-offs. 4. If you cannot fulfill the request, output nothing or a code comment explaining why."},{"role":"user","content":"'
+ .. query
+ .. '"}],"temperature":0.0,"stream":false}"',
+ }
+end
+
+--- @return string
+function BareMetalProvider._get_provider_name()
+ return "BareMetalProvider"
+end
+
+--- @return string
+function BareMetalProvider._get_default_model()
+ return "Qwen2.5-Coder-3B-Instruct"
+end
+
+function BareMetalProvider.fetch_models(callback)
+ callback({
+ "Qwen2.5-Coder-3B-Instruct",
+ }, nil)
end
return {
- BaseProvider = BaseProvider,
- OpenCodeProvider = OpenCodeProvider,
- ClaudeCodeProvider = ClaudeCodeProvider,
- CursorAgentProvider = CursorAgentProvider,
- KiroProvider = KiroProvider,
- GeminiCLIProvider = GeminiCLIProvider,
+ BaseProvider = BaseProvider,
+ OpenCodeProvider = OpenCodeProvider,
+ ClaudeCodeProvider = ClaudeCodeProvider,
+ CursorAgentProvider = CursorAgentProvider,
+ KiroProvider = KiroProvider,
+ GeminiCLIProvider = GeminiCLIProvider,
+ BareMetalProvider = BareMetalProvider,
}
diff --git a/lua/99/state.lua b/lua/99/state.lua
index b4ac692..3c2b64d 100644
--- a/lua/99/state.lua
+++ b/lua/99/state.lua
@@ -6,11 +6,12 @@ local Window = require("99.window")
local _99_STATE_FILE = "state"
local function default_completion()
- return { source = nil, custom_rules = {} }
+ return { source = nil, custom_rules = {} }
end
--- @class _99.StateProps
--- @field model string
+--- @field endpoint string
--- @field md_files string[]
--- @field prompts _99.Prompts
--- @field ai_stdout_rows number
@@ -25,6 +26,7 @@ end
--- @class _99.State
--- @field completion _99.Completion
--- @field model string
+--- @field endpoint string?
--- @field md_files string[]
--- @field prompts _99.Prompts
--- @field ai_stdout_rows number
@@ -39,77 +41,81 @@ State.__index = State
--- @return _99.StateProps
local function create()
- return {
- model = "opencode/claude-sonnet-4-5",
- md_files = {},
- ai_stdout_rows = 3,
- display_errors = false,
- provider_override = nil,
- tmp_dir = nil,
- }
+ return {
+ model = "qwen2.5-coder-3b-instruct-gguf",
+ endpoint = "",
+ md_files = {},
+ ai_stdout_rows = 3,
+ display_errors = false,
+ provider_override = nil,
+ tmp_dir = nil,
+ }
end
--- @param oos _99.Options | _99.State
local function get_tmp_dir(oos)
- local tmp_dir = oos.tmp_dir and type(oos.tmp_dir) == "string" and oos.tmp_dir
- or oos.__tmp_dir and oos.__tmp_dir
- or "./tmp"
- if tmp_dir then
- tmp_dir = vim.fn.expand(tmp_dir)
- end
- return tmp_dir
+ local tmp_dir = oos.tmp_dir
+ and type(oos.tmp_dir) == "string"
+ and oos.tmp_dir
+ or oos.__tmp_dir and oos.__tmp_dir
+ or "./tmp"
+ if tmp_dir then
+ tmp_dir = vim.fn.expand(tmp_dir)
+ end
+ return tmp_dir
end
--- @param opts _99.Options
--- @return _99.State.Tracking.Serialized | nil
local function read_state_from_tmp(opts)
- local state_file = utils.named_tmp_file(get_tmp_dir(opts), _99_STATE_FILE)
- return utils.read_file_json_safe(state_file) --[[@as _99.State.Tracking.Serialized]]
+ local state_file = utils.named_tmp_file(get_tmp_dir(opts), _99_STATE_FILE)
+ return utils.read_file_json_safe(state_file) --[[@as _99.State.Tracking.Serialized]]
end
--- @param opts _99.Options
--- @return _99.State
function State.new(opts)
- local props = create()
- local _99_state = setmetatable(props, State) --[[@as _99.State]]
+ local props = create()
+ local _99_state = setmetatable(props, State) --[[@as _99.State]]
- _99_state.provider_override = opts.provider
- _99_state.provider_extra_args = opts.provider_extra_args or {}
- _99_state.completion = opts.completion or default_completion()
- _99_state.completion.custom_rules = _99_state.completion.custom_rules or {}
- _99_state.completion.files = _99_state.completion.files or {}
+ _99_state.provider_override = opts.provider
+ _99_state.endpoint = opts.endpoint
+ _99_state.provider_extra_args = opts.provider_extra_args or {}
+ _99_state.completion = opts.completion or default_completion()
+ _99_state.completion.custom_rules = _99_state.completion.custom_rules or {}
+ _99_state.completion.files = _99_state.completion.files or {}
- --- TODO: Prompt overrides would be a great thing, we just have to get there
- --- for now, i am going to have this as just a hardcoded ... thing
- _99_state.prompts = require("99.prompt-settings")
+ --- TODO: Prompt overrides would be a great thing, we just have to get there
+ --- for now, i am going to have this as just a hardcoded ... thing
+ _99_state.prompts = require("99.prompt-settings")
- local previous = read_state_from_tmp(opts)
- _99_state.tracking = Tracking.new(_99_state, previous)
+ local previous = read_state_from_tmp(opts)
+ _99_state.tracking = Tracking.new(_99_state, previous)
- return _99_state
+ return _99_state
end
function State:sync()
- local tracking = self.tracking:serialize()
- local tmp = self:tmp_dir()
- local file = utils.named_tmp_file(tmp, _99_STATE_FILE)
- utils.write_file_json_safe(tracking, file)
+ local tracking = self.tracking:serialize()
+ local tmp = self:tmp_dir()
+ local file = utils.named_tmp_file(tmp, _99_STATE_FILE)
+ utils.write_file_json_safe(tracking, file)
end
--- @return string
function State:tmp_dir()
- return get_tmp_dir(self)
+ return get_tmp_dir(self)
end
--- @return boolean
function State:active()
- _ = self
- if Window.has_active_window() then
- return true
- end
+ _ = self
+ if Window.has_active_window() then
+ return true
+ end
- local qf = vim.fn.getqflist({ winid = 0 })
- return qf.winid ~= 0
+ local qf = vim.fn.getqflist({ winid = 0 })
+ return qf.winid ~= 0
end
--- TODO: This is something to understand. I bet that this is going to need
@@ -122,8 +128,8 @@ end
--- 3. do the operation once at setup instead of every time.
--- likely not needed to do this all the time.
function State:refresh_rules()
- self.rules = Agents.rules(self)
- Extensions.refresh(self)
+ self.rules = Agents.rules(self)
+ Extensions.refresh(self)
end
return State