Skip to content

Commit 34f9f42

Browse files
authored
fix(ui): add defensive guards for async window/buffer operations (#264)
- topbar: pcall wrap winbar setting + win_is_valid check before render - loading_animation: buf_is_valid guard + pcall for extmark operations - curl: explicit is_running boolean flag instead of job.pid check (job.pid persists after process exit, causing false health check positives) - Add curl lifecycle tests
1 parent b379c29 commit 34f9f42

4 files changed

Lines changed: 77 additions & 9 deletions

File tree

lua/opencode/curl.lua

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ function M.request(opts)
7878

7979
if opts.stream then
8080
local buffer = ''
81+
-- job.pid is not cleared on process exit
82+
local is_running = true
8183

8284
local job_opts = {
8385
stdout = function(err, chunk)
@@ -114,20 +116,26 @@ function M.request(opts)
114116
job_opts.stdin = opts.body
115117
end
116118

117-
local job = vim.system(args, job_opts, opts.on_exit and function(result)
118-
-- Flush any remaining buffer content
119+
local job = vim.system(args, job_opts, function(result)
120+
is_running = false
121+
119122
if buffer and buffer ~= '' then
120123
opts.stream(nil, buffer)
121124
end
122-
opts.on_exit(result.code, result.signal)
123-
end or nil)
125+
126+
if opts.on_exit then
127+
opts.on_exit(result.code, result.signal)
128+
end
129+
end)
124130

125131
return {
126132
_job = job,
127133
is_running = function()
128-
return job and job.pid ~= nil
134+
return is_running
129135
end,
130136
shutdown = function()
137+
-- Flip state before kill so callers immediately observe shutdown.
138+
is_running = false
131139
if job and job.pid then
132140
pcall(function()
133141
job:kill(15) -- SIGTERM

lua/opencode/ui/loading_animation.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ end
9797
function M.stop()
9898
M._clear_animation_timer()
9999
M._animation.current_frame = 1
100-
if state.windows and state.windows.footer_buf then
101-
vim.api.nvim_buf_clear_namespace(state.windows.footer_buf, M._animation.ns_id, 0, -1)
100+
if state.windows and state.windows.footer_buf and vim.api.nvim_buf_is_valid(state.windows.footer_buf) then
101+
pcall(vim.api.nvim_buf_clear_namespace, state.windows.footer_buf, M._animation.ns_id, 0, -1)
102102
end
103103
end
104104

lua/opencode/ui/topbar.lua

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ local function format_token_info()
1616
if state.current_model then
1717
if config.ui.display_context_size then
1818
local provider, model = state.current_model:match('^(.-)/(.+)$')
19-
local model_info = config_file.get_model_info(provider, model)
19+
local ok, model_info = pcall(config_file.get_model_info, provider, model)
20+
if not ok then
21+
model_info = nil
22+
end
2023
local limit = state.tokens_count and model_info and model_info.limit and model_info.limit.context or 0
2124
table.insert(parts, util.format_number(state.tokens_count) or nil)
2225
if limit > 0 then
@@ -92,7 +95,7 @@ function M.render()
9295
return
9396
end
9497
local win = state.windows.output_win
95-
if not win then
98+
if not win or not vim.api.nvim_win_is_valid(win) then
9699
return
97100
end
98101

tests/unit/curl_spec.lua

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
local curl = require('opencode.curl')
2+
3+
describe('curl stream handle lifecycle', function()
4+
local original_system
5+
6+
before_each(function()
7+
original_system = vim.system
8+
end)
9+
10+
after_each(function()
11+
vim.system = original_system
12+
end)
13+
14+
it('marks stream handle as stopped after process exit', function()
15+
local on_complete
16+
17+
vim.system = function(_, _, cb)
18+
on_complete = cb
19+
return {
20+
pid = 123,
21+
kill = function() end,
22+
}
23+
end
24+
25+
local handle = curl.request({
26+
url = 'http://127.0.0.1:1/event',
27+
stream = function() end,
28+
})
29+
30+
assert.is_true(handle.is_running())
31+
on_complete({ code = 0, signal = 0 })
32+
assert.is_false(handle.is_running())
33+
end)
34+
35+
it('marks stream handle as stopped on shutdown', function()
36+
local killed = false
37+
38+
vim.system = function(_, _, _)
39+
return {
40+
pid = 123,
41+
kill = function()
42+
killed = true
43+
end,
44+
}
45+
end
46+
47+
local handle = curl.request({
48+
url = 'http://127.0.0.1:1/event',
49+
stream = function() end,
50+
})
51+
52+
handle.shutdown()
53+
54+
assert.is_true(killed)
55+
assert.is_false(handle.is_running())
56+
end)
57+
end)

0 commit comments

Comments
 (0)