local M = {} local progress_win = nil local progress_buf = nil local job_id = nil function M.show_progress(msg) if progress_win and vim.api.nvim_win_is_valid(progress_win) then local lines = vim.split(msg, "\n") vim.api.nvim_buf_set_lines(progress_buf, -1, -1, false, lines) local line_count = vim.api.nvim_buf_line_count(progress_buf) vim.api.nvim_win_set_cursor(progress_win, {line_count, 0}) return end progress_buf = vim.api.nvim_create_buf(false, true) local lines = vim.split(msg, "\n") vim.api.nvim_buf_set_lines(progress_buf, 0, -1, false, lines) local width = math.min(math.floor(vim.o.columns * 0.4), 80) local height = math.min(math.floor(vim.o.lines * 0.4), 20) local row = 1 local col = vim.o.columns - width - 2 progress_win = vim.api.nvim_open_win(progress_buf, true, { relative = 'editor', width = width, height = height, row = row, col = col, style = 'minimal', border = 'rounded', title = ' Antigravity AI ', title_pos = 'center' }) local function close_win() if job_id then vim.fn.jobstop(job_id) job_id = nil end if progress_win and vim.api.nvim_win_is_valid(progress_win) then vim.api.nvim_win_close(progress_win, true) progress_win = nil end end vim.keymap.set('n', 'q', close_win, { buffer = progress_buf, noremap = true, silent = true }) vim.keymap.set('n', '', close_win, { buffer = progress_buf, noremap = true, silent = true }) end function M.close_progress() if progress_win and vim.api.nvim_win_is_valid(progress_win) then vim.api.nvim_win_close(progress_win, true) progress_win = nil end end local function send_udp(payload) local uv = vim.uv or vim.loop local client = uv.new_udp() if client then local ok, data = pcall(vim.fn.json_encode, payload) if ok then client:send(data, "127.0.0.1", 3002, function(err) client:close() end) else client:close() end end end function M.prompt_user(prompt_text, options, request_id) if options and #options > 0 then vim.ui.select(options, { prompt = prompt_text }, function(choice, idx) send_udp({ event = "agent_prompt_response", request_id = request_id, response = choice, index = idx, session_id = vim.v.servername }) end) else vim.ui.input({ prompt = prompt_text }, function(input) send_udp({ event = "agent_prompt_response", request_id = request_id, response = input, session_id = vim.v.servername }) end) end end local ns_id = vim.api.nvim_create_namespace("gemini_ghost_text") local knowledge_ns = vim.api.nvim_create_namespace("gemini_knowledge_projection") local active_diffs = {} local active_knowledge = {} function M.project_knowledge(tech_debts, adrs, bufnr) bufnr = (bufnr == 0 or bufnr == nil) and vim.api.nvim_get_current_buf() or bufnr if not vim.api.nvim_buf_is_valid(bufnr) then return end vim.api.nvim_buf_clear_namespace(bufnr, knowledge_ns, 0, -1) active_knowledge[bufnr] = { tech_debts = tech_debts or {}, adrs = adrs or {}, file = vim.api.nvim_buf_get_name(bufnr) } local line_count = vim.api.nvim_buf_line_count(bufnr) -- 1. Project Tech Debts if tech_debts and #tech_debts > 0 then for _, debt in ipairs(tech_debts) do local target_line = 0 if debt.line_range then local num = tonumber(string.match(debt.line_range, "%d+")) if num and num > 0 then target_line = num - 1 end end target_line = math.max(0, math.min(target_line, line_count - 1)) local sev = debt.severity or "debt" local hl = (sev == "critical" or sev == "high") and "DiagnosticError" or "DiagnosticWarn" vim.api.nvim_buf_set_extmark(bufnr, knowledge_ns, target_line, 0, { virt_text = {{" ⚡ [" .. sev:upper() .. " DEBT] " .. debt.description, hl}}, virt_text_pos = "eol", hl_mode = "combine", }) end end -- 2. Project ADRs if adrs and #adrs > 0 then for _, adr in ipairs(adrs) do vim.api.nvim_buf_set_extmark(bufnr, knowledge_ns, 0, 0, { virt_text = {{" 📜 [" .. adr.id .. "] " .. adr.title, "DiagnosticInfo"}}, virt_text_pos = "eol", hl_mode = "combine", }) end end end function M.show_knowledge() local bufnr = vim.api.nvim_get_current_buf() local data = active_knowledge[bufnr] if not data or ((#data.tech_debts == 0) and (#data.adrs == 0)) then print("No active Tech Debt or ADRs found for this file.") return end local lines = {} table.insert(lines, "# Antigravity Knowledge Graph Projection") table.insert(lines, "File: " .. (data.file or vim.api.nvim_buf_get_name(bufnr))) table.insert(lines, string.rep("=", 50)) table.insert(lines, "") if #data.tech_debts > 0 then table.insert(lines, "## Active Tech Debts (" .. #data.tech_debts .. ")") for _, d in ipairs(data.tech_debts) do table.insert(lines, string.format("- [%s] (%s): %s", d.id, d.severity or "unspecified", d.description)) if d.line_range then table.insert(lines, " Lines: " .. d.line_range) end if d.ideal_solution and d.ideal_solution ~= "" then table.insert(lines, " Ideal Solution: " .. d.ideal_solution) end end table.insert(lines, "") end if #data.adrs > 0 then table.insert(lines, "## Architectural Decision Records (ADRs) (" .. #data.adrs .. ")") for _, a in ipairs(data.adrs) do table.insert(lines, string.format("- [%s] %s (Status: %s)", a.id, a.title, a.status)) table.insert(lines, " Decision: " .. a.decision) end table.insert(lines, "") end local kbuf = vim.api.nvim_create_buf(false, true) vim.api.nvim_buf_set_lines(kbuf, 0, -1, false, lines) vim.bo[kbuf].filetype = "markdown" local width = math.min(math.floor(vim.o.columns * 0.7), 90) local height = math.min(#lines + 2, math.floor(vim.o.lines * 0.6)) local row = math.floor((vim.o.lines - height) / 2) local col = math.floor((vim.o.columns - width) / 2) local kwin = vim.api.nvim_open_win(kbuf, true, { relative = 'editor', width = width, height = height, row = row, col = col, style = 'minimal', border = 'rounded', title = ' Antigravity Knowledge Projection ', title_pos = 'center' }) local function close_kwin() if kwin and vim.api.nvim_win_is_valid(kwin) then vim.api.nvim_win_close(kwin, true) end end vim.keymap.set('n', 'q', close_kwin, { buffer = kbuf, noremap = true, silent = true }) vim.keymap.set('n', '', close_kwin, { buffer = kbuf, noremap = true, silent = true }) end function M.preview_diff(diff_id, bufnr, start_row, end_row, new_lines) bufnr = (bufnr == 0 or bufnr == nil) and vim.api.nvim_get_current_buf() or bufnr local virt_lines = {} for _, line in ipairs(new_lines) do table.insert(virt_lines, {{line, "DiffAdd"}}) end local extmark_id = vim.api.nvim_buf_set_extmark(bufnr, ns_id, start_row, 0, { virt_lines = virt_lines, virt_lines_above = true, }) active_diffs[diff_id] = { bufnr = bufnr, start_row = start_row, end_row = end_row, new_lines = new_lines, extmark_id = extmark_id } if pcall(require, "notify") then require("notify")("Ghost text diff added. Use aa to accept or ar to reject.", "info", { title = "Antigravity" }) else print("Ghost text diff added. Use aa to accept or ar to reject.") end end function M.accept_diff() local diff_id, diff = next(active_diffs) if not diff then print("No active diffs to accept.") return end vim.api.nvim_buf_set_lines(diff.bufnr, diff.start_row, diff.end_row, false, diff.new_lines) vim.api.nvim_buf_del_extmark(diff.bufnr, ns_id, diff.extmark_id) active_diffs[diff_id] = nil send_udp({ event = "agent_diff_accepted", diff_id = diff_id, session_id = vim.v.servername }) print("Diff accepted.") end function M.reject_diff() local diff_id, diff = next(active_diffs) if not diff then print("No active diffs to reject.") return end vim.api.nvim_buf_del_extmark(diff.bufnr, ns_id, diff.extmark_id) active_diffs[diff_id] = nil send_udp({ event = "agent_diff_rejected", diff_id = diff_id, session_id = vim.v.servername }) print("Diff rejected.") end function M.ask_agent() local file = vim.api.nvim_buf_get_name(0) vim.ui.input({ prompt = 'Ask Agent: ' }, function(input) if not input or input == '' then return end local cursor = vim.api.nvim_win_get_cursor(0) send_udp({ event = "agent_ask", prompt = input, file = file, line = cursor[1], col = cursor[2], session_id = vim.v.servername }) M.show_progress("Sent to Antigravity via UDP...") vim.defer_fn(M.close_progress, 1500) end) end function M.setup() vim.keymap.set('n', 'ai', M.ask_agent, { desc = "Ask Antigravity Agent", noremap = true, silent = true }) vim.keymap.set('v', 'ai', function() local _, csrow, cscol, _ = unpack(vim.fn.getpos("'<")) local _, cerow, cecol, _ = unpack(vim.fn.getpos("'>")) vim.ui.input({ prompt = 'Ask Agent (about selection): ' }, function(input) if not input or input == '' then return end local file = vim.api.nvim_buf_get_name(0) send_udp({ event = "agent_ask_selection", prompt = input, file = file, start_row = csrow, end_row = cerow, session_id = vim.v.servername }) M.show_progress("Sent selection to Antigravity via UDP...") vim.defer_fn(M.close_progress, 1500) end) end, { desc = "Ask Antigravity Agent about selection", noremap = true, silent = true }) vim.keymap.set('n', 'aa', M.accept_diff, { desc = "Accept Agent Diff", noremap = true, silent = true }) vim.keymap.set('n', 'ar', M.reject_diff, { desc = "Reject Agent Diff", noremap = true, silent = true }) vim.keymap.set('n', 'ak', M.show_knowledge, { desc = "Show Knowledge (Tech Debt & ADRs)", noremap = true, silent = true }) end function M.send_to_terminal(command) local term_buf = nil for _, buf in ipairs(vim.api.nvim_list_bufs()) do if vim.api.nvim_buf_is_loaded(buf) and vim.bo[buf].buftype == "terminal" then term_buf = buf break end end if not term_buf then vim.cmd("split | term") term_buf = vim.api.nvim_get_current_buf() else local win_found = false for _, win in ipairs(vim.api.nvim_list_wins()) do if vim.api.nvim_win_get_buf(win) == term_buf then vim.api.nvim_set_current_win(win) win_found = true break end end if not win_found then vim.cmd("split") vim.api.nvim_win_set_buf(0, term_buf) end end local job_id = vim.b[term_buf].terminal_job_id if job_id then vim.api.nvim_chan_send(job_id, command .. "\r\n") vim.api.nvim_win_set_cursor(0, {vim.api.nvim_buf_line_count(term_buf), 0}) end end -- ============================================================================ -- Antigravity Statusline Integration (Push-driven / Zero-polling) -- ============================================================================ local statusline_cache = { text = "" } function M.update_statusline(active_task, active_gate) local parts = {} if active_task and active_task ~= vim.NIL and active_task ~= "" then local title = tostring(active_task) if #title > 24 then title = title:sub(1, 22) .. ".." end table.insert(parts, "📋 " .. title) end if active_gate and active_gate ~= vim.NIL and active_gate ~= "" then local status = tostring(active_gate):lower() if status == "authorized" then table.insert(parts, "🟢 PUSH:OK") elseif status == "blocked" then table.insert(parts, "🔴 PUSH:BLOCKED") else table.insert(parts, "🟡 PUSH:WAIT") end end if #parts > 0 then statusline_cache.text = " [AGY: " .. table.concat(parts, " | ") .. "] " else statusline_cache.text = "" end vim.schedule(function() pcall(vim.cmd, "redrawstatus") end) end function M.statusline() return statusline_cache.text end return M