Skip to content

Commit b4bdd01

Browse files
committed
feat(output): preview latest N lines below closed folds
Add a new ui.output.tools setting only_show_latest_n that keeps the latest N lines visible below a closed fold for long-running tool output. Update folding logic to compute fold endpoints respecting this preview count, update fold text to mention the preview when enabled, lower the default folding threshold.
1 parent 3798e7f commit b4bdd01

7 files changed

Lines changed: 174 additions & 7 deletions

File tree

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,12 +236,13 @@ require('opencode').setup({
236236
},
237237
output = {
238238
filetype = 'opencode_output', -- Filetype assigned to the output buffer (default: 'opencode_output')
239-
compact_assistant_headers = false, -- 'full' (default), 'minimal' (compact if same mode), or 'hidden' (no headers for assistant)
239+
compact_assistant_headers = false, -- 'full' (default), 'minimal' (compact if same mode), or 'hidden' (no headers for assistant)
240240
tools = {
241241
show_output = true, -- Show tools output [diffs, cmd output, etc.] (default: true)
242242
show_reasoning_output = true, -- Show reasoning/thinking steps output (default: true)
243243
use_folds = true, -- Use folds for tool output (default: true)
244-
folding_threshold = 5, -- Number of lines to show before folding when show_output is true (default: 5)
244+
only_show_latest_n = nil, -- Keep the latest N lines visible below a closed fold; nil keeps the current behavior (default: nil)
245+
folding_threshold = 25, -- Number of leading lines to keep visible before folding when show_output is true (default: 25)
245246
},
246247
rendering = {
247248
markdown_debounce_ms = 250, -- Debounce time for markdown rendering on new data (default: 250ms)

lua/opencode/config.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ M.defaults = {
177177
show_output = true,
178178
show_reasoning_output = true,
179179
use_folds = true,
180+
-- Keep the latest N lines visible below folds for long-running progress.
181+
only_show_latest_n = 3,
180182
-- Reduced default threshold to make small tool outputs foldable by default.
181183
-- Users can override this in their config if they prefer the previous value.
182184
folding_threshold = 25,

lua/opencode/types.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@
254254

255255
---@class OpencodeUIOutputConfig
256256
---@field time_format string|nil # Custom os.date format for timestamps, e.g. '%m/%d %H:%M'. Uses fixed default when nil.
257-
---@field tools { show_output: boolean, show_reasoning_output: boolean, use_folds: boolean, folding_threshold: number }
257+
---@field tools { show_output: boolean, show_reasoning_output: boolean, use_folds: boolean, folding_threshold: number, only_show_latest_n: integer|nil }
258258
---@field rendering OpencodeUIOutputRenderingConfig
259259
---@field max_messages integer|nil
260260
---@field always_scroll_to_bottom boolean

lua/opencode/ui/output.lua

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,14 +103,26 @@ function Output:add_fold_with_threshold(start_line, show, use_folds)
103103
end
104104

105105
local threshold = config.ui.output.tools.folding_threshold or 5
106+
local only_show_latest_n = config.ui.output.tools.only_show_latest_n
107+
if type(only_show_latest_n) ~= 'number' or only_show_latest_n < 1 then
108+
only_show_latest_n = 0
109+
else
110+
only_show_latest_n = math.floor(only_show_latest_n)
111+
end
112+
106113
local end_line = self:get_line_count()
107114

108115
if not show then
109-
if end_line > start_line then
110-
self:add_fold(start_line, end_line)
116+
local fold_end = end_line - only_show_latest_n
117+
if fold_end >= start_line then
118+
self:add_fold(start_line, fold_end)
111119
end
112120
elseif end_line > start_line + threshold then
113-
self:add_fold(start_line + threshold, end_line)
121+
local fold_start = start_line + threshold
122+
local fold_end = end_line - only_show_latest_n
123+
if fold_end >= fold_start then
124+
self:add_fold(fold_start, fold_end)
125+
end
114126
end
115127
end
116128

lua/opencode/ui/output_window.lua

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,13 @@ function M.fold_text()
339339
local windows = state.windows
340340
local output_buf = windows and windows.output_buf
341341
local line = vim.v.foldstart
342+
local latest_n = config.ui.output.tools.only_show_latest_n
343+
344+
if type(latest_n) ~= 'number' or latest_n < 1 then
345+
latest_n = nil
346+
else
347+
latest_n = math.floor(latest_n)
348+
end
342349

343350
if not output_buf or not vim.api.nvim_buf_is_valid(output_buf) then
344351
return vim.fn.foldtext()
@@ -355,7 +362,12 @@ function M.fold_text()
355362
end
356363

357364
if line_count > 0 then
358-
local text = string.format('▶ %d lines hidden (zo open, zc close) ◀', line_count)
365+
local text
366+
if latest_n then
367+
text = string.format('▶ %d lines hidden, latest %d below (zo open, zc close) ◀', line_count, latest_n)
368+
else
369+
text = string.format('▶ %d lines hidden (zo open, zc close) ◀', line_count)
370+
end
359371
local width = vim.api.nvim_win_get_width(0)
360372
local padding = math.max(0, math.floor((width - #text) / 2))
361373
return string.rep('-', padding) .. text .. string.rep('-', padding)

tests/unit/output_spec.lua

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
local config = require('opencode.config')
2+
local Output = require('opencode.ui.output')
3+
4+
describe('output fold thresholds', function()
5+
local original_config
6+
7+
before_each(function()
8+
original_config = vim.deepcopy(config.values)
9+
config.values = vim.deepcopy(config.defaults)
10+
end)
11+
12+
after_each(function()
13+
config.values = original_config
14+
end)
15+
16+
it('uses the default latest-line preview when only_show_latest_n is not overridden', function()
17+
config.setup({
18+
ui = {
19+
output = {
20+
tools = {
21+
folding_threshold = 3,
22+
},
23+
},
24+
},
25+
})
26+
27+
local output = Output.new()
28+
output:add_lines({ '1', '2', '3', '4', '5', '6', '7' })
29+
30+
output:add_fold_with_threshold(1, true, true)
31+
32+
assert.same({ { from = 4, to = 4 } }, output.fold_ranges)
33+
end)
34+
35+
it('preserves the current fold behavior when only_show_latest_n is explicitly disabled', function()
36+
config.setup({
37+
ui = {
38+
output = {
39+
tools = {
40+
folding_threshold = 3,
41+
only_show_latest_n = 0,
42+
},
43+
},
44+
},
45+
})
46+
47+
local output = Output.new()
48+
output:add_lines({ '1', '2', '3', '4', '5', '6', '7' })
49+
50+
output:add_fold_with_threshold(1, true, true)
51+
52+
assert.same({ { from = 4, to = 7 } }, output.fold_ranges)
53+
end)
54+
55+
it('keeps the latest lines visible below folds when output is shown', function()
56+
config.setup({
57+
ui = {
58+
output = {
59+
tools = {
60+
folding_threshold = 3,
61+
only_show_latest_n = 2,
62+
},
63+
},
64+
},
65+
})
66+
67+
local output = Output.new()
68+
output:add_lines({ '1', '2', '3', '4', '5', '6', '7' })
69+
70+
output:add_fold_with_threshold(1, true, true)
71+
72+
assert.same({ { from = 4, to = 5 } }, output.fold_ranges)
73+
end)
74+
75+
it('keeps the latest lines visible when output is otherwise hidden', function()
76+
config.setup({
77+
ui = {
78+
output = {
79+
tools = {
80+
show_output = false,
81+
only_show_latest_n = 2,
82+
},
83+
},
84+
},
85+
})
86+
87+
local output = Output.new()
88+
output:add_lines({ '1', '2', '3', '4', '5', '6', '7' })
89+
90+
output:add_fold_with_threshold(1, false, true)
91+
92+
assert.same({ { from = 1, to = 5 } }, output.fold_ranges)
93+
end)
94+
end)

tests/unit/output_window_spec.lua

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,52 @@ describe('output_window.setup', function()
211211
end)
212212
end)
213213

214+
describe('output_window.fold_text', function()
215+
local buf
216+
local win
217+
218+
before_each(function()
219+
buf = vim.api.nvim_create_buf(false, true)
220+
win = vim.api.nvim_open_win(buf, true, {
221+
relative = 'editor',
222+
width = 100,
223+
height = 10,
224+
row = 0,
225+
col = 0,
226+
})
227+
state.ui.set_windows({ output_buf = buf, output_win = win })
228+
output_window.setup({ output_buf = buf, output_win = win })
229+
output_window.set_lines({ 'a', 'b', 'c', 'd', 'e', 'f' })
230+
end)
231+
232+
after_each(function()
233+
state.ui.set_windows(nil)
234+
pcall(vim.api.nvim_win_close, win, true)
235+
pcall(vim.api.nvim_buf_delete, buf, { force = true })
236+
end)
237+
238+
it('mentions visible latest lines when latest preview is enabled', function()
239+
config.setup({
240+
ui = {
241+
output = {
242+
tools = {
243+
only_show_latest_n = 2,
244+
},
245+
},
246+
},
247+
})
248+
249+
output_window.set_folds({ { from = 2, to = 4 } })
250+
251+
local text = vim.api.nvim_win_call(win, function()
252+
vim.v.foldstart = 2
253+
return output_window.fold_text()
254+
end)
255+
256+
assert.is_truthy(text:find('3 lines hidden, latest 2 below', 1, true))
257+
end)
258+
end)
259+
214260
describe('output_window extmarks', function()
215261
local buf
216262

0 commit comments

Comments
 (0)