From 433c8c419e97b7703a3155be86cd0f7b5eb5f7a1 Mon Sep 17 00:00:00 2001 From: phanium <91544758+phanen@users.noreply.github.com> Date: Sat, 11 Jul 2026 00:49:42 +0800 Subject: [PATCH] c --- lua/opencode/commands/handlers/session.lua | 7 + lua/opencode/services/client_queue.lua | 147 +++++++++++++++++++++ lua/opencode/ui/footer.lua | 1 + lua/opencode/ui/input_window.lua | 31 ++++- lua/opencode/ui/loading_animation.lua | 6 + 5 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 lua/opencode/services/client_queue.lua diff --git a/lua/opencode/commands/handlers/session.lua b/lua/opencode/commands/handlers/session.lua index f6829212..683bd1eb 100644 --- a/lua/opencode/commands/handlers/session.lua +++ b/lua/opencode/commands/handlers/session.lua @@ -452,6 +452,13 @@ end ---@param message_id? string function M.actions.undo(message_id) return with_active_session('No active session to undo', function(state_obj) + local queue = require('opencode.services.client_queue') + local queued = queue.undo_last() + if queued then + vim.notify(('Removed from client queue: ' .. queued.prompt):sub(vim.v.echospace), vim.log.levels.INFO) + return + end + local target = message_id and find_message_in_state(state_obj, message_id) or find_last_user_message(state_obj) if not target then vim.notify('No user message to undo', vim.log.levels.WARN) diff --git a/lua/opencode/services/client_queue.lua b/lua/opencode/services/client_queue.lua new file mode 100644 index 00000000..568e8161 --- /dev/null +++ b/lua/opencode/services/client_queue.lua @@ -0,0 +1,147 @@ +local state = require('opencode.state') +local loading_animation = require('opencode.ui.loading_animation') +local messaging = require('opencode.services.messaging') + +local M = {} + +---@class OpencodeClientQueueItem +---@field prompt string +---@field opts? SendMessageOpts +---@field enqueued_at integer + +---@type OpencodeClientQueueItem[] +local _queue = {} + +local tool_call_is_running = {} + +local _setup_done = false + +---@return string|nil +local function current_session_id() + local s = state.active_session + return s and s.id or nil +end + +---@param session_id string +---@return boolean +local function is_session_idle(session_id) + local status = loading_animation.get_status(session_id) + return not status or status.type == 'idle' +end + +local function refresh_hint() + local ok, input_window = pcall(require, 'opencode.ui.input_window') + if not ok or not input_window or not input_window.render_queue_hint then + return + end + input_window.render_queue_hint(#_queue) +end + +---Drain one item from the queue, but only when the active session is +---idle. Returns true if an item was drained. +---@return boolean drained_one +function M.flush_one_if_idle() + if #_queue == 0 then + return false + end + local sid = current_session_id() + if not sid then + return false + end + local messages = state.messages or {} + if not is_session_idle(sid) and not tool_call_is_running[sid] then + return false + end + local item = table.remove(_queue, 1) + refresh_hint() + messaging.send_message(item.prompt, item.opts or {}) + return true +end + +---Drain one item unconditionally (no status check). Used by the tool-call +---completion listener, which knows the server's runLoop is about to read +---store for the next step. +---@return boolean drained_one +function M.flush_one_now() + if #_queue == 0 then + return false + end + local sid = current_session_id() + if not sid then + return false + end + local item = table.remove(_queue, 1) + refresh_hint() + messaging.send_message(item.prompt, item.opts or {}) + return true +end + +---Submit a prompt to the active session. +---Drains immediately if the session is idle; otherwise holds in queue and +---lets the tool-call completion listener pick it up. +---@param prompt string +---@param opts? SendMessageOpts +function M.submit(prompt, opts) + if not prompt or prompt == '' then + return + end + M.setup() + table.insert(_queue, { + prompt = prompt, + opts = opts, + enqueued_at = os.time(), + }) + refresh_hint() + while M.flush_one_if_idle() do + end +end + +---@return integer +function M.size() + return #_queue +end + +---@return OpencodeClientQueueItem|nil +function M.undo_last() + if #_queue == 0 then + return nil + end + local item = table.remove(_queue) + refresh_hint() + return item +end + +function M.clear() + _queue = {} + refresh_hint() +end + +---@param properties EventMessagePartUpdated['properties'] +local function on_part_updated(properties) + local part = properties.part + local sid = current_session_id() + if not sid or part.sessionID and part.sessionID ~= sid or part.type ~= 'tool' then + return + end + if part.state.status == 'completed' then + tool_call_is_running[sid] = nil + return + end + tool_call_is_running[sid] = true + while M.flush_one_now() do + end +end + +function M.setup() + if _setup_done then + return + end + _setup_done = true + local mgr = state.event_manager + if not mgr or type(mgr.subscribe) ~= 'function' then + return + end + mgr:subscribe('message.part.updated', on_part_updated) +end + +return M diff --git a/lua/opencode/ui/footer.lua b/lua/opencode/ui/footer.lua index 99c0ead3..1c901177 100644 --- a/lua/opencode/ui/footer.lua +++ b/lua/opencode/ui/footer.lua @@ -167,6 +167,7 @@ function M.setup(windows) }) loading_animation.setup() + require('opencode.services.client_queue').setup() end ---@param preserve_buffer? boolean diff --git a/lua/opencode/ui/input_window.lua b/lua/opencode/ui/input_window.lua index ae96cbb7..d1e4ddfa 100644 --- a/lua/opencode/ui/input_window.lua +++ b/lua/opencode/ui/input_window.lua @@ -153,7 +153,7 @@ function M.handle_submit() return false end - require('opencode.services.messaging').send_message(input_content) + require('opencode.services.client_queue').submit(input_content) return true end @@ -726,4 +726,33 @@ function M.is_hidden() return M._hidden end +local _queue_hint_ns = vim.api.nvim_create_namespace('opencode_queue_hint') +local _queue_hint_extmark_id = nil + +---Render a small "[N queued]" hint at the end of the input line when the +---client queue has pending items. Called by services/client_queue. +---@param size integer +function M.render_queue_hint(size) + local windows = state.windows + if not windows or not windows.input_buf or not vim.api.nvim_buf_is_valid(windows.input_buf) then + return + end + local input_buf = windows.input_buf + local cleared = pcall(vim.api.nvim_buf_clear_namespace, input_buf, _queue_hint_ns, 0, -1) + if not cleared then + return + end + _queue_hint_extmark_id = nil + if not size or size <= 0 then + return + end + local last_line = vim.api.nvim_buf_line_count(input_buf) - 1 + local last_col = #(vim.api.nvim_buf_get_lines(input_buf, last_line, last_line + 1, false)[1] or '') + _queue_hint_extmark_id = vim.api.nvim_buf_set_extmark(input_buf, _queue_hint_ns, last_line, last_col, { + virt_text = { { string.format(' [%d queued]', size), 'Comment' } }, + virt_text_pos = 'eol', + hl_mode = 'combine', + }) +end + return M diff --git a/lua/opencode/ui/loading_animation.lua b/lua/opencode/ui/loading_animation.lua index 3cb6b102..c417112c 100644 --- a/lua/opencode/ui/loading_animation.lua +++ b/lua/opencode/ui/loading_animation.lua @@ -275,6 +275,12 @@ function M.is_running() return M._animation.timer ~= nil end +---@param session_id string +---@return OpencodeSessionStatusInfo['type']|nil +function M.get_status(session_id) + return M._animation.last_status_map[session_id] +end + function M.refresh() if not state.windows then return