Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
efccfa2
refactor(renderer): add batched flush, append and scroll subsystems
sudo-tee Mar 26, 2026
ed72861
feat(renderer): incremental rendering, markdown-on-idle, and changed-…
sudo-tee Mar 26, 2026
55d1241
fix: cursor scrolling
sudo-tee Mar 26, 2026
4194128
fix(renderer/buffer): include first line when slicing extmarks
sudo-tee Mar 26, 2026
b5325d5
perf(renderer): suppress TextChanged during flushes, reduce deepcopy …
sudo-tee Mar 27, 2026
d5144fe
refactor(renderer): remove prev_line_count; extract scrolling logic a…
sudo-tee Mar 27, 2026
debf63b
feat(ui): refactor renderer and scrolling
sudo-tee Mar 27, 2026
cf438f9
Update lua/opencode/config.lua
sudo-tee Mar 27, 2026
09ab292
revert: disable session.status subscription in loading animation (avo…
sudo-tee Mar 30, 2026
22c4b84
refactor(ui/output): use sticky at-bottom viewport flag for auto-scroll
sudo-tee Mar 30, 2026
7a9b428
refactor(ui/renderer/tests): remove trailing-border flags, filter int…
sudo-tee Mar 30, 2026
9394bad
fix(events): ignore server heartbeat and avoid draining empty queue
sudo-tee Mar 31, 2026
6a86b99
refactor(renderer/flush): suppress output-window autocmds and ensure …
sudo-tee Apr 1, 2026
6639e32
refactor(ui/loading_animation): simplify session status handling
sudo-tee Apr 1, 2026
e80116e
fix(formatter/grep): normalize grep input handling
sudo-tee Apr 1, 2026
afc7c49
refactor(renderer): extract extmark_clear_range and avoid redundant c…
sudo-tee Apr 1, 2026
0556f70
fix(ui): correct action & task line offsets
sudo-tee Apr 1, 2026
30ea5db
fix(renderer/flush): guard 'eventignorewin' usage on older Neovim
sudo-tee Apr 1, 2026
837a11d
refactor(ui): add type annotations and clarifying comments
sudo-tee Apr 1, 2026
53d5864
test(renderer): make eventignorewin handling robust across Neovim ver…
sudo-tee Apr 1, 2026
51922dd
feat(renderer): pin permission/question displays to bottom and add tests
sudo-tee Apr 2, 2026
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/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ M.defaults = {
rendering = {
markdown_debounce_ms = 250,
on_data_rendered = nil,
markdown_on_idle = false,
-- If set to a number, markdown rendering will be deferred while
-- `state.user_message_count[session_id]` is greater than this value.
-- If `nil`, the existing behavior is used (defer while > 0).
markdown_on_idle_threshold = nil,
event_throttle_ms = 40,
event_collapsing = true,
},
Expand Down Expand Up @@ -255,6 +260,8 @@ M.defaults = {
enabled = false,
capture_streamed_events = false,
show_ids = true,
highlight_changed_lines = false,
highlight_changed_lines_timeout_ms = 120,
quick_chat = {
keep_session = false,
set_active_session = false,
Expand Down
2 changes: 2 additions & 0 deletions lua/opencode/core.lua
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,8 @@ M.initialize_current_model = Promise.async(function()
end)

M._on_user_message_count_change = Promise.async(function(_, new, old)
require('opencode.ui.renderer.flush').flush_pending_on_data_rendered()

if config.hooks and config.hooks.on_done_thinking then
local all_sessions = session.get_all_workspace_sessions():await()
local done_sessions = vim.tbl_filter(function(s)
Expand Down
1 change: 0 additions & 1 deletion lua/opencode/id.lua
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,3 @@ function M.get_prefixes()
end

return M

4 changes: 3 additions & 1 deletion lua/opencode/throttling_emitter.lua
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ function ThrottlingEmitter:_drain()
local items_to_process = self.queue
self.queue = {}

self.process_fn(items_to_process)
if #items_to_process > 0 then
self.process_fn(items_to_process)
end

-- double check that items weren't added while processing
if #self.queue > 0 and not self.drain_scheduled then
Expand Down
3 changes: 3 additions & 0 deletions lua/opencode/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@
---@class OpencodeUIOutputRenderingConfig
---@field markdown_debounce_ms number
---@field on_data_rendered (fun(buf: integer, win: integer)|boolean)|nil
---@field markdown_on_idle boolean
---@field event_throttle_ms number
---@field event_collapsing boolean

Expand Down Expand Up @@ -207,6 +208,8 @@
---@field enabled boolean
---@field capture_streamed_events boolean
---@field show_ids boolean
---@field highlight_changed_lines boolean
---@field highlight_changed_lines_timeout_ms number
---@field quick_chat {keep_session: boolean, set_active_session: boolean}

---@class OpencodeHooks
Expand Down
8 changes: 8 additions & 0 deletions lua/opencode/ui/base_picker.lua
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ local function telescope_ui(opts)
local entry_display = require('telescope.pickers.entry_display')

-- Create displayer dynamically based on number of parts
---@param picker_item PickerItem
---@return table
local function create_displayer(picker_item)
local items = {}
for _ in ipairs(picker_item.parts) do
Expand Down Expand Up @@ -129,6 +131,7 @@ local function telescope_ui(opts)
return entry
end

---@return unknown
local function refresh_picker()
return current_picker
and current_picker:refresh(
Expand Down Expand Up @@ -232,6 +235,7 @@ end
local function fzf_ui(opts)
local fzf_lua = require('fzf-lua')

---@return table
local function create_fzf_config()
local has_multi_action = util.some(opts.actions, function(action)
return action.multi_selection
Expand Down Expand Up @@ -261,6 +265,7 @@ local function fzf_ui(opts)
}
end

---@return fun(fzf_cb: fun(line?: string))
local function create_finder()
return function(fzf_cb)
for idx, item in ipairs(opts.items) do
Expand Down Expand Up @@ -299,6 +304,7 @@ local function fzf_ui(opts)
end
end

---Reopen fzf-lua to reflect updated picker items.
local function refresh_fzf()
vim.schedule(function()
fzf_ui(opts)
Expand Down Expand Up @@ -644,6 +650,7 @@ function M.create_picker_item(parts)
parts = parts,
}

---@return string
function item:to_string()
local texts = {}
for _, part in ipairs(self.parts) do
Expand All @@ -652,6 +659,7 @@ function M.create_picker_item(parts)
return table.concat(texts, ' ')
end

---@return table
function item:to_formatted_text()
local formatted = {}
for _, part in ipairs(self.parts) do
Expand Down
104 changes: 70 additions & 34 deletions lua/opencode/ui/dialog.lua
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,11 @@ function Dialog:format_dialog(output, config)
local end_line = output:get_line_count()

if config.border_hl then
formatter.add_vertical_border(output, start_line + 1, end_line, config.border_hl, -2)
local border_end = end_line
if config.extend_border_to_trailing_blank then
border_end = border_end + 1
end
formatter.add_vertical_border(output, start_line + 1, border_end, config.border_hl, -2)
end

output:add_line('')
Expand All @@ -277,15 +281,22 @@ function Dialog:format_options(output, options)
label = label .. ' - ' .. option.description
end

local line_idx = output:get_line_count()
local is_selected = self._selected_index == i
local line_text = is_selected and string.format(' %d. %s ', i, label) or string.format(' %d. %s', i, label)

output:add_line(line_text)
-- Output uses 0-based indexing for extmarks. The correct target for
-- extmarks is the previous line count (0-based) because add_line will
-- append a new line and increase the 1-based line count. Capture the
-- current count first and then add the line so we can use that 0-based
-- index for extmarks.
-- add_line returns a 1-based line index; Output extmarks use 0-based
-- keys, so subtract 1 to get the correct extmark key.
local added_idx = output:add_line(line_text)

if is_selected then
output:add_extmark(line_idx, { line_hl_group = 'OpencodeDialogOptionHover' } --[[@as OutputExtmark]])
output:add_extmark(line_idx, {
local extmark_idx = added_idx - 1
output:add_extmark(extmark_idx, { line_hl_group = 'OpencodeDialogOptionHover' } --[[@as OutputExtmark]])
output:add_extmark(extmark_idx, {
start_col = 2,
virt_text = { { '› ', 'OpencodeDialogOptionHover' } },
virt_text_pos = 'overlay',
Expand All @@ -309,11 +320,16 @@ function Dialog:_setup_keymaps()
if keymaps.up then
for _, key in ipairs(keymaps.up) do
if key and key ~= '' then
vim.keymap.set('n', key, function()
self:navigate(-1)
end, vim.tbl_extend('force', keymap_opts, {
desc = 'Dialog: navigate up',
}))
vim.keymap.set(
'n',
key,
function()
self:navigate(-1)
end,
vim.tbl_extend('force', keymap_opts, {
desc = 'Dialog: navigate up',
})
)
table.insert(self._keymaps, key)
end
end
Expand All @@ -322,31 +338,46 @@ function Dialog:_setup_keymaps()
if keymaps.down then
for _, key in ipairs(keymaps.down) do
if key and key ~= '' then
vim.keymap.set('n', key, function()
self:navigate(1)
end, vim.tbl_extend('force', keymap_opts, {
desc = 'Dialog: navigate down',
}))
vim.keymap.set(
'n',
key,
function()
self:navigate(1)
end,
vim.tbl_extend('force', keymap_opts, {
desc = 'Dialog: navigate down',
})
)
table.insert(self._keymaps, key)
end
end
end

if keymaps.select and keymaps.select ~= '' then
vim.keymap.set('n', keymaps.select, function()
self:select()
end, vim.tbl_extend('force', keymap_opts, {
desc = 'Dialog: select option',
}))
vim.keymap.set(
'n',
keymaps.select,
function()
self:select()
end,
vim.tbl_extend('force', keymap_opts, {
desc = 'Dialog: select option',
})
)
table.insert(self._keymaps, keymaps.select)
end

if keymaps.dismiss and keymaps.dismiss ~= '' then
vim.keymap.set('n', keymaps.dismiss, function()
self:dismiss()
end, vim.tbl_extend('force', keymap_opts, {
desc = 'Dialog: dismiss',
}))
vim.keymap.set(
'n',
keymaps.dismiss,
function()
self:dismiss()
end,
vim.tbl_extend('force', keymap_opts, {
desc = 'Dialog: dismiss',
})
)
table.insert(self._keymaps, keymaps.dismiss)
end

Expand All @@ -355,15 +386,20 @@ function Dialog:_setup_keymaps()
local number_keymap_opts = vim.tbl_extend('force', keymap_opts, { nowait = true })
for i = 1, math.min(option_count, 9) do
local key = tostring(i)
vim.keymap.set('n', key, function()
if not self._active or not self._config.check_focused() then
return
end
self._selected_index = i
self._config.on_select(i)
end, vim.tbl_extend('force', number_keymap_opts, {
desc = 'Dialog: select option ' .. key,
}))
vim.keymap.set(
'n',
key,
function()
if not self._active or not self._config.check_focused() then
return
end
self._selected_index = i
self._config.on_select(i)
end,
vim.tbl_extend('force', number_keymap_opts, {
desc = 'Dialog: select option ' .. key,
})
)
table.insert(self._keymaps, key)
end
end
Expand Down
Loading
Loading