diff --git a/README.md b/README.md index a0bbe04..3e18b86 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ the project. Set `model` when the OpenCode server default is not usable. - Select code and run `:'<,'>Brokencode` to send that code as the prompt. - `:BrokencodeMessage` opens a message dialog. Press `Ctrl-S` to send. - `:BrokencodeChat` opens the current conversation in a virtual buffer. +- `:BrokencodeClear` creates a new session and clears the conversation. - `:BrokencodeNew` clears the project session selection. The session remains on the OpenCode server. The plugin stores its session ID diff --git a/lua/brokencode/init.lua b/lua/brokencode/init.lua index 972e907..7605083 100644 --- a/lua/brokencode/init.lua +++ b/lua/brokencode/init.lua @@ -40,6 +40,23 @@ local function resolve_model(cwd, current_id, callback) end) end +local function create_session(cwd, model, callback) + client.create(cwd, model, function(err, session) + if err then + vim.notify(err, vim.log.levels.ERROR) + return + end + + if not session or not session.id then + vim.notify("OpenCode returned an invalid session", vim.log.levels.ERROR) + return + end + + state.set(cwd, session.id) + callback(session.id) + end) +end + local function ensure_session(cwd, callback) local session_id = state.get(cwd) if session_id then @@ -61,20 +78,7 @@ local function ensure_session(cwd, callback) end resolve_model(cwd, nil, function(model) - client.create(cwd, model, function(err, session) - if err then - vim.notify(err, vim.log.levels.ERROR) - return - end - - if not session or not session.id then - vim.notify("OpenCode returned an invalid session", vim.log.levels.ERROR) - return - end - - state.set(cwd, session.id) - callback(session.id) - end) + create_session(cwd, model, callback) end) end @@ -83,22 +87,30 @@ local function poll(session_id, attempts) client.active(function(err, active) if err then vim.notify("Brokencode status check failed: " .. err, vim.log.levels.WARN) + ui.set_running(session_id, false) ui.refresh(session_id) return end - ui.refresh(session_id) if active and active[session_id] and attempts > 0 then + ui.refresh(session_id) poll(session_id, attempts - 1) + return end + + + ui.set_running(session_id, false) + ui.refresh(session_id) end) end, 1000) end local function submit(text, retry, cwd) ensure_session(cwd, function(session_id) + ui.set_running(session_id, true) client.prompt(session_id, text, function(err) if err then + ui.set_running(session_id, false) if retry and err:find("Session not found", 1, true) then state.clear(cwd) submit(text, false, cwd) @@ -126,6 +138,7 @@ end function M.chat() ensure_session(directory(), function(session_id) + ui.render({}, session_id) ui.refresh(session_id) end) end @@ -150,6 +163,16 @@ local function command(opts) ui.input(M.send) end +local function clear() + local cwd = directory() + resolve_model(cwd, nil, function(model) + create_session(cwd, model, function(session_id) + ui.render({}, session_id) + vim.notify("Brokencode session created") + end) + end) +end + function M.setup(opts) config.setup(opts) if loaded then @@ -163,6 +186,7 @@ function M.setup(opts) end vim.api.nvim_create_user_command("Brokencode", command, { nargs = "*", range = true }) vim.api.nvim_create_user_command("BrokencodeChat", M.chat, {}) + vim.api.nvim_create_user_command("BrokencodeClear", clear, {}) vim.api.nvim_create_user_command("BrokencodeMessage", function() ui.input(M.send) end, {}) vim.api.nvim_create_user_command("BrokencodeNew", function() state.clear(directory()) diff --git a/lua/brokencode/ui.lua b/lua/brokencode/ui.lua index 6f89afc..11e0363 100644 --- a/lua/brokencode/ui.lua +++ b/lua/brokencode/ui.lua @@ -1,7 +1,13 @@ local client = require("brokencode.client") local config = require("brokencode.config") -local M = { chat_buf = nil, chat_win = nil } +local M = { + chat_buf = nil, + chat_win = nil, + current_session = nil, + messages = {}, + running = {}, +} -- check this file for bugs local function valid(value, check) @@ -12,9 +18,13 @@ local function append_text(lines, text) vim.list_extend(lines, vim.split(text or "", "\n", { plain = true })) end -local function message_lines(messages) +local function message_lines(messages, running) local lines = { "# OpenCode", "" } + if running then + vim.list_extend(lines, { "> **Request running...**", "" }) + end + for _, message in ipairs(messages or {}) do if message.type == "user" then vim.list_extend(lines, { "## You", "" }) @@ -66,14 +76,29 @@ function M.open() end end -function M.render(messages) +function M.render(messages, session_id) + if session_id then + M.current_session = session_id + end + M.messages = messages or {} + M.open() vim.bo[M.chat_buf].modifiable = true - vim.api.nvim_buf_set_lines(M.chat_buf, 0, -1, false, message_lines(messages)) + local running = M.current_session and M.running[M.current_session] + vim.api.nvim_buf_set_lines(M.chat_buf, 0, -1, false, message_lines(M.messages, running)) vim.bo[M.chat_buf].modifiable = false vim.api.nvim_win_set_cursor(M.chat_win, { vim.api.nvim_buf_line_count(M.chat_buf), 0 }) end +function M.set_running(session_id, running) + if M.current_session and M.current_session ~= session_id then + M.messages = {} + end + M.current_session = session_id + M.running[session_id] = running or nil + M.render(M.messages, session_id) +end + function M.refresh(session_id, callback) client.messages(session_id, function(err, messages) if err then @@ -84,7 +109,7 @@ function M.refresh(session_id, callback) return end - M.render(messages) + M.render(messages, session_id) if callback then callback(nil, messages) end diff --git a/tests/session_spec.lua b/tests/session_spec.lua index 68882e5..d260fd0 100644 --- a/tests/session_spec.lua +++ b/tests/session_spec.lua @@ -2,6 +2,7 @@ local current = "ses_old" local created = 0 local prompted = 0 local inherited +local rendered package.loaded["brokencode.client"] = { sessions = function(_, callback) @@ -12,7 +13,7 @@ package.loaded["brokencode.client"] = { create = function(_, model, callback) created = created + 1 inherited = model - callback(nil, { id = "ses_new" }) + callback(nil, { id = created == 1 and "ses_new" or "ses_clear" }) end, prompt = function(session_id, _, callback) prompted = prompted + 1 @@ -33,11 +34,23 @@ package.loaded["brokencode.state"] = { package.loaded["brokencode.ui"] = { refresh = function() end, input = function() end, + set_running = function() end, + render = function(messages, session_id) rendered = { messages = messages, session_id = session_id } end, } +package.loaded["brokencode.bridge"] = { start = function() end } -require("brokencode").send("hello") +local brokencode = require("brokencode") +brokencode.send("hello") assert(created == 1, "a replacement session was not created") assert(prompted == 2, "the prompt was not retried once") assert(current == "ses_new", "the replacement session was not saved") assert(inherited and inherited.id == "working", "the recent model was not inherited") + +brokencode.setup() +vim.cmd("BrokencodeClear") + +assert(created == 2, "BrokencodeClear did not create a session") +assert(current == "ses_clear", "BrokencodeClear did not save the new session") +assert(rendered and rendered.session_id == "ses_clear", "BrokencodeClear did not select the new session") +assert(vim.tbl_isempty(rendered.messages), "BrokencodeClear did not clear the conversation") diff --git a/tests/ui_spec.lua b/tests/ui_spec.lua index 3b856bb..8ca7698 100644 --- a/tests/ui_spec.lua +++ b/tests/ui_spec.lua @@ -1,9 +1,49 @@ local ui = require("brokencode.ui") +local client = require("brokencode.client") + +local source_win = vim.api.nvim_get_current_win() ui.render({ { type = "user", text = "first line\nsecond line" }, }) +assert(vim.api.nvim_get_current_win() == source_win, "render changed the current window") + local lines = vim.api.nvim_buf_get_lines(ui.chat_buf, 0, -1, false) assert(vim.tbl_contains(lines, "first line"), vim.inspect(lines)) assert(vim.tbl_contains(lines, "second line"), vim.inspect(lines)) + +local replacement = vim.api.nvim_create_buf(false, true) +vim.api.nvim_win_set_buf(ui.chat_win, replacement) +ui.render({ { type = "user", text = "restored" } }) +assert(vim.api.nvim_win_get_buf(ui.chat_win) == ui.chat_buf, "chat buffer was not restored") + +ui.set_running("ses_test", true) +lines = vim.api.nvim_buf_get_lines(ui.chat_buf, 0, -1, false) +assert(vim.tbl_contains(lines, "> **Request running...**"), vim.inspect(lines)) + +ui.set_running("ses_test", false) +lines = vim.api.nvim_buf_get_lines(ui.chat_buf, 0, -1, false) +assert(not vim.tbl_contains(lines, "> **Request running...**"), vim.inspect(lines)) + +local callbacks = {} +local messages = client.messages +client.messages = function(_, callback) + table.insert(callbacks, callback) +end + +ui.refresh("ses_test") +ui.refresh("ses_test") +callbacks[2](nil, { { type = "user", text = "new response" } }) +callbacks[1](nil, { { type = "user", text = "stale response" } }) +lines = vim.api.nvim_buf_get_lines(ui.chat_buf, 0, -1, false) +assert(vim.tbl_contains(lines, "new response"), vim.inspect(lines)) +assert(not vim.tbl_contains(lines, "stale response"), vim.inspect(lines)) + +ui.set_running("ses_new", true) +ui.set_running("ses_old", false) +ui.refresh("ses_old") +callbacks[3](nil, { { type = "user", text = "old session" } }) +assert(ui.current_session == "ses_new", "old polling replaced the active session") + +client.messages = messages