Skip to content
6 changes: 3 additions & 3 deletions lua/opencode/commands/handlers/workflow.lua
Original file line number Diff line number Diff line change
Expand Up @@ -273,14 +273,14 @@ function M.actions.toggle_tool_output()
local action_text = config.ui.output.tools.show_output and 'Hiding' or 'Showing'
vim.notify(action_text .. ' tool output display', vim.log.levels.INFO)
config.values.ui.output.tools.show_output = not config.ui.output.tools.show_output
ui.render_output()
ui.render_output_from_cache()
end

function M.actions.toggle_reasoning_output()
local action_text = config.ui.output.tools.show_reasoning_output and 'Hiding' or 'Showing'
vim.notify(action_text .. ' reasoning output display', vim.log.levels.INFO)
config.values.ui.output.tools.show_reasoning_output = not config.ui.output.tools.show_reasoning_output
ui.render_output()
ui.render_output_from_cache()
end

local original_max_messages = config.ui.output.max_messages
Expand All @@ -297,7 +297,7 @@ function M.actions.toggle_max_messages()
local val_text = next_val == nil and 'none' or tostring(next_val)
vim.notify(action_text .. ' message limit to ' .. val_text, vim.log.levels.INFO)
config.values.ui.output.max_messages = next_val
ui.render_output()
ui.render_output_from_cache()
end

M.actions.review = Promise.async(function(args)
Expand Down
19 changes: 18 additions & 1 deletion lua/opencode/ui/renderer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ end
function M._render_full_session_data(session_data, opts)
opts = opts or {}
M.reset()
state.renderer.set_messages(vim.deepcopy(session_data or {}))
state.renderer.set_messages(session_data or {})

if not state.active_session or not state.messages then
return
Expand Down Expand Up @@ -390,6 +390,23 @@ function M._render_full_session_data(session_data, opts)
end
end

---Re-render from cached session data without a server round-trip.
---Used for display-only changes (toggle folds, max_messages, etc.)
---@param session_data OpencodeMessage[]
function M.render_from_cache(session_data)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea , the round trip was not necessary at all.

if not output_window.mounted() or not state.api_client then
return
end
M._render_full_session_data(session_data, {
restore_model_from_messages = true,
})
local active_session = state.active_session
if active_session and active_session.id then
require('opencode.ui.question_window').restore_pending_question(active_session.id)
permission_window.restore_pending_permissions(active_session.id)
end
end

---Fetch the active session from the server and render it
---@return Promise<OpencodeMessage[]>
function M.render_full_session()
Expand Down
78 changes: 74 additions & 4 deletions lua/opencode/ui/renderer/buffer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -600,12 +600,12 @@ function M.upsert_part_now(part_id, message_id, formatted_data, previous_formatt
set_part_extmark_state(part_id, formatted_data)

if formatted_data.fold_ranges and #formatted_data.fold_ranges > 0 then
M.set_all_folds()
M.update_part_folds(part_id)
end

return true
end

local insert_at = get_part_insertion_line(part_id, message_id)
if not insert_at then
return false
Expand Down Expand Up @@ -635,22 +635,89 @@ end

function M.set_all_folds()
local all_folds = {}
ctx.part_folds = {}
for part_id_iter, data in pairs(ctx.formatted_parts) do
if data.fold_ranges then
local cached_part = ctx.render_state:get_part(part_id_iter)
if cached_part and cached_part.line_start then
local part_abs_folds = {}
for _, f in ipairs(data.fold_ranges) do
table.insert(all_folds, {
local abs = {
from = cached_part.line_start + f.from - 1,
to = cached_part.line_start + f.to - 1,
})
}
table.insert(part_abs_folds, abs)
table.insert(all_folds, abs)
end
ctx.part_folds[part_id_iter] = part_abs_folds
end
end
end
ctx.global_folds = all_folds
output_window.set_folds(all_folds)
end

---Update folds for a single part during streaming, avoiding a full rebuild.
---@param part_id string
function M.update_part_folds(part_id)
local formatted_data = ctx.formatted_parts[part_id]
if not formatted_data or not formatted_data.fold_ranges then
ctx.part_folds[part_id] = nil
M.set_all_folds()
return
end
local cached_part = ctx.render_state:get_part(part_id)
if not cached_part or not cached_part.line_start then
return
end

local new_folds = {}
for _, f in ipairs(formatted_data.fold_ranges) do
table.insert(new_folds, {
from = cached_part.line_start + f.from - 1,
to = cached_part.line_start + f.to - 1,
})
end

local old_folds = ctx.part_folds[part_id]
if old_folds and #old_folds == #new_folds then
local same = true
for i = 1, #new_folds do
if new_folds[i].from ~= old_folds[i].from or new_folds[i].to ~= old_folds[i].to then
same = false
break
end
end
if same then
return
end
end
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could become a reusable function folds_equal

local function folds_equal(a, b)
  if #a ~= #b then return false end
  for i = 1, #a do
    if a[i].from ~= b[i].from or a[i].to ~= b[i].to then return false end
  end
  return true
end


local new_global = {}
local found = false
for pid, pf in pairs(ctx.part_folds) do
if pid == part_id then
found = true
for _, nf in ipairs(new_folds) do
table.insert(new_global, nf)
end
else
for _, of in ipairs(pf) do
table.insert(new_global, of)
end
end
end
if not found then
for _, nf in ipairs(new_folds) do
table.insert(new_global, nf)
end
end

ctx.part_folds[part_id] = new_folds
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this whole block can be simplified.

By assigning the new folds to the ctx.part_folds first then after this it's only matter of looping to the fold list.

local new_global = {}

ctx.part_folds[part_id] = new_folds

for pid, part_folds in pairs(ctx.part_folds) do
  for _, f in ipairs(part_folds ) do
    table.insert(new_global, f)
  end
end

ctx.global_folds = new_global
output_window.set_folds(new_global)
end


---@param part_id string
---@param extra_lines string[]
Expand All @@ -676,6 +743,9 @@ function M.append_part_now(part_id, extra_lines, extra_extmarks, previous_format
apply_part_actions(part_id, formatted_data, cached.line_start)
apply_extmarks(previous_formatted, formatted_data, cached.line_start, old_line_end, new_line_end)
set_part_extmark_state(part_id, formatted_data)
if formatted_data.fold_ranges then
M.update_part_folds(part_id)
end
elseif has_extmarks(extra_extmarks) then
output_window.set_extmarks(extra_extmarks, insert_at)
end
Expand Down
6 changes: 6 additions & 0 deletions lua/opencode/ui/renderer/ctx.lua
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ local ctx = {
bulk_buffer_lines = {},
bulk_extmarks_by_line = {},
bulk_folds = {},
---@type {from: number, to: number}[]
global_folds = {},
---@type table<string, {from: number, to: number}[]>
part_folds = {},
}

---Reset all renderer caches and pending state.
Expand All @@ -50,6 +54,8 @@ function ctx:reset()
}
self.flush_scheduled = false
self.markdown_render_scheduled = false
self.global_folds = {}
self.part_folds = {}
self:bulk_reset()
end

Expand Down
13 changes: 13 additions & 0 deletions lua/opencode/ui/ui.lua
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,19 @@ function M.clear_output()
-- state.restore_points = {}
end

---Re-render the output buffer from cached session data, avoiding a server round-trip.
---Used for display-only toggles (show_reasoning_output, show_output, max_messages).
---Falls back to render_output() if no cached messages are available.
---@param opts? {force_scroll?: boolean}
function M.render_output_from_cache(opts)
local session_data = state.messages
if not session_data or not next(session_data) then
M.render_output(false, opts)
return
end
renderer.render_from_cache(session_data)
end

---Force a full rerender of the output buffer. Should be done synchronously if
---called before submitting input or doing something that might generate events
---from opencode
Expand Down
Loading