Add chat session

This commit is contained in:
Jonas Widen 2025-03-16 14:44:36 +01:00
parent 514a1e08ac
commit 2aeaed13b8
3 changed files with 132 additions and 56 deletions

View File

@ -63,6 +63,26 @@ The AI response appears in a floating window. You can close it using:
- `<Enter>` key - `<Enter>` key
- `:q` command - `:q` command
## Chat Features
The plugin now maintains a continuous chat session with Gemini:
- All conversations appear in a persistent chat window
- Chat history is maintained throughout the session
- Press `i` in the chat window to enter a new query
- Press `q` to close the chat window (history is preserved)
- Use `:GeminiClearChat` to clear the conversation history
### Chat Window Controls
While in the chat window:
- `i`: Enter a new query
- `q`: Close the window
- Normal mode scrolling commands work as expected
- Window automatically scrolls to show new messages
The chat window appears on the right side of your editor and preserves the entire conversation history until you explicitly clear it or restart Neovim.
## Features ## Features
- Floating window interface - Floating window interface

View File

@ -2,6 +2,9 @@
local M = {} local M = {}
-- Store conversation history
local conversation_history = {}
local function get_api_key() local function get_api_key()
-- Check for environment variable -- Check for environment variable
local api_key = os.getenv("GEMINI_API_KEY") local api_key = os.getenv("GEMINI_API_KEY")
@ -30,20 +33,29 @@ local function make_request(prompt, context)
end end
local model = "gemini-2.0-flash" local model = "gemini-2.0-flash"
local contents = { local contents = {}
{
parts = { -- Add conversation history to the request
{ for _, message in ipairs(conversation_history) do
text = prompt, table.insert(contents, {
}, parts = {{
}, text = message.role .. ": " .. message.content
}, }}
} })
end
-- If context is provided, add it to the contents -- Add the current prompt
if context then if context then
table.insert(contents[1].parts, 1, { table.insert(contents, {
text = "Context:\n" .. context .. "\n\nQuery:\n", parts = {{
text = "Context:\n" .. context .. "\n\nUser: " .. prompt
}}
})
else
table.insert(contents, {
parts = {{
text = "User: " .. prompt
}}
}) })
end end
@ -82,6 +94,12 @@ local function make_request(prompt, context)
end end
function M.get_response(prompt, context) function M.get_response(prompt, context)
-- Add user message to history
table.insert(conversation_history, {
role = "User",
content = prompt
})
local result = make_request(prompt, context) local result = make_request(prompt, context)
if result then if result then
@ -98,7 +116,13 @@ function M.get_response(prompt, context)
and result.candidates[1].content.parts[1] and result.candidates[1].content.parts[1]
and result.candidates[1].content.parts[1].text and result.candidates[1].content.parts[1].text
then then
return result.candidates[1].content.parts[1].text local response_text = result.candidates[1].content.parts[1].text
-- Add assistant response to history
table.insert(conversation_history, {
role = "Assistant",
content = response_text
})
return response_text
end end
vim.notify("Unexpected response structure: " .. vim.inspect(result), vim.log.levels.ERROR) vim.notify("Unexpected response structure: " .. vim.inspect(result), vim.log.levels.ERROR)
@ -108,4 +132,9 @@ function M.get_response(prompt, context)
return nil return nil
end end
-- Add function to clear conversation history
function M.clear_conversation()
conversation_history = {}
end
return M return M

View File

@ -3,6 +3,10 @@
local api = require("gemini.api") local api = require("gemini.api")
local M = {} local M = {}
-- Store the buffer number of the chat window
local chat_bufnr = nil
local chat_winnr = nil
local function get_current_buffer_content() local function get_current_buffer_content()
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
return table.concat(lines, "\n") return table.concat(lines, "\n")
@ -18,65 +22,77 @@ local function setup_treesitter_highlight(bufnr)
end end
end end
local function gemini_query(prompt, context) local function update_chat_window(new_content)
local response = api.get_response(prompt, context) if not chat_bufnr or not vim.api.nvim_buf_is_valid(chat_bufnr) then
-- Create a new buffer for chat
chat_bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_option(chat_bufnr, 'buftype', 'nofile')
vim.api.nvim_buf_set_option(chat_bufnr, 'filetype', 'markdown')
end
if response then -- Calculate dimensions
-- Create a scratch buffer local width = math.floor(vim.o.columns / 3)
local new_buf = vim.api.nvim_create_buf(false, true) local height = vim.o.lines - 2
vim.api.nvim_buf_set_lines(new_buf, 0, 0, false, vim.split(response, "\n"))
if not chat_winnr or not vim.api.nvim_win_is_valid(chat_winnr) then
-- Set buffer options
vim.api.nvim_buf_set_option(new_buf, 'modifiable', false)
vim.api.nvim_buf_set_option(new_buf, 'buftype', 'nofile')
vim.api.nvim_buf_set_option(new_buf, 'filetype', 'markdown')
-- Calculate dimensions
local width = math.floor(vim.o.columns / 3)
local height = vim.o.lines - 2 -- Account for status line and command line
-- Create the window -- Create the window
local new_win = vim.api.nvim_open_win(new_buf, true, { chat_winnr = vim.api.nvim_open_win(chat_bufnr, true, {
relative = "editor", relative = "editor",
width = width, width = width,
height = height, height = height,
row = 0, row = 0,
col = vim.o.columns - width, col = vim.o.columns - width,
border = "rounded", border = "rounded",
title = "Google AI Response", title = "Gemini Chat Session",
title_pos = "center", title_pos = "center",
style = "minimal" style = "minimal"
}) })
-- Set window-local options -- Set window-local options
vim.api.nvim_win_set_option(new_win, 'wrap', true) vim.api.nvim_win_set_option(chat_winnr, 'wrap', true)
vim.api.nvim_win_set_option(new_win, 'linebreak', true) vim.api.nvim_win_set_option(chat_winnr, 'linebreak', true)
vim.api.nvim_win_set_option(new_win, 'breakindent', true) vim.api.nvim_win_set_option(chat_winnr, 'breakindent', true)
-- Setup treesitter highlighting -- Setup treesitter highlighting
setup_treesitter_highlight(new_buf) setup_treesitter_highlight(chat_bufnr)
-- Set window-local keymaps -- Set window-local keymaps
local close_keys = {'q', '<Esc>', '<CR>'} vim.keymap.set('n', 'q', function()
for _, key in ipairs(close_keys) do vim.api.nvim_win_close(chat_winnr, true)
vim.keymap.set('n', key, function() chat_winnr = nil
vim.api.nvim_win_close(new_win, true) end, { buffer = chat_bufnr, nowait = true })
end, { buffer = new_buf, nowait = true })
end
-- Add autocmd to enable closing with :q -- Add input mapping
vim.api.nvim_create_autocmd("BufWinLeave", { vim.keymap.set('n', 'i', function()
buffer = new_buf, vim.ui.input({ prompt = "Gemini: " }, function(input)
callback = function() if input then
if vim.api.nvim_win_is_valid(new_win) then M.gemini_query(input)
vim.api.nvim_win_close(new_win, true)
end end
end, end)
once = true, end, { buffer = chat_bufnr, nowait = true })
}) end
-- Return focus to the previous window -- Make buffer modifiable
vim.cmd('wincmd p') vim.api.nvim_buf_set_option(chat_bufnr, 'modifiable', true)
-- Update content
vim.api.nvim_buf_set_lines(chat_bufnr, -1, -1, false, vim.split(new_content, "\n"))
-- Make buffer unmodifiable again
vim.api.nvim_buf_set_option(chat_bufnr, 'modifiable', false)
-- Scroll to bottom
vim.api.nvim_win_set_cursor(chat_winnr, {vim.api.nvim_buf_line_count(chat_bufnr), 0})
-- Return focus to the previous window
vim.cmd('wincmd p')
end
local function gemini_query(prompt, context)
local response = api.get_response(prompt, context)
if response then
local formatted_content = "\n\nUser: " .. prompt .. "\n\nAssistant: " .. response
update_chat_window(formatted_content)
else else
vim.notify("Failed to get a response from Gemini API", vim.log.levels.ERROR) vim.notify("Failed to get a response from Gemini API", vim.log.levels.ERROR)
end end
@ -112,24 +128,35 @@ function M.setup()
complete = "shellcmd", complete = "shellcmd",
}) })
-- Add command to clear chat history
vim.api.nvim_create_user_command("GeminiClearChat", function()
api.clear_conversation()
if chat_bufnr and vim.api.nvim_buf_is_valid(chat_bufnr) then
vim.api.nvim_buf_set_lines(chat_bufnr, 0, -1, false, {})
end
vim.notify("Chat history cleared", vim.log.levels.INFO)
end, {
desc = "Clear Gemini chat history"
})
-- Set up keymapping with 'gc' for 'gemini chat' -- Set up keymapping with 'gc' for 'gemini chat'
vim.keymap.set("n", "<leader>gc", function() vim.keymap.set("n", "<leader>gc", function()
vim.ui.input({ prompt = "Gemini Query: " }, function(input) vim.ui.input({ prompt = "Gemini: " }, function(input)
if input then if input then
M.gemini_query(input) M.gemini_query(input)
end end
end) end)
end, { desc = "Query Google AI (via Input)" }) end, { desc = "Chat with Gemini AI" })
-- Set up keymapping with 'gs' for 'gemini sync' -- Set up keymapping with 'gs' for 'gemini sync'
vim.keymap.set("n", "<leader>gs", function() vim.keymap.set("n", "<leader>gs", function()
vim.ui.input({ prompt = "Gemini Query (with buffer context): " }, function(input) vim.ui.input({ prompt = "Gemini (with buffer context): " }, function(input)
if input then if input then
local buffer_content = get_current_buffer_content() local buffer_content = get_current_buffer_content()
M.gemini_query(input, buffer_content) M.gemini_query(input, buffer_content)
end end
end) end)
end, { desc = "Query Google AI (with buffer context)" }) end, { desc = "Chat with Gemini AI (with buffer context)" })
end end
return M return M