Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions lua/opencode/commands/handlers/session.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
147 changes: 147 additions & 0 deletions lua/opencode/services/client_queue.lua
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions lua/opencode/ui/footer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ function M.setup(windows)
})

loading_animation.setup()
require('opencode.services.client_queue').setup()
end

---@param preserve_buffer? boolean
Expand Down
31 changes: 30 additions & 1 deletion lua/opencode/ui/input_window.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
6 changes: 6 additions & 0 deletions lua/opencode/ui/loading_animation.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading