summaryrefslogtreecommitdiff
path: root/lua
diff options
context:
space:
mode:
authorcodegirl007 <s.raide@gmail.com>2026-02-08 18:04:32 -0800
committerGitHub <noreply@github.com>2026-02-08 18:04:32 -0800
commite4050fe6164765202814b353db43da75c1947af6 (patch)
tree8e9fe8b195edcde1d3b37134be672655d169f5e1 /lua
parent17be2bff90a22d8bafc102e3ca1730bb05026841 (diff)
parent76acc1d15d8a2161d43b60ee8c5d49c1a5885c60 (diff)
downloada4-e4050fe6164765202814b353db43da75c1947af6.tar.xz
a4-e4050fe6164765202814b353db43da75c1947af6.zip
Merge branch 'master' into back-that-hash-up
Diffstat (limited to 'lua')
-rw-r--r--lua/99/init.lua9
-rw-r--r--lua/99/ops/search.lua70
-rw-r--r--lua/99/ops/throbber.lua94
-rw-r--r--lua/99/prompt-settings.lua19
-rw-r--r--lua/99/providers.lua10
-rw-r--r--lua/99/request-context.lua7
-rw-r--r--lua/99/test/providers_spec.lua13
-rw-r--r--lua/99/test/throbber_spec.lua91
-rw-r--r--lua/99/window/init.lua9
9 files changed, 300 insertions, 22 deletions
diff --git a/lua/99/init.lua b/lua/99/init.lua
index d48c26d..4f27543 100644
--- a/lua/99/init.lua
+++ b/lua/99/init.lua
@@ -256,10 +256,11 @@ local function set_selection_marks()
end
--- @param cb fun(context: _99.RequestContext, o: _99.ops.Opts?): nil
+--- @param name string
--- @param context _99.RequestContext
--- @param opts _99.ops.Opts
-local function capture_prompt(cb, context, opts)
- Window.capture_input({
+local function capture_prompt(cb, name, context, opts)
+ Window.capture_input(name, {
--- @param ok boolean
--- @param response string
cb = function(ok, response)
@@ -331,7 +332,7 @@ function _99.search(opts)
ops.search(context, o)
return
else
- capture_prompt(ops.search, context, o)
+ capture_prompt(ops.search, "Search", context, o)
end
end
@@ -368,7 +369,7 @@ function _99.visual(opts)
if opts.additional_prompt then
perform_range()
else
- capture_prompt(perform_range, context, opts)
+ capture_prompt(perform_range, "Visual", context, opts)
end
end
diff --git a/lua/99/ops/search.lua b/lua/99/ops/search.lua
index aa3d08a..a4017c0 100644
--- a/lua/99/ops/search.lua
+++ b/lua/99/ops/search.lua
@@ -1,7 +1,69 @@
+local Request = require("99.request")
+local make_clean_up = require("99.ops.clean-up")
+local Agents = require("99.extensions.agents")
+
+--- @param response string
+local function create_search_locations(response)
+ local lines = vim.split(response, "\n")
+ print(vim.inspect(lines))
+end
+
--- @param context _99.RequestContext
---@param opts _99.ops.SearchOpts
-return function(context, opts)
- _ = context
- _ = opts
- error("search not implemented")
+local function search(context, opts)
+ opts = opts or {}
+ local user_prompt = opts.additional_prompt
+ assert(user_prompt, "search requires a prompt to run, please provide prompt")
+
+ local logger = context.logger:set_area("search")
+ local request = Request.new(context)
+
+ logger:debug("search", "with opts", opts.additional_prompt)
+
+ -- TODO: How to surface progress.. I was thinking about a status line plugin
+ -- local top_status = RequestStatus.new(
+ -- 250,
+ -- context._99.ai_stdout_rows or 1,
+ -- "Implementing",
+ -- top_mark
+ -- )
+ local clean_up = make_clean_up(context, function()
+ request:cancel()
+ end)
+
+ local full_prompt = context._99.prompts.prompts.semantic_search()
+ full_prompt = context._99.prompts.prompts.prompt(user_prompt, full_prompt)
+ local rules = Agents.find_rules(context._99.rules, user_prompt)
+ context:add_agent_rules(rules)
+
+ local additional_rules = opts.additional_rules
+ if additional_rules then
+ context:add_agent_rules(additional_rules)
+ end
+
+ request:add_prompt_content(full_prompt)
+ 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(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,
+ })
end
+return search
diff --git a/lua/99/ops/throbber.lua b/lua/99/ops/throbber.lua
new file mode 100644
index 0000000..b994fcd
--- /dev/null
+++ b/lua/99/ops/throbber.lua
@@ -0,0 +1,94 @@
+local time = require("99.time")
+local throb_icons = {
+ { "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" },
+ { "◐", "◓", "◑", "◒" },
+ { "⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷" },
+ { "◰", "◳", "◲", "◱" },
+ { "◜", "◠", "◝", "◞", "◡", "◟" },
+}
+
+--- @alias _99.Throbber.ThrobFN fun(perc: number): string
+--- @alias _99.Throbber.EaseFN fun(perc: number): number
+
+--- @param ease_fn _99.Throbber.EaseFN
+--- @return _99.Throbber.ThrobFN
+local function create_throbber(ease_fn)
+ ease_fn = ease_fn or function(p)
+ return p
+ end
+ local icon_set = throb_icons[math.random(#throb_icons)]
+ return function(percent)
+ local eased = ease_fn(percent)
+ local index = math.floor(eased * #icon_set) + 1
+ return icon_set[math.min(index, #icon_set)]
+ end
+end
+
+local function ease_in_ease_out_cubic(percent)
+ if percent < 0.5 then
+ return 4 * percent * percent * percent
+ else
+ local f = (2 * percent) - 2
+ return 1 - (f * f * f / 2)
+ end
+end
+
+local throb_time = 1000
+local cooldown_time = 500
+
+--- @class _99.Throbber
+--- @field start_time number
+--- @field section_time number
+--- @field state "init" | "throbbing" | "cooldown" | "stopped"
+--- @field throb_fn _99.Throbber.ThrobFN
+--- @field cb fun(str: string): nil
+local Throbber = {}
+Throbber.__index = Throbber
+
+--- @param cb fun(str: string): nil
+--- @return _99.Throbber
+function Throbber.new(cb)
+ return setmetatable({
+ state = "init",
+ start_time = 0,
+ section_time = 0,
+ cb = cb,
+ throb_fn = create_throbber(ease_in_ease_out_cubic),
+ }, Throbber)
+end
+
+function Throbber:_run()
+ if self.state ~= "throbbing" and self.state ~= "cooldown" then
+ return
+ end
+
+ local elapsed = time.now() - self.start_time
+ local percent = math.min(1, elapsed / self.section_time)
+ local icon = self.throb_fn(self.state == "throbbing" and percent or 1)
+
+ if percent == 1 then
+ self.state = self.state == "cooldown" and "throbbing" or "cooldown"
+ self.start_time = time.now()
+ self.section_time = self.state == "cooldown" and cooldown_time or throb_time
+ end
+
+ self.cb(icon)
+ vim.defer_fn(function()
+ self:_run()
+ end, 75)
+end
+
+function Throbber:start()
+ self.start_time = time.now()
+ self.section_time = throb_time
+ self.state = "throbbing"
+ self:_run()
+end
+
+function Throbber:stop()
+ self.state = "stopped"
+end
+
+Throbber._icons = throb_icons
+
+return Throbber
diff --git a/lua/99/prompt-settings.lua b/lua/99/prompt-settings.lua
index 45f46dd..0bc564d 100644
--- a/lua/99/prompt-settings.lua
+++ b/lua/99/prompt-settings.lua
@@ -8,7 +8,7 @@ end
--- @class _99.Prompts.SpecificOperations
--- @field visual_selection fun(range: _99.Range): string
--- @field semantic_search fun(): string
---- @field prompt fun(prompt: string, action: string, name: string): string
+--- @field prompt fun(prompt: string, action: string, name?: string): string
--- @field role fun(): string
--- @field read_tmp fun(): string
local prompts = {
@@ -19,21 +19,26 @@ local prompts = {
return [[
you are given a prompt and you must search through this project and return code that matches the description provided.
<Rule>You must provide output without any commentary, just text locations</Rule>
-<Rule>Text locations are in the format of: /path/to/file.ext:lnum:cnum,X
+<Rule>Text locations are in the format of: /path/to/file.ext:lnum:cnum,X,NOTES
lnum = starting line number 1 based
cnum = starting column number 1 based
X = how many lines should be highlighted
+NOTES = A text description of why this highlight is important
</Rule>
+<Rule>NOTES cannot have new lines</Rule>
+<Rule>You must adhere to the output format</Rule>
+<Rule>Double check output format before writing it to the file</Rule>
<Rule>Each location is separated by new lines</Rule>
<Rule>Each path is specified in absolute pathing</Rule>
+<Rule>You can provide notes you think are relevant per location</Rule>
<Example>
You have found 3 locations in files foo.js, bar.js, and baz.js.
There are 2 locations in foo.js, 1 in bar.js and baz.js.
<Output>
-/path/to/project/src/foo.js:24:8,3
-/path/to/project/src/foo.js:71:12,7
-/path/to/project/src/bar.js:13:2,1
-/path/to/project/src/baz.js:1:1,52
+/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!
+/path/to/project/src/bar.js:13:2,1,more notes again, this time specfically about bar and why bar is so important
+/path/to/project/src/baz.js:1:1,52,Notes about why baz is very important to the results
</Output>
<Meaning>
This means that the search results found
@@ -53,7 +58,7 @@ ONLY provide requested changes by writing the change to TEMP_FILE
end,
--- @param prompt string
--- @param action string
- --- @param name string defaults to DIRECTIONS
+ --- @param name? string defaults to DIRECTIONS
--- @return string
prompt = function(prompt, action, name)
name = name or "DIRECTIONS"
diff --git a/lua/99/providers.lua b/lua/99/providers.lua
index ab56738..7d991b2 100644
--- a/lua/99/providers.lua
+++ b/lua/99/providers.lua
@@ -141,7 +141,15 @@ local OpenCodeProvider = setmetatable({}, { __index = BaseProvider })
--- @param request _99.Request
--- @return string[]
function OpenCodeProvider._build_command(_, query, request)
- return { "opencode", "run", "-m", request.context.model, query }
+ return {
+ "opencode",
+ "run",
+ "--agent",
+ "build",
+ "-m",
+ request.context.model,
+ query,
+ }
end
--- @return string
diff --git a/lua/99/request-context.lua b/lua/99/request-context.lua
index 358cdd2..6671cab 100644
--- a/lua/99/request-context.lua
+++ b/lua/99/request-context.lua
@@ -136,6 +136,13 @@ end
--- @param prompt string
function RequestContext:save_prompt(prompt)
local prompt_file = self.tmp_file .. "-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)
+ end
+
local file = io.open(prompt_file, "w")
if file then
file:write(prompt)
diff --git a/lua/99/test/providers_spec.lua b/lua/99/test/providers_spec.lua
index 2c75030..809080b 100644
--- a/lua/99/test/providers_spec.lua
+++ b/lua/99/test/providers_spec.lua
@@ -8,10 +8,15 @@ describe("providers", function()
local request = { context = { model = "anthropic/claude-sonnet-4-5" } }
local cmd =
Providers.OpenCodeProvider._build_command(nil, "test query", request)
- eq(
- { "opencode", "run", "-m", "anthropic/claude-sonnet-4-5", "test query" },
- cmd
- )
+ eq({
+ "opencode",
+ "run",
+ "--agent",
+ "build",
+ "-m",
+ "anthropic/claude-sonnet-4-5",
+ "test query",
+ }, cmd)
end)
it("has correct default model", function()
diff --git a/lua/99/test/throbber_spec.lua b/lua/99/test/throbber_spec.lua
new file mode 100644
index 0000000..39425a5
--- /dev/null
+++ b/lua/99/test/throbber_spec.lua
@@ -0,0 +1,91 @@
+---- PURELY AI GENERATED FILE -----
+---- This could be crap test, i did about ~15 second code review,
+---- looks mostly correct, a little weird, but close enough ----
+local Throbber = require("99.ops.throbber")
+local eq = assert.are.same
+
+describe("Throbber", function()
+ it(
+ "cycles through throb, cooldown, and restart phases, then stops",
+ function()
+ local test_icons = { "a", "b", "c", "d", "e" }
+ local original_icons = Throbber._icons
+ Throbber._icons = { test_icons }
+
+ local received = {}
+ local states = {}
+ --- @type _99.Throbber
+ local throbber
+ throbber = Throbber.new(function(icon)
+ table.insert(received, icon)
+ table.insert(states, throbber.state)
+ end)
+ throbber.throb_fn = function(percent)
+ local index = math.floor(percent * #test_icons) + 1
+ return test_icons[math.min(index, #test_icons)]
+ end
+
+ -- Start throbbing
+ throbber:start()
+ vim.wait(800)
+
+ -- Verify we cycled through multiple icons (throb phase)
+ local seen_icons = {}
+ for _, icon in ipairs(received) do
+ seen_icons[icon] = true
+ end
+ assert.is_true(
+ seen_icons["a"] and seen_icons["b"] and seen_icons["c"],
+ "Expected to cycle through multiple icons during throb phase"
+ )
+
+ -- Wait for cooldown (should stay on first icon)
+ local icon_count_before_cooldown = #received
+ vim.wait(600)
+
+ -- Verify cooldown: stays on first icon
+ local cooldown_icons = {}
+ for i = icon_count_before_cooldown + 1, #received do
+ table.insert(cooldown_icons, received[i])
+ end
+ for _, icon in ipairs(cooldown_icons) do
+ eq("e", icon, "Expected cooldown to stay on last icon")
+ end
+
+ -- Wait for second throb cycle to start
+ vim.wait(600)
+
+ -- Verify we transitioned back to throbbing state
+ local seen_throbbing = false
+ local seen_cooldown = false
+ local seen_second_throb = false
+ for _, state in ipairs(states) do
+ if state == "throbbing" then
+ if not seen_throbbing then
+ seen_throbbing = true
+ elseif seen_cooldown then
+ seen_second_throb = true
+ end
+ elseif state == "cooldown" then
+ seen_cooldown = true
+ end
+ end
+ assert.is_true(seen_throbbing, "Expected to see throbbing state")
+ assert.is_true(seen_cooldown, "Expected to see cooldown state")
+ assert.is_true(
+ seen_second_throb,
+ "Expected to see second throbbing cycle"
+ )
+
+ -- Stop the throbber
+ throbber:stop()
+ local count_after_stop = #received
+
+ -- Verify no more updates after stop
+ vim.wait(300)
+ eq(count_after_stop, #received, "Expected no more updates after stop")
+
+ Throbber._icons = original_icons
+ end
+ )
+end)
diff --git a/lua/99/window/init.lua b/lua/99/window/init.lua
index 358b83b..6451ede 100644
--- a/lua/99/window/init.lua
+++ b/lua/99/window/init.lua
@@ -336,13 +336,14 @@ end
--- @field on_load? fun(): nil
--- @field rules _99.Agents.Rules
+--- @param name string
--- @param opts _99.window.CaptureInputOpts
-function M.capture_input(opts)
+function M.capture_input(name, opts)
M.clear_active_popups()
local config = create_centered_window()
local win = create_floating_window(config, {
- title = " 99 Prompt ",
+ title = string.format(" 99 %s ", name),
border = "rounded",
})
set_defaul_win_options(win, "99-prompt")
@@ -420,4 +421,8 @@ function M.clear_active_popups()
M.active_windows = {}
end
+function M.status_window()
+ M.clear_active_popups()
+end
+
return M