Skip to content
Merged
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/api_client.lua
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,13 @@ function OpencodeApiClient:list_sessions(directory)
return self:_call('/session', 'GET', nil, { directory = directory })
end

--- List the current status of all sessions in a workspace.
--- @param directory string|nil Directory path
--- @return Promise<{[string]: OpencodeSessionStatusInfo}>
function OpencodeApiClient:list_session_status(directory)
return self:_call('/session/status', 'GET', nil, { directory = directory })
end

--- List sessions across all projects (experimental global endpoint).
--- Bypasses _call's automatic directory injection so the server returns all
--- directories instead of being filtered to the current cwd.
Expand Down
7 changes: 7 additions & 0 deletions lua/opencode/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -775,3 +775,10 @@
---@class OpencodeSelectionRange
---@field start number Starting line number (inclusive)
---@field stop number Ending line number (inclusive)

---@class OpencodeSessionStatusInfo
---@field type 'idle'|'busy'|'retry' Current status of the session
---@field message? string Human-readable detail (populated for `retry`)
---@field attempt? number Retry attempt counter (populated for `retry`)
---@field next? number Server-side timestamp of the next retry (populated for `retry`)
---@field action? table Optional retry action metadata (populated for `retry`)
109 changes: 90 additions & 19 deletions lua/opencode/ui/loading_animation.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ M._animation = {
frames = nil,
text = 'Thinking... ',
status_data = nil,
status_session_id = nil,
current_frame = 1,
timer = nil,
fps = 10,
extmark_id = nil,
ns_id = vim.api.nvim_create_namespace('opencode_loading_animation'),
status_event_manager = nil,
last_status_map = {},
}

---@param status table|nil
Expand Down Expand Up @@ -90,20 +92,45 @@ function M.on_session_status(properties)
return
end

local active_session = state.active_session
if active_session and active_session.id and properties.sessionID ~= active_session.id then
if not properties.sessionID or not properties.status then
return
end

M._animation.status_data = properties.status
M._animation.last_status_map[properties.sessionID] = properties.status

local active_session = state.active_session
if active_session and active_session.id == properties.sessionID then
M._animation.status_data = properties.status
M._animation.status_session_id = properties.sessionID
M.refresh()
end
M.render(state.windows)
end

local function on_active_session_change(_, new_session, old_session)
local function replay_status_for(session_id)
local status = M._animation.last_status_map[session_id]
if not status then
return
end
local active_session = state.active_session
if not active_session or active_session.id ~= session_id then
return
end
M._animation.status_data = status
M._animation.status_session_id = session_id
M.refresh()
M.render(state.windows)
end

M._on_active_session_change = function(_, new_session, old_session)
local new_id = new_session and new_session.id
local old_id = old_session and old_session.id
if new_id ~= old_id then
if old_id and old_id ~= new_id then
M._animation.status_data = nil
M._animation.status_session_id = nil
end
if new_id then
replay_status_for(new_id)
end
end

Expand Down Expand Up @@ -138,8 +165,9 @@ M.render = vim.schedule_wrap(function(windows)
return false
end

if not state.jobs.is_running() then
M.stop()
M.refresh()

if not M.is_running() then
return false
end

Expand Down Expand Up @@ -167,8 +195,8 @@ function M._start_animation_timer(windows)
interval = interval,
on_tick = function()
M._animation.current_frame = M._next_frame()
M.render(state.windows)
if state.jobs.is_running() then
M.render(windows)
if M._should_animate() then
return true
else
M.stop()
Expand Down Expand Up @@ -199,41 +227,84 @@ end
function M.stop()
M._clear_animation_timer()
M._animation.current_frame = 1
M._animation.status_data = nil
if state.windows and state.windows.footer_buf and vim.api.nvim_buf_is_valid(state.windows.footer_buf) then
pcall(vim.api.nvim_buf_clear_namespace, state.windows.footer_buf, M._animation.ns_id, 0, -1)
end
end

function M._should_animate()
local status = M._animation.status_data
if not status or status.type == 'idle' then
return false
end
local active_session = state.active_session
if not active_session then
return false
end
return M._animation.status_session_id == active_session.id
end

function M.sync_from_server()
local api_client = state.api_client
if not api_client or not api_client.list_session_status then
return
end

api_client
:list_session_status(state.current_cwd or vim.fn.getcwd())
:and_then(function(status_map)
if type(status_map) ~= 'table' then
return
end
for session_id, status in pairs(status_map) do
if not M._animation.last_status_map[session_id] then
M._animation.last_status_map[session_id] = status
end
end
local active_session = state.active_session
if active_session then
replay_status_for(active_session.id)
end
end)
:catch(function(err)
require('opencode.log').debug('loading_animation.sync_from_server failed: %s', tostring(err))
end)
end

function M.is_running()
return M._animation.timer ~= nil
end

local function on_running_change(_, new_value)
function M.refresh()
if not state.windows then
return
end

if not M.is_running() and new_value and new_value > 0 then
M.start(state.windows)
else
if M._should_animate() then
if not M.is_running() then
M.start(state.windows)
end
elseif M.is_running() then
M.stop()
end
end

function M.setup()
state.store.subscribe('job_count', on_running_change)
state.store.subscribe('active_session', on_active_session_change)
state.store.subscribe('job_count', M.refresh)
state.store.subscribe('active_session', M._on_active_session_change)
state.store.subscribe('event_manager', on_event_manager_change)
subscribe_session_status_event(state.event_manager)
M.sync_from_server()
end

function M.teardown()
state.store.unsubscribe('job_count', on_running_change)
state.store.unsubscribe('active_session', on_active_session_change)
state.store.unsubscribe('job_count', M.refresh)
state.store.unsubscribe('active_session', M._on_active_session_change)
state.store.unsubscribe('event_manager', on_event_manager_change)
unsubscribe_session_status_event(M._animation.status_event_manager)
M._animation.last_status_map = {}
M._animation.status_data = nil
M._animation.status_session_id = nil
M._clear_animation_timer()
end

return M
Loading
Loading