refactoring the context

This commit is contained in:
2026-03-24 21:32:03 -04:00
parent f8ce473877
commit 565e3658b5
16 changed files with 475 additions and 454 deletions

View File

@@ -4,38 +4,36 @@
local function get_brain_completions(prefix)
local items = {}
local ok_brain, brain = pcall(require, "codetyper.brain")
if not ok_brain then
local brain_loaded, brain = pcall(require, "codetyper.brain")
if not brain_loaded then
return items
end
-- Check if brain is initialized safely
local is_init = false
local brain_initialized = false
if brain.is_initialized then
local ok, result = pcall(brain.is_initialized)
is_init = ok and result
local init_check_success, init_state = pcall(brain.is_initialized)
brain_initialized = init_check_success and init_state
end
if not is_init then
if not brain_initialized then
return items
end
-- Query brain for relevant patterns
local ok_query, result = pcall(brain.query, {
local query_success, query_result = pcall(brain.query, {
query = prefix,
max_results = 10,
types = { "pattern" },
})
if ok_query and result and result.nodes then
for _, node in ipairs(result.nodes) do
if query_success and query_result and query_result.nodes then
for _, node in ipairs(query_result.nodes) do
if node.c and node.c.s then
local summary = node.c.s
for name in summary:gmatch("functions:%s*([^;]+)") do
for func in name:gmatch("([%w_]+)") do
if func:lower():find(prefix:lower(), 1, true) then
for matched_functions in summary:gmatch("functions:%s*([^;]+)") do
for func_name in matched_functions:gmatch("([%w_]+)") do
if func_name:lower():find(prefix:lower(), 1, true) then
table.insert(items, {
label = func,
label = func_name,
kind = 3, -- Function
detail = "[brain]",
documentation = summary,
@@ -43,11 +41,11 @@ local function get_brain_completions(prefix)
end
end
end
for name in summary:gmatch("classes:%s*([^;]+)") do
for class in name:gmatch("([%w_]+)") do
if class:lower():find(prefix:lower(), 1, true) then
for matched_classes in summary:gmatch("classes:%s*([^;]+)") do
for class_name in matched_classes:gmatch("([%w_]+)") do
if class_name:lower():find(prefix:lower(), 1, true) then
table.insert(items, {
label = class,
label = class_name,
kind = 7, -- Class
detail = "[brain]",
documentation = summary,

View File

@@ -2,24 +2,18 @@
---@param prefix string
---@return string|nil suggestion
local function get_copilot_suggestion(prefix)
-- Try copilot.lua suggestion API first
local ok, copilot_suggestion = pcall(require, "copilot.suggestion")
if ok and copilot_suggestion and type(copilot_suggestion.get_suggestion) == "function" then
local ok2, suggestion = pcall(copilot_suggestion.get_suggestion)
if ok2 and suggestion and suggestion ~= "" then
if prefix == "" or suggestion:lower():match(prefix:lower(), 1) then
return suggestion
else
return suggestion
end
local suggestion_api_loaded, copilot_suggestion_api = pcall(require, "copilot.suggestion")
if suggestion_api_loaded and copilot_suggestion_api and type(copilot_suggestion_api.get_suggestion) == "function" then
local suggestion_fetch_success, suggestion = pcall(copilot_suggestion_api.get_suggestion)
if suggestion_fetch_success and suggestion and suggestion ~= "" then
return suggestion
end
end
-- Fallback: try older copilot module if present
local ok3, copilot = pcall(require, "copilot")
if ok3 and copilot and type(copilot.get_suggestion) == "function" then
local ok4, suggestion = pcall(copilot.get_suggestion)
if ok4 and suggestion and suggestion ~= "" then
local copilot_loaded, copilot = pcall(require, "copilot")
if copilot_loaded and copilot and type(copilot.get_suggestion) == "function" then
local suggestion_fetch_success, suggestion = pcall(copilot.get_suggestion)
if suggestion_fetch_success and suggestion and suggestion ~= "" then
return suggestion
end
end

View File

@@ -4,54 +4,52 @@
local function get_indexer_completions(prefix)
local items = {}
local ok_indexer, indexer = pcall(require, "codetyper.indexer")
if not ok_indexer then
local indexer_loaded, indexer = pcall(require, "codetyper.indexer")
if not indexer_loaded then
return items
end
local ok_load, index = pcall(indexer.load_index)
if not ok_load or not index then
local index_load_success, index = pcall(indexer.load_index)
if not index_load_success or not index then
return items
end
-- Search symbols
if index.symbols then
for symbol, files in pairs(index.symbols) do
if symbol:lower():find(prefix:lower(), 1, true) then
local files_str = type(files) == "table" and table.concat(files, ", ") or tostring(files)
local files_display = type(files) == "table" and table.concat(files, ", ") or tostring(files)
table.insert(items, {
label = symbol,
kind = 6, -- Variable (generic)
detail = "[index] " .. files_str:sub(1, 30),
documentation = "Symbol found in: " .. files_str,
detail = "[index] " .. files_display:sub(1, 30),
documentation = "Symbol found in: " .. files_display,
})
end
end
end
-- Search functions in files
if index.files then
for filepath, file_index in pairs(index.files) do
if file_index and file_index.functions then
for _, func in ipairs(file_index.functions) do
if func.name and func.name:lower():find(prefix:lower(), 1, true) then
for _, func_entry in ipairs(file_index.functions) do
if func_entry.name and func_entry.name:lower():find(prefix:lower(), 1, true) then
table.insert(items, {
label = func.name,
label = func_entry.name,
kind = 3, -- Function
detail = "[index] " .. vim.fn.fnamemodify(filepath, ":t"),
documentation = func.docstring or ("Function at line " .. (func.line or "?")),
documentation = func_entry.docstring or ("Function at line " .. (func_entry.line or "?")),
})
end
end
end
if file_index and file_index.classes then
for _, class in ipairs(file_index.classes) do
if class.name and class.name:lower():find(prefix:lower(), 1, true) then
for _, class_entry in ipairs(file_index.classes) do
if class_entry.name and class_entry.name:lower():find(prefix:lower(), 1, true) then
table.insert(items, {
label = class.name,
label = class_entry.name,
kind = 7, -- Class
detail = "[index] " .. vim.fn.fnamemodify(filepath, ":t"),
documentation = class.docstring or ("Class at line " .. (class.line or "?")),
documentation = class_entry.docstring or ("Class at line " .. (class_entry.line or "?")),
})
end
end

View File

@@ -30,8 +30,8 @@ function source:complete(params, callback)
local seen = {}
-- Get brain completions (highest priority)
local ok1, brain_items = pcall(get_brain_completions, prefix)
if ok1 and brain_items then
local brain_completions_success, brain_items = pcall(get_brain_completions, prefix)
if brain_completions_success and brain_items then
for _, item in ipairs(brain_items) do
if not seen[item.label] then
seen[item.label] = true
@@ -42,8 +42,8 @@ function source:complete(params, callback)
end
-- Get indexer completions
local ok2, indexer_items = pcall(get_indexer_completions, prefix)
if ok2 and indexer_items then
local indexer_completions_success, indexer_items = pcall(get_indexer_completions, prefix)
if indexer_completions_success and indexer_items then
for _, item in ipairs(indexer_items) do
if not seen[item.label] then
seen[item.label] = true
@@ -56,8 +56,8 @@ function source:complete(params, callback)
-- Get buffer completions as fallback (lower priority)
local bufnr = params.context.bufnr
if bufnr then
local ok3, buffer_items = pcall(get_buffer_completions, prefix, bufnr)
if ok3 and buffer_items then
local buffer_completions_success, buffer_items = pcall(get_buffer_completions, prefix, bufnr)
if buffer_completions_success and buffer_items then
for _, item in ipairs(buffer_items) do
if not seen[item.label] then
seen[item.label] = true
@@ -69,12 +69,12 @@ function source:complete(params, callback)
end
-- If Copilot is installed, prefer its suggestion as a top-priority completion
local ok_cp, _ = pcall(require, "copilot")
if ok_cp then
local copilot_installed = pcall(require, "copilot")
if copilot_installed then
local suggestion = nil
local ok_sug, res = pcall(get_copilot_suggestion, prefix)
if ok_sug then
suggestion = res
local copilot_suggestion_success, copilot_suggestion_result = pcall(get_copilot_suggestion, prefix)
if copilot_suggestion_success then
suggestion = copilot_suggestion_result
end
if suggestion and suggestion ~= "" then
local first_line = suggestion:match("([^\n]+)") or suggestion
@@ -115,8 +115,8 @@ end
--- Check if source is registered
---@return boolean
function M.is_registered()
local ok, cmp = pcall(require, "cmp")
if not ok then
local cmp_loaded, cmp = pcall(require, "cmp")
if not cmp_loaded then
return false
end

View File

@@ -6,395 +6,9 @@
local M = {}
---@class ContextModalState
---@field buf number|nil Buffer number
---@field win number|nil Window number
---@field original_event table|nil Original prompt event
---@field callback function|nil Callback with additional context
---@field llm_response string|nil LLM's response asking for context
local state = {
buf = nil,
win = nil,
original_event = nil,
callback = nil,
llm_response = nil,
attached_files = nil,
}
--- Close the context modal
function M.close()
if state.win and vim.api.nvim_win_is_valid(state.win) then
vim.api.nvim_win_close(state.win, true)
end
if state.buf and vim.api.nvim_buf_is_valid(state.buf) then
vim.api.nvim_buf_delete(state.buf, { force = true })
end
state.win = nil
state.buf = nil
state.original_event = nil
state.callback = nil
state.llm_response = nil
end
--- Submit the additional context
local function submit()
if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then
return
end
local lines = vim.api.nvim_buf_get_lines(state.buf, 0, -1, false)
local additional_context = table.concat(lines, "\n")
-- Trim whitespace
additional_context = additional_context:match("^%s*(.-)%s*$") or additional_context
if additional_context == "" then
M.close()
return
end
local original_event = state.original_event
local callback = state.callback
M.close()
if callback and original_event then
-- Pass attached_files as third optional parameter
callback(original_event, additional_context, state.attached_files)
end
end
--- Parse requested file paths from LLM response and resolve to full paths
local function parse_requested_files(response)
if not response or response == "" then
return {}
end
local cwd = vim.fn.getcwd()
local candidates = {}
local seen = {}
for path in response:gmatch("`([%w%._%-%/]+%.[%w_]+)`") do
if not seen[path] then
table.insert(candidates, path)
seen[path] = true
end
end
for path in response:gmatch("([%w%._%-%/]+%.[%w_]+)") do
if not seen[path] then
table.insert(candidates, path)
seen[path] = true
end
end
-- Resolve to full paths using cwd and glob
local resolved = {}
for _, p in ipairs(candidates) do
local full = nil
if p:sub(1, 1) == "/" and vim.fn.filereadable(p) == 1 then
full = p
else
local try1 = cwd .. "/" .. p
if vim.fn.filereadable(try1) == 1 then
full = try1
else
local tail = p:match("[^/]+$") or p
local matches = vim.fn.globpath(cwd, "**/" .. tail, false, true)
if matches and #matches > 0 then
full = matches[1]
end
end
end
if full and vim.fn.filereadable(full) == 1 then
table.insert(resolved, full)
end
end
return resolved
end
--- Attach parsed files into the modal buffer and remember them for submission
local function attach_requested_files()
if not state.llm_response or state.llm_response == "" then
return
end
local files = parse_requested_files(state.llm_response)
if #files == 0 then
local ui_prompts = require("codetyper.prompts.agents.modal").ui
vim.api.nvim_buf_set_lines(state.buf, vim.api.nvim_buf_line_count(state.buf), -1, false, ui_prompts.files_header)
return
end
state.attached_files = state.attached_files or {}
for _, full in ipairs(files) do
local ok, lines = pcall(vim.fn.readfile, full)
if ok and lines and #lines > 0 then
table.insert(
state.attached_files,
{ path = vim.fn.fnamemodify(full, ":~:."), full_path = full, content = table.concat(lines, "\n") }
)
local insert_at = vim.api.nvim_buf_line_count(state.buf)
vim.api.nvim_buf_set_lines(state.buf, insert_at, insert_at, false, { "", "-- Attached: " .. full .. " --" })
for i, l in ipairs(lines) do
vim.api.nvim_buf_set_lines(state.buf, insert_at + 1 + i, insert_at + 1 + i, false, { l })
end
else
local insert_at = vim.api.nvim_buf_line_count(state.buf)
vim.api.nvim_buf_set_lines(state.buf, insert_at, insert_at, false, { "", "-- Failed to read: " .. full .. " --" })
end
end
-- Move cursor to end and enter insert mode
vim.api.nvim_win_set_cursor(state.win, { vim.api.nvim_buf_line_count(state.buf), 0 })
vim.cmd("startinsert")
end
--- Open the context modal
---@param original_event table Original prompt event
---@param llm_response string LLM's response asking for context
---@param callback function(event: table, additional_context: string, attached_files?: table)
---@param suggested_commands table[]|nil Optional list of {label,cmd} suggested shell commands
function M.open(original_event, llm_response, callback, suggested_commands)
-- Close any existing modal
M.close()
state.original_event = original_event
state.llm_response = llm_response
state.callback = callback
-- Calculate window size
local width = math.min(80, vim.o.columns - 10)
local height = 10
-- Create buffer
state.buf = vim.api.nvim_create_buf(false, true)
vim.bo[state.buf].buftype = "nofile"
vim.bo[state.buf].bufhidden = "wipe"
vim.bo[state.buf].filetype = "markdown"
-- Create window
local row = math.floor((vim.o.lines - height) / 2)
local col = math.floor((vim.o.columns - width) / 2)
state.win = vim.api.nvim_open_win(state.buf, true, {
relative = "editor",
row = row,
col = col,
width = width,
height = height,
style = "minimal",
border = "rounded",
title = " Additional Context Needed ",
title_pos = "center",
})
-- Set window options
vim.wo[state.win].wrap = true
vim.wo[state.win].cursorline = true
local ui_prompts = require("codetyper.prompts.agents.modal").ui
-- Add header showing what the LLM said
local header_lines = {
ui_prompts.llm_response_header,
}
-- Truncate LLM response for display
local response_preview = llm_response or ""
if #response_preview > 200 then
response_preview = response_preview:sub(1, 200) .. "..."
end
for line in response_preview:gmatch("[^\n]+") do
table.insert(header_lines, "-- " .. line)
end
-- If suggested commands were provided, show them in the header
if suggested_commands and #suggested_commands > 0 then
table.insert(header_lines, "")
table.insert(header_lines, ui_prompts.suggested_commands_header)
for i, s in ipairs(suggested_commands) do
local label = s.label or s.cmd
table.insert(header_lines, string.format("[%d] %s: %s", i, label, s.cmd))
end
table.insert(header_lines, ui_prompts.commands_hint)
end
table.insert(header_lines, "")
table.insert(header_lines, ui_prompts.input_header)
table.insert(header_lines, "")
vim.api.nvim_buf_set_lines(state.buf, 0, -1, false, header_lines)
-- Move cursor to the end
vim.api.nvim_win_set_cursor(state.win, { #header_lines, 0 })
-- Set up keymaps
local opts = { buffer = state.buf, noremap = true, silent = true }
-- Submit with Ctrl+Enter or <leader>s
vim.keymap.set("n", "<C-CR>", submit, opts)
vim.keymap.set("i", "<C-CR>", submit, opts)
vim.keymap.set("n", "<leader>s", submit, opts)
vim.keymap.set("n", "<CR><CR>", submit, opts)
-- Attach parsed files (from LLM response)
vim.keymap.set("n", "a", function()
attach_requested_files()
end, opts)
-- Confirm and submit with 'c' (convenient when doing question round)
vim.keymap.set("n", "c", submit, opts)
-- Quick run of project inspection from modal with <leader>r / <C-r> in insert mode
vim.keymap.set("n", "<leader>r", run_project_inspect, opts)
vim.keymap.set("i", "<C-r>", function()
vim.schedule(run_project_inspect)
end, { buffer = state.buf, noremap = true, silent = true })
-- If suggested commands provided, create per-command keymaps <leader>1..n to run them
state.suggested_commands = suggested_commands
if suggested_commands and #suggested_commands > 0 then
for i, s in ipairs(suggested_commands) do
local key = "<leader>" .. tostring(i)
vim.keymap.set("n", key, function()
-- run this single command and append output
if not s or not s.cmd then
return
end
local ok, out = pcall(vim.fn.systemlist, s.cmd)
local insert_at = vim.api.nvim_buf_line_count(state.buf)
vim.api.nvim_buf_set_lines(state.buf, insert_at, insert_at, false, { "", "-- Output: " .. s.cmd .. " --" })
if ok and out and #out > 0 then
for j, line in ipairs(out) do
vim.api.nvim_buf_set_lines(state.buf, insert_at + j, insert_at + j, false, { line })
end
else
vim.api.nvim_buf_set_lines(
state.buf,
insert_at + 1,
insert_at + 1,
false,
{ "(no output or command failed)" }
)
end
vim.api.nvim_win_set_cursor(state.win, { vim.api.nvim_buf_line_count(state.buf), 0 })
vim.cmd("startinsert")
end, opts)
end
-- Also map <leader>0 to run all suggested commands
vim.keymap.set("n", "<leader>0", function()
for _, s in ipairs(suggested_commands) do
pcall(function()
local ok, out = pcall(vim.fn.systemlist, s.cmd)
local insert_at = vim.api.nvim_buf_line_count(state.buf)
vim.api.nvim_buf_set_lines(state.buf, insert_at, insert_at, false, { "", "-- Output: " .. s.cmd .. " --" })
if ok and out and #out > 0 then
for j, line in ipairs(out) do
vim.api.nvim_buf_set_lines(state.buf, insert_at + j, insert_at + j, false, { line })
end
else
vim.api.nvim_buf_set_lines(
state.buf,
insert_at + 1,
insert_at + 1,
false,
{ "(no output or command failed)" }
)
end
end)
end
vim.api.nvim_win_set_cursor(state.win, { vim.api.nvim_buf_line_count(state.buf), 0 })
vim.cmd("startinsert")
end, opts)
end
-- Close with Esc or q
vim.keymap.set("n", "<Esc>", M.close, opts)
vim.keymap.set("n", "q", M.close, opts)
-- Start in insert mode
vim.cmd("startinsert")
-- Log
pcall(function()
local logs = require("codetyper.adapters.nvim.ui.logs")
logs.add({
type = "info",
message = "Context modal opened - waiting for user input",
})
end)
end
--- Run a small set of safe project inspection commands and insert outputs into the modal buffer
local function run_project_inspect()
if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then
return
end
local cmds = {
{ label = "List files (ls -la)", cmd = "ls -la" },
{ label = "Git status (git status --porcelain)", cmd = "git status --porcelain" },
{ label = "Git top (git rev-parse --show-toplevel)", cmd = "git rev-parse --show-toplevel" },
{ label = "Show repo files (git ls-files)", cmd = "git ls-files" },
}
local ui_prompts = require("codetyper.prompts.agents.modal").ui
local insert_pos = vim.api.nvim_buf_line_count(state.buf)
vim.api.nvim_buf_set_lines(state.buf, insert_pos, insert_pos, false, ui_prompts.project_inspect_header)
for _, c in ipairs(cmds) do
local ok, out = pcall(vim.fn.systemlist, c.cmd)
if ok and out and #out > 0 then
vim.api.nvim_buf_set_lines(state.buf, insert_pos + 2, insert_pos + 2, false, { "-- " .. c.label .. " --" })
for i, line in ipairs(out) do
vim.api.nvim_buf_set_lines(state.buf, insert_pos + 2 + i, insert_pos + 2 + i, false, { line })
end
insert_pos = vim.api.nvim_buf_line_count(state.buf)
else
vim.api.nvim_buf_set_lines(
state.buf,
insert_pos + 2,
insert_pos + 2,
false,
{ "-- " .. c.label .. " --", "(no output or command failed)" }
)
insert_pos = vim.api.nvim_buf_line_count(state.buf)
end
end
-- Move cursor to end
vim.api.nvim_win_set_cursor(state.win, { vim.api.nvim_buf_line_count(state.buf), 0 })
vim.cmd("startinsert")
end
-- Provide a keybinding in the modal to run project inspection commands
pcall(function()
if state.buf and vim.api.nvim_buf_is_valid(state.buf) then
vim.keymap.set("n", "<leader>r", run_project_inspect, { buffer = state.buf, noremap = true, silent = true })
vim.keymap.set("i", "<C-r>", function()
vim.schedule(run_project_inspect)
end, { buffer = state.buf, noremap = true, silent = true })
end
end)
--- Check if modal is open
---@return boolean
function M.is_open()
return state.win ~= nil and vim.api.nvim_win_is_valid(state.win)
end
--- Setup autocmds for the context modal
function M.setup()
local group = vim.api.nvim_create_augroup("CodetypeContextModal", { clear = true })
-- Close context modal when exiting Neovim
vim.api.nvim_create_autocmd("VimLeavePre", {
group = group,
callback = function()
M.close()
end,
desc = "Close context modal before exiting Neovim",
})
end
M.close = require("codetyper.adapters.nvim.ui.context_modal.close")
M.is_open = require("codetyper.adapters.nvim.ui.context_modal.is_open")
M.setup = require("codetyper.adapters.nvim.ui.context_modal.setup")
M.open = require("codetyper.adapters.nvim.ui.context_modal.open")
return M

View File

@@ -0,0 +1,55 @@
local state = require("codetyper.state.state")
local parse_requested_files = require("codetyper.utils.parse_requested_files")
--- Attach parsed files from LLM response into the modal buffer
local function attach_requested_files()
if not state.llm_response or state.llm_response == "" then
return
end
local resolved_files = parse_requested_files(state.llm_response)
if #resolved_files == 0 then
local ui_prompts = require("codetyper.prompts.agents.modal").ui
vim.api.nvim_buf_set_lines(state.buf, vim.api.nvim_buf_line_count(state.buf), -1, false, ui_prompts.files_header)
return
end
state.attached_files = state.attached_files or {}
for _, file_path in ipairs(resolved_files) do
local read_success, file_lines = pcall(vim.fn.readfile, file_path)
if read_success and file_lines and #file_lines > 0 then
table.insert(state.attached_files, {
path = vim.fn.fnamemodify(file_path, ":~:."),
full_path = file_path,
content = table.concat(file_lines, "\n"),
})
local insert_at = vim.api.nvim_buf_line_count(state.buf)
vim.api.nvim_buf_set_lines(state.buf, insert_at, insert_at, false, { "", "-- Attached: " .. file_path .. " --" })
for line_index, line_content in ipairs(file_lines) do
vim.api.nvim_buf_set_lines(
state.buf,
insert_at + 1 + line_index,
insert_at + 1 + line_index,
false,
{ line_content }
)
end
else
local insert_at = vim.api.nvim_buf_line_count(state.buf)
vim.api.nvim_buf_set_lines(
state.buf,
insert_at,
insert_at,
false,
{ "", "-- Failed to read: " .. file_path .. " --" }
)
end
end
vim.api.nvim_win_set_cursor(state.win, { vim.api.nvim_buf_line_count(state.buf), 0 })
vim.cmd("startinsert")
end
return attach_requested_files

View File

@@ -0,0 +1,18 @@
local state = require("codetyper.state.state")
--- Close the context modal and reset state
local function close()
if state.win and vim.api.nvim_win_is_valid(state.win) then
vim.api.nvim_win_close(state.win, true)
end
if state.buf and vim.api.nvim_buf_is_valid(state.buf) then
vim.api.nvim_buf_delete(state.buf, { force = true })
end
state.win = nil
state.buf = nil
state.original_event = nil
state.callback = nil
state.llm_response = nil
end
return close

View File

@@ -0,0 +1,9 @@
local state = require("codetyper.state.state")
--- Check if the context modal is currently open
---@return boolean
local function is_open()
return state.win ~= nil and vim.api.nvim_win_is_valid(state.win)
end
return is_open

View File

@@ -0,0 +1,118 @@
local state = require("codetyper.state.state")
local submit = require("codetyper.adapters.nvim.ui.context_modal.submit")
local attach_requested_files = require("codetyper.adapters.nvim.ui.context_modal.attach_requested_files")
local run_project_inspect = require("codetyper.adapters.nvim.ui.context_modal.run_project_inspect")
local run_suggested_command = require("codetyper.adapters.nvim.ui.context_modal.run_suggested_command")
local run_all_suggested_commands = require("codetyper.adapters.nvim.ui.context_modal.run_all_suggested_commands")
--- Open the context modal
---@param original_event table Original prompt event
---@param llm_response string LLM's response asking for context
---@param callback function(event: table, additional_context: string, attached_files?: table)
---@param suggested_commands table[]|nil Optional list of {label,cmd} suggested shell commands
function M.open(original_event, llm_response, callback, suggested_commands)
close()
state.original_event = original_event
state.llm_response = llm_response
state.callback = callback
local width = math.min(80, vim.o.columns - 10)
local height = 10
state.buf = vim.api.nvim_create_buf(false, true)
vim.bo[state.buf].buftype = "nofile"
vim.bo[state.buf].bufhidden = "wipe"
vim.bo[state.buf].filetype = "markdown"
local row = math.floor((vim.o.lines - height) / 2)
local col = math.floor((vim.o.columns - width) / 2)
state.win = vim.api.nvim_open_win(state.buf, true, {
relative = "editor",
row = row,
col = col,
width = width,
height = height,
style = "minimal",
border = "rounded",
title = " Additional Context Needed ",
title_pos = "center",
})
vim.wo[state.win].wrap = true
vim.wo[state.win].cursorline = true
local ui_prompts = require("codetyper.prompts.agents.modal").ui
local header_lines = {
ui_prompts.llm_response_header,
}
local response_preview = llm_response or ""
if #response_preview > 200 then
response_preview = response_preview:sub(1, 200) .. "..."
end
for line in response_preview:gmatch("[^\n]+") do
table.insert(header_lines, "-- " .. line)
end
if suggested_commands and #suggested_commands > 0 then
table.insert(header_lines, "")
table.insert(header_lines, ui_prompts.suggested_commands_header)
for command_index, command in ipairs(suggested_commands) do
local label = command.label or command.cmd
table.insert(header_lines, string.format("[%d] %s: %s", command_index, label, command.cmd))
end
table.insert(header_lines, ui_prompts.commands_hint)
end
table.insert(header_lines, "")
table.insert(header_lines, ui_prompts.input_header)
table.insert(header_lines, "")
vim.api.nvim_buf_set_lines(state.buf, 0, -1, false, header_lines)
vim.api.nvim_win_set_cursor(state.win, { #header_lines, 0 })
local keymap_opts = { buffer = state.buf, noremap = true, silent = true }
vim.keymap.set("n", "<C-CR>", submit, keymap_opts)
vim.keymap.set("i", "<C-CR>", submit, keymap_opts)
vim.keymap.set("n", "<leader>s", submit, keymap_opts)
vim.keymap.set("n", "<CR><CR>", submit, keymap_opts)
vim.keymap.set("n", "c", submit, keymap_opts)
vim.keymap.set("n", "a", attach_requested_files, keymap_opts)
vim.keymap.set("n", "<leader>r", run_project_inspect, keymap_opts)
vim.keymap.set("i", "<C-r>", function()
vim.schedule(run_project_inspect)
end, keymap_opts)
state.suggested_commands = suggested_commands
if suggested_commands and #suggested_commands > 0 then
for command_index, command in ipairs(suggested_commands) do
local key = "<leader>" .. tostring(command_index)
vim.keymap.set("n", key, function()
run_suggested_command(command)
end, keymap_opts)
end
vim.keymap.set("n", "<leader>0", function()
run_all_suggested_commands(suggested_commands)
end, keymap_opts)
end
vim.keymap.set("n", "<Esc>", close, keymap_opts)
vim.keymap.set("n", "q", close, keymap_opts)
vim.cmd("startinsert")
pcall(function()
local logs = require("codetyper.adapters.nvim.ui.logs")
logs.add({
type = "info",
message = "Context modal opened - waiting for user input",
})
end)
end

View File

@@ -0,0 +1,15 @@
local state = require("codetyper.state.state")
local run_suggested_command = require("codetyper.adapters.nvim.ui.context_modal.run_suggested_command")
--- Run all suggested shell commands and append their outputs to the modal buffer
---@param commands table[] List of {label, cmd} suggested command entries
local function run_all_suggested_commands(commands)
for _, command in ipairs(commands) do
pcall(run_suggested_command, command)
end
vim.api.nvim_win_set_cursor(state.win, { vim.api.nvim_buf_line_count(state.buf), 0 })
vim.cmd("startinsert")
end
return run_all_suggested_commands

View File

@@ -0,0 +1,56 @@
local state = require("codetyper.state.state")
--- Run a small set of safe project inspection commands and insert outputs into the modal buffer
local function run_project_inspect()
if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then
return
end
local inspection_commands = {
{ label = "List files (ls -la)", cmd = "ls -la" },
{ label = "Git status (git status --porcelain)", cmd = "git status --porcelain" },
{ label = "Git top (git rev-parse --show-toplevel)", cmd = "git rev-parse --show-toplevel" },
{ label = "Show repo files (git ls-files)", cmd = "git ls-files" },
}
local ui_prompts = require("codetyper.prompts.agents.modal").ui
local insert_pos = vim.api.nvim_buf_line_count(state.buf)
vim.api.nvim_buf_set_lines(state.buf, insert_pos, insert_pos, false, ui_prompts.project_inspect_header)
for _, command in ipairs(inspection_commands) do
local run_success, output_lines = pcall(vim.fn.systemlist, command.cmd)
if run_success and output_lines and #output_lines > 0 then
vim.api.nvim_buf_set_lines(
state.buf,
insert_pos + 2,
insert_pos + 2,
false,
{ "-- " .. command.label .. " --" }
)
for line_index, line_content in ipairs(output_lines) do
vim.api.nvim_buf_set_lines(
state.buf,
insert_pos + 2 + line_index,
insert_pos + 2 + line_index,
false,
{ line_content }
)
end
insert_pos = vim.api.nvim_buf_line_count(state.buf)
else
vim.api.nvim_buf_set_lines(
state.buf,
insert_pos + 2,
insert_pos + 2,
false,
{ "-- " .. command.label .. " --", "(no output or command failed)" }
)
insert_pos = vim.api.nvim_buf_line_count(state.buf)
end
end
vim.api.nvim_win_set_cursor(state.win, { vim.api.nvim_buf_line_count(state.buf), 0 })
vim.cmd("startinsert")
end
return run_project_inspect

View File

@@ -0,0 +1,38 @@
local state = require("codetyper.state.state")
--- Run a single suggested shell command and append its output to the modal buffer
---@param command table A {label, cmd} suggested command entry
local function run_suggested_command(command)
if not command or not command.cmd then
return
end
local run_success, output_lines = pcall(vim.fn.systemlist, command.cmd)
local insert_at = vim.api.nvim_buf_line_count(state.buf)
vim.api.nvim_buf_set_lines(state.buf, insert_at, insert_at, false, { "", "-- Output: " .. command.cmd .. " --" })
if run_success and output_lines and #output_lines > 0 then
for line_index, line_content in ipairs(output_lines) do
vim.api.nvim_buf_set_lines(
state.buf,
insert_at + line_index,
insert_at + line_index,
false,
{ line_content }
)
end
else
vim.api.nvim_buf_set_lines(
state.buf,
insert_at + 1,
insert_at + 1,
false,
{ "(no output or command failed)" }
)
end
vim.api.nvim_win_set_cursor(state.win, { vim.api.nvim_buf_line_count(state.buf), 0 })
vim.cmd("startinsert")
end
return run_suggested_command

View File

@@ -0,0 +1,16 @@
local close = require("codetyper.adapters.nvim.ui.context_modal.close")
--- Setup autocmds for the context modal
local function setup()
local group = vim.api.nvim_create_augroup("CodetypeContextModal", { clear = true })
vim.api.nvim_create_autocmd("VimLeavePre", {
group = group,
callback = function()
close()
end,
desc = "Close context modal before exiting Neovim",
})
end
return setup

View File

@@ -0,0 +1,31 @@
local state = require("codetyper.state.state")
local close = require("codetyper.adapters.nvim.ui.context_modal.close")
--- Submit the additional context from the modal buffer
local function submit()
if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then
return
end
local lines = vim.api.nvim_buf_get_lines(state.buf, 0, -1, false)
local additional_context = table.concat(lines, "\n")
additional_context = additional_context:match("^%s*(.-)%s*$") or additional_context
if additional_context == "" then
close()
return
end
local original_event = state.original_event
local callback = state.callback
local attached_files = state.attached_files
close()
if callback and original_event then
callback(original_event, additional_context, attached_files)
end
end
return submit

View File

@@ -0,0 +1,10 @@
local state = {
buf = nil,
win = nil,
original_event = nil,
callback = nil,
llm_response = nil,
attached_files = nil,
}
return state

View File

@@ -0,0 +1,51 @@
--- Parse requested file paths from LLM response and resolve to full paths
---@param response string LLM response text
---@return string[] resolved_paths List of resolved absolute file paths
local function parse_requested_files(response)
if not response or response == "" then
return {}
end
local cwd = vim.fn.getcwd()
local candidates = {}
local seen = {}
for path in response:gmatch("`([%w%._%-%/]+%.[%w_]+)`") do
if not seen[path] then
table.insert(candidates, path)
seen[path] = true
end
end
for path in response:gmatch("([%w%._%-%/]+%.[%w_]+)") do
if not seen[path] then
table.insert(candidates, path)
seen[path] = true
end
end
local resolved = {}
for _, candidate_path in ipairs(candidates) do
local full_path = nil
if candidate_path:sub(1, 1) == "/" and vim.fn.filereadable(candidate_path) == 1 then
full_path = candidate_path
else
local relative_path = cwd .. "/" .. candidate_path
if vim.fn.filereadable(relative_path) == 1 then
full_path = relative_path
else
local filename = candidate_path:match("[^/]+$") or candidate_path
local glob_matches = vim.fn.globpath(cwd, "**/" .. filename, false, true)
if glob_matches and #glob_matches > 0 then
full_path = glob_matches[1]
end
end
end
if full_path and vim.fn.filereadable(full_path) == 1 then
table.insert(resolved, full_path)
end
end
return resolved
end
return parse_requested_files