summaryrefslogtreecommitdiff
path: root/lua
diff options
context:
space:
mode:
authortheprimeagain <the.primeagen@gmail.com>2026-02-23 19:50:47 -0700
committertheprimeagain <the.primeagen@gmail.com>2026-02-23 19:50:47 -0700
commit6d80dccf18b0bb66415d355857cb5c11d777d297 (patch)
treed5ab4ce9c663d7209099301afdcb293093ca1652 /lua
parent6a64e0b2f4c7f1e3911db1f8318e5d7c68cb8dff (diff)
downloada4-6d80dccf18b0bb66415d355857cb5c11d777d297.tar.xz
a4-6d80dccf18b0bb66415d355857cb5c11d777d297.zip
beginning the vibe work
Diffstat (limited to 'lua')
-rw-r--r--lua/99/extensions/work/worker.lua26
-rw-r--r--lua/99/init.lua25
-rw-r--r--lua/99/ops/init.lua1
-rw-r--r--lua/99/ops/qfix-helpers.lua43
-rw-r--r--lua/99/ops/search.lua46
-rw-r--r--lua/99/ops/vibe.lua52
-rw-r--r--lua/99/prompt.lua41
-rw-r--r--lua/99/providers.lua1
-rw-r--r--lua/99/test/qfix_helpers_spec.lua86
9 files changed, 265 insertions, 56 deletions
diff --git a/lua/99/extensions/work/worker.lua b/lua/99/extensions/work/worker.lua
index 780b2f2..be38aca 100644
--- a/lua/99/extensions/work/worker.lua
+++ b/lua/99/extensions/work/worker.lua
@@ -1,10 +1,26 @@
local Window = require("99.window")
local utils = require("99.utils")
---- @class _99.Extension.Worker
+--- @class _99.Extensions.Worker
+--- A persistent way to keep track of work.
+---
+--- this will likely be where the most change and focus goes into. I would like
+--- to take this into worktree territory and be able to swap between stuff super
+--- slick.
+---
+--- Until then, it is going to be a single bit of work that you can provide
+--- the description and then use search to find what is left that needs to be done.
+--- @docs base
+--- @field set_work fun(opts?: _99.WorkOpts): nil
+--- will set the work for the project. If opts provide a description then no
+--- input capture of work description will be required
+--- @field work fun(): nil
+--- will use _99.search to find what is left to be done for this work item to be
+--- considered done
local M = {}
--- @class _99.WorkOpts
+--- @docs included
--- @field description string | nil
--- @return string
@@ -49,7 +65,7 @@ local function set_work_item_cb(success, result)
end
end
-function M.updated_work()
+function M.update_work()
local work = M.current_work_item
or "Put in the description of the work you want to complete"
Window.capture_input(" Work ", {
@@ -67,7 +83,7 @@ function M.set_work(opts)
else
Window.capture_input(" Work ", {
cb = set_work_item_cb,
- content = { "Put in the description of the work you want to complete" },
+ content = { M.current_work_item or "Put in the description of the work you want to complete" },
})
end
@@ -76,7 +92,7 @@ function M.set_work(opts)
end
--- craft_prompt can be overridden so you can create your own prompt
---- @param worker _99.Extension.Worker
+--- @param worker _99.Extensions.Worker
--- @return string
function M.craft_prompt(worker)
return string.format(
@@ -136,7 +152,7 @@ function M.last_search_results()
return
end
- require("99").qfix_search_results(M.last_work_search)
+ require("99").qfix(M.last_work_search)
end
return M
diff --git a/lua/99/init.lua b/lua/99/init.lua
index 6e7e1e4..f0c7132 100644
--- a/lua/99/init.lua
+++ b/lua/99/init.lua
@@ -197,6 +197,8 @@ local _99_state
--- be killed (OpenCode) and any result will be discared
--- @field clear_previous_requests fun(): nil
--- clears all previous search and visual operations
+--- @field Extensions _99.Extensions
+--- clears all previous search and visual operations
local _99 = {
DEBUG = Level.DEBUG,
INFO = Level.INFO,
@@ -311,6 +313,19 @@ function _99:rule_from_path(path)
return Agents.get_rule_by_path(_99_state.rules, path)
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
+ 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)
@@ -402,14 +417,13 @@ function _99.clear_all_marks()
end
--- @param xid number | nil
-function _99.qfix_search_results(xid)
+function _99.qfix(xid)
--- @type _99.Prompt
local entry = _99_state.__request_by_id[xid]
assert(entry, "qfix_search_results could not find id: " .. xid)
- local data = entry:search_data()
- local items = data.qfix_items
- vim.fn.setqflist({}, "r", { title = "99 Search Results", items = items })
+ local items = entry:qfix_data()
+ vim.fn.setqflist({}, "r", { title = "99 Results", items = items })
vim.cmd("copen")
end
@@ -529,6 +543,9 @@ function _99.__debug()
end
_99.Providers = Providers
+
+--- @class _99.Extensions
+--- @field Worker _99.Extensions.Worker
_99.Extensions = {
Worker = require("99.extensions.work.worker"),
}
diff --git a/lua/99/ops/init.lua b/lua/99/ops/init.lua
index e4596c2..d45b1a5 100644
--- a/lua/99/ops/init.lua
+++ b/lua/99/ops/init.lua
@@ -35,4 +35,5 @@ return {
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/qfix-helpers.lua b/lua/99/ops/qfix-helpers.lua
new file mode 100644
index 0000000..8d7ee63
--- /dev/null
+++ b/lua/99/ops/qfix-helpers.lua
@@ -0,0 +1,43 @@
+local M = {}
+
+--- @return _99.Search.Result | nil
+function M.parse_line(line)
+ local parts = vim.split(line, ":", { plain = true })
+ if #parts ~= 3 then
+ return nil
+ end
+
+ local filepath = parts[1]
+ local lnum = parts[2]
+ local comma_parts = vim.split(parts[3], ",", { plain = true })
+ local col = comma_parts[1]
+ local notes = nil
+
+ if #comma_parts >= 2 then
+ notes = table.concat(comma_parts, ",", 2)
+ end
+
+ return {
+ filename = filepath,
+ lnum = tonumber(lnum) or 1,
+ col = tonumber(col) or 1,
+ text = notes or "",
+ }
+end
+
+--- @param response string
+--- @return _99.Search.Result[]
+function M.create_qfix_entries(response)
+ local lines = vim.split(response, "\n")
+ local qf_list = {} --[[ @as _99.Search.Result[] ]]
+
+ for _, line in ipairs(lines) do
+ local res = M.parse_line(line)
+ if res then
+ table.insert(qf_list, res)
+ end
+ end
+ return qf_list
+end
+
+return M
diff --git a/lua/99/ops/search.lua b/lua/99/ops/search.lua
index dda70d1..0619f47 100644
--- a/lua/99/ops/search.lua
+++ b/lua/99/ops/search.lua
@@ -1,59 +1,23 @@
local make_prompt = require("99.ops.make-prompt")
local CleanUp = require("99.ops.clean-up")
+local QFixHelpers = require("99.ops.qfix-helpers")
local make_clean_up = CleanUp.make_clean_up
local make_observer = CleanUp.make_observer
---- @class _99.Search.Result
---- @field filename string
---- @field lnum number
---- @field col number
---- @field text string
-
---- @return _99.Search.Result | nil
-local function parse_line(line)
- local parts = vim.split(line, ":", { plain = true })
- if #parts ~= 3 then
- return nil
- end
-
- local filepath = parts[1]
- local lnum = parts[2]
- local comma_parts = vim.split(parts[3], ",", { plain = true })
- local col = comma_parts[1]
- local notes = nil
-
- if #comma_parts >= 2 then
- notes = table.concat(comma_parts, ",", 2)
- end
-
- return {
- filename = filepath,
- lnum = tonumber(lnum) or 1,
- col = tonumber(col) or 1,
- text = notes or "",
- }
-end
-
--- @param context _99.Prompt
--- @param response string
local function create_search_locations(context, response)
- local lines = vim.split(response, "\n")
- local qf_list = {}
-
- for _, line in ipairs(lines) do
- local res = parse_line(line)
- if res then
- table.insert(qf_list, res)
- end
- end
+ local qf_list = QFixHelpers.create_qfix_entries(response)
+ context.logger:set_area("search"):debug("qf_list created", "qf_list", qf_list)
context.data = {
type = "search",
qfix_items = qf_list,
+ response = response,
}
if #qf_list > 0 then
- require("99").qfix_search_results(context.xid)
+ require("99").qfix(context.xid)
else
vim.notify("No search results found", vim.log.levels.INFO)
end
diff --git a/lua/99/ops/vibe.lua b/lua/99/ops/vibe.lua
new file mode 100644
index 0000000..291fc45
--- /dev/null
+++ b/lua/99/ops/vibe.lua
@@ -0,0 +1,52 @@
+local make_prompt = require("99.ops.make-prompt")
+local CleanUp = require("99.ops.clean-up")
+
+local make_clean_up = CleanUp.make_clean_up
+local make_observer = CleanUp.make_observer
+
+--- @class _99.Search.Result
+--- @field filename string
+--- @field lnum number
+--- @field col number
+--- @field text string
+
+--- @param context _99.Prompt
+---@param opts _99.ops.SearchOpts
+local function search(context, opts)
+ opts = opts or {}
+
+ local logger = context.logger:set_area("search")
+ logger:debug("search", "with opts", opts.additional_prompt)
+
+ local clean_up = make_clean_up(function()
+ context:stop()
+ end)
+
+ local prompt, refs =
+ make_prompt(context, context._99.prompts.prompts.semantic_search(), opts)
+
+ context:add_prompt_content(prompt)
+ context:add_references(refs)
+ context:add_clean_up(clean_up)
+
+ --- TODO: part of the context request clean up there needs to be a refactoring of
+ --- make observer... it really should just be within the context observer creation.
+ --- same with cleanup.. that should just be clean_ups from context, instead of a
+ --- once cleanup function wrapper.
+ ---
+ --- i think an interface, CleanUpI could be something that is worth it :)
+ context:start_request(make_observer(clean_up, function(status, response)
+ if status == "cancelled" then
+ logger:debug("request cancelled for search")
+ elseif status == "failed" then
+ logger:error(
+ "request failed for search",
+ "error response",
+ response or "no response provided"
+ )
+ elseif status == "success" then
+ create_search_locations(context, response)
+ end
+ end))
+end
+return search
diff --git a/lua/99/prompt.lua b/lua/99/prompt.lua
index ec1d0ae..840b5ee 100644
--- a/lua/99/prompt.lua
+++ b/lua/99/prompt.lua
@@ -21,8 +21,8 @@ local filetype_map = {
}
-- luacheck: ignore
---- @alias _99.Prompt.Data _99.Prompt.Data.Search | _99.Prompt.Data.Tutorial | _99.Prompt.Data.Visual
---- @alias _99.Prompt.Operation "visual" | "tutorial" | "search"
+--- @alias _99.Prompt.Data _99.Prompt.Data.Search | _99.Prompt.Data.Tutorial | _99.Prompt.Data.Visual | _99.Prompt.Data.Vibe
+--- @alias _99.Prompt.Operation "visual" | "tutorial" | "search" | "vibe"
--- @alias _99.Prompt.EndingState "failed" | "success" | "cancelled"
--- @alias _99.Prompt.State "ready" | "requesting" | _99.Prompt.EndingState
--- @alias _99.Prompt.Cleanup fun(): nil
@@ -30,6 +30,12 @@ local filetype_map = {
--- @class _99.Prompt.Data.Search
--- @field type "search"
--- @field qfix_items _99.Search.Result[]
+--- @field response string
+
+--- @class _99.Prompt.Data.Vibe
+--- @field type "vibe"
+--- @field response string
+--- @field xfix_items _99.Search.Result[]
--- @class _99.Prompt.Data.Visual
--- @field type "visual"
@@ -96,10 +102,23 @@ function Prompt.todo(_99)
assert(false, "not implemented")
end
-function Prompt.vibe(_99, opts)
- _ = _99
- _ = opts
- assert(false, "not implemented")
+--- @param _99 _99.State
+--- @return _99.Prompt
+function Prompt.vibe(_99)
+ _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")
+
+ return context
end
--- @param _99 _99.State
@@ -289,6 +308,16 @@ function Prompt:search_data()
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.xfix_items
+end
+
+
function Prompt:stop()
self:cancel()
for _, cb in ipairs(self.clean_ups) do
diff --git a/lua/99/providers.lua b/lua/99/providers.lua
index 24c3d28..5eab070 100644
--- a/lua/99/providers.lua
+++ b/lua/99/providers.lua
@@ -20,6 +20,7 @@ end
--- @class _99.Providers.BaseProvider
--- @field _build_command fun(self: _99.Providers.BaseProvider, query: string, context: _99.Prompt): string[]
--- @field _get_provider_name fun(self: _99.Providers.BaseProvider): string
+--- @field _get_default_model fun(): string
local BaseProvider = {}
--- @param callback fun(models: string[]|nil, err: string|nil): nil
diff --git a/lua/99/test/qfix_helpers_spec.lua b/lua/99/test/qfix_helpers_spec.lua
new file mode 100644
index 0000000..ad9e5ab
--- /dev/null
+++ b/lua/99/test/qfix_helpers_spec.lua
@@ -0,0 +1,86 @@
+-- luacheck: globals describe it assert
+local QFixHelpers = require("99.ops.qfix-helpers")
+local eq = assert.are.same
+local create_entries = QFixHelpers.create_qfix_entries
+
+describe("qfix helpers", function()
+ it("parse_line parses filename, line, column, and notes", function()
+ local parsed =
+ QFixHelpers.parse_line("lua/99/ops/search.lua:42:7,found semantic search")
+
+ eq({
+ filename = "lua/99/ops/search.lua",
+ lnum = 42,
+ col = 7,
+ text = "found semantic search",
+ }, parsed)
+ end)
+
+ it("parse_line keeps commas in notes text", function()
+ local parsed = QFixHelpers.parse_line("file.lua:10:3,note,with,commas")
+
+ eq("file.lua", parsed.filename)
+ eq(10, parsed.lnum)
+ eq(3, parsed.col)
+ eq("note,with,commas", parsed.text)
+ end)
+
+ it("parse_line returns nil for malformed lines", function()
+ eq(nil, QFixHelpers.parse_line("file.lua:10"))
+ eq(nil, QFixHelpers.parse_line("file.lua:10:3:extra"))
+ end)
+
+ it(
+ "parse_line uses defaults for invalid numbers and missing notes",
+ function()
+ local parsed = QFixHelpers.parse_line("file.lua:notnum:notcol")
+
+ eq({
+ filename = "file.lua",
+ lnum = 1,
+ col = 1,
+ text = "",
+ }, parsed)
+ end
+ )
+
+ it(
+ "create_qfix_entries parses valid lines and skips malformed ones",
+ function()
+ local response = table.concat({
+ "a.lua:1:2,first hit",
+ "not a valid line",
+ "b.lua:3:4",
+ "c.lua:notnum:notcol,fallback values",
+ "",
+ }, "\n")
+
+ local locations = create_entries(response)
+
+ eq({
+ {
+ filename = "a.lua",
+ lnum = 1,
+ col = 2,
+ text = "first hit",
+ },
+ {
+ filename = "b.lua",
+ lnum = 3,
+ col = 4,
+ text = "",
+ },
+ {
+ filename = "c.lua",
+ lnum = 1,
+ col = 1,
+ text = "fallback values",
+ },
+ }, locations)
+ end
+ )
+
+ it("create_qfix_entries returns empty list for empty response", function()
+ eq({}, create_entries(""))
+ end)
+end)