Skip to content

Commit 626098e

Browse files
committed
feat(reasoning): show reasoning output + toggle
This PR add reasoning output to the panel. It cols be toggled with `/reasoning` or `/thinking`, `Opencode toggle_reasoning_output ` or `<leader>otr` This should resolve #138
1 parent fcc8666 commit 626098e

File tree

9 files changed

+245
-11
lines changed

9 files changed

+245
-11
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ require('opencode').setup({
127127
['<leader>opa'] = { 'permission_accept' }, -- Accept permission request once
128128
['<leader>opA'] = { 'permission_accept_all' }, -- Accept all (for current tool)
129129
['<leader>opd'] = { 'permission_deny' }, -- Deny permission request once
130+
['<leader>ott'] = { 'toggle_tools_output' }, -- Toggle tools output (diffs, cmd output, etc.)
131+
['<leader>otr'] = { 'toggle_reasoning_output' }, -- Toggle reasoning output (thinking steps)
130132
},
131133
input_window = {
132134
['<cr>'] = { 'submit_input_prompt', mode = { 'n', 'i' } }, -- Submit prompt (normal mode and insert mode)
@@ -191,6 +193,7 @@ require('opencode').setup({
191193
output = {
192194
tools = {
193195
show_output = true, -- Show tools output [diffs, cmd output, etc.] (default: true)
196+
show_reasoning_output = true, -- Show reasoning/thinking steps output (default: true)
194197
},
195198
rendering = {
196199
markdown_debounce_ms = 250, -- Debounce time for markdown rendering on new data (default: 250ms)
@@ -389,7 +392,8 @@ The plugin provides the following actions that can be triggered via keymaps, com
389392
| Navigate to next prompt in history | `<down>` | - | `require('opencode.api').next_history()` |
390393
| Toggle input/output panes | `<tab>` | - | - |
391394
| Swap Opencode pane left/right | `<leader>ox` | `:Opencode swap position` | `require('opencode.api').swap_position()` |
392-
| Toggle tools output (diffs, cmd output, etc.) | - | `:Opencode toggle_tools_output` | `require('opencode.api').toggle_tools_output()` |
395+
| Toggle tools output (diffs, cmd output, etc.) | `<leader>ott` | `:Opencode toggle_tools_output` | `require('opencode.api').toggle_tools_output()` |
396+
| Toggle reasoning output (thinking steps) | `<leader>otr` | `:Opencode toggle_reasoning_output` | `require('opencode.api').toggle_reasoning_output()` |
393397

394398
---
395399

lua/opencode/api.lua

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -904,10 +904,19 @@ function M.permission_deny()
904904
end
905905

906906
function M.toggle_tool_output()
907+
local action_text = config.ui.output.tools.show_output and 'Hiding' or 'Showing'
908+
vim.notify(action_text .. ' tool output display', vim.log.levels.INFO)
907909
config.values.ui.output.tools.show_output = not config.ui.output.tools.show_output
908910
ui.render_output()
909911
end
910912

913+
function M.toggle_reasoning_output()
914+
local action_text = config.ui.output.tools.show_reasoning_output and 'Hiding' or 'Showing'
915+
vim.notify(action_text .. ' reasoning output display', vim.log.levels.INFO)
916+
config.values.ui.output.tools.show_reasoning_output = not config.ui.output.tools.show_reasoning_output
917+
ui.render_output()
918+
end
919+
911920
---@type table<string, OpencodeUICommand>
912921
M.commands = {
913922
open = {
@@ -1210,6 +1219,11 @@ M.commands = {
12101219
desc = 'Toggle tool output visibility in the output window',
12111220
fn = M.toggle_tool_output,
12121221
},
1222+
1223+
toggle_reasoning_output = {
1224+
desc = 'Toggle reasoning output visibility in the output window',
1225+
fn = M.toggle_reasoning_output,
1226+
},
12131227
paste_image = {
12141228
desc = 'Paste image from clipboard and add to context',
12151229
fn = M.paste_image,
@@ -1234,6 +1248,8 @@ M.slash_commands_map = {
12341248
['/undo'] = { fn = M.undo, desc = 'Undo last action' },
12351249
['/unshare'] = { fn = M.unshare, desc = 'Unshare current session' },
12361250
['/rename'] = { fn = M.rename_session, desc = 'Rename current session' },
1251+
['/thinking'] = { fn = M.toggle_reasoning_output, desc = 'Toggle reasoning output' },
1252+
['/reasoning'] = { fn = M.toggle_reasoning_output, desc = 'Toggle reasoning output' },
12371253
}
12381254

12391255
M.legacy_command_map = {

lua/opencode/config.lua

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ M.defaults = {
4141
['<leader>oPa'] = { 'permission_accept', desc = 'Accept permission' },
4242
['<leader>oPA'] = { 'permission_accept_all', desc = 'Accept all permissions' },
4343
['<leader>oPd'] = { 'permission_deny', desc = 'Deny permission' },
44+
['<leader>otr'] = { 'toggle_reasoning_output', desc = 'Toggle reasoning output' },
45+
['<leader>ott'] = { 'toggle_tool_output', desc = 'Toggle tool output' },
4446
},
4547
output_window = {
4648
['<esc>'] = { 'close' },
@@ -118,6 +120,7 @@ M.defaults = {
118120
},
119121
tools = {
120122
show_output = true,
123+
show_reasoning_output = true,
121124
},
122125
always_scroll_to_bottom = false,
123126
},

lua/opencode/types.lua

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@
129129
---@field output OpencodeUIOutputConfig
130130
---@field input { text: { wrap: boolean } }
131131
---@field completion OpencodeCompletionConfig
132+
---@field highlights? OpencodeHighlightConfig
133+
134+
---@class OpencodeHighlightConfig
135+
---@field vertical_borders? { tool?: { fg?: string, bg?: string }, user?: { fg?: string, bg?: string }, assistant?: { fg?: string, bg?: string } }
132136

133137
---@class OpencodeUIOutputRenderingConfig
134138
---@field markdown_debounce_ms number
@@ -137,7 +141,7 @@
137141
---@field event_collapsing boolean
138142

139143
---@class OpencodeUIOutputConfig
140-
---@field tools { show_output: boolean }
144+
---@field tools { show_output: boolean, show_reasoning_output: boolean }
141145
---@field rendering OpencodeUIOutputRenderingConfig
142146
---@field always_scroll_to_bottom boolean
143147

@@ -382,7 +386,7 @@
382386
---@field value string|nil
383387

384388
---@class OpencodeMessagePart
385-
---@field type 'text'|'file'|'agent'|'tool'|'step-start'|'patch'|string
389+
---@field type 'text'|'file'|'agent'|'tool'|'step-start'|'patch'|'reasoning'|string
386390
---@field id string|nil Unique identifier for tool use parts
387391
---@field text string|nil
388392
---@field tool string|nil Name of the tool being used
@@ -399,6 +403,7 @@
399403
---@field callID string|nil Call identifier (used for tools)
400404
---@field hash string|nil Hash identifier for patch parts
401405
---@field files string[]|nil List of file paths for patch parts
406+
---@field time { start: number, end: number }|nil Timestamps for the part
402407

403408
---@class OpencodeModelModalities
404409
---@field input ('text'|'image'|'audio'|'video')[] Supported input modalities

lua/opencode/ui/formatter.lua

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,46 @@ M.separator = {
1414
'',
1515
}
1616

17+
---@param output Output
18+
---@param part OpencodeMessagePart
19+
function M._format_reasoning(output, part)
20+
local text = vim.trim(part.text or '')
21+
if text == '' then
22+
return
23+
end
24+
25+
local start_line = output:get_line_count() + 1
26+
27+
local title = 'Reasoning'
28+
local time = part.time
29+
if time and type(time) == 'table' and time.start then
30+
local start_text = util.format_time(time.start) or ''
31+
local end_text = (time['end'] and util.format_time(time['end'])) or nil
32+
if end_text and end_text ~= '' then
33+
title = string.format('%s (%s - %s)', title, start_text, end_text)
34+
elseif start_text ~= '' then
35+
title = string.format('%s (%s)', title, start_text)
36+
end
37+
end
38+
39+
M._format_action(output, icons.get('reasoning') .. ' ' .. title, '')
40+
41+
if config.ui.output.tools.show_reasoning_output then
42+
output:add_empty_line()
43+
output:add_lines(vim.split(text, '\n'))
44+
output:add_empty_line()
45+
end
46+
47+
local end_line = output:get_line_count()
48+
if end_line - start_line > 1 then
49+
M._add_vertical_border(output, start_line, end_line, 'OpencodeToolBorder', -1, 'OpencodeReasoningText')
50+
else
51+
M.output:add_extmark(start_line - 1, {
52+
line_hl_group = 'OpencodeReasoningText',
53+
} --[[@as OutputExtmark]])
54+
end
55+
end
56+
1757
function M._handle_permission_request(output, part)
1858
if part.state and part.state.status == 'error' and part.state.error then
1959
if part.state.error:match('rejected permission') then
@@ -431,14 +471,15 @@ function M._format_assistant_message(output, text)
431471
end
432472

433473
---@param output Output Output object to write to
434-
---@param type string Tool type (e.g., 'run', 'read', 'edit', etc.)
474+
---@param tool_type string Tool type (e.g., 'run', 'read', 'edit', etc.)
435475
---@param value string Value associated with the action (e.g., filename, command)
436-
function M._format_action(output, type, value)
437-
if not type or not value then
476+
function M._format_action(output, tool_type, value)
477+
if not tool_type or not value then
438478
return
439479
end
480+
local line = string.format('**%s** %s', tool_type, value and #value > 0 and ('`' .. value .. '`') or '')
440481

441-
output:add_line('**' .. type .. '** `' .. value .. '`')
482+
output:add_line(line)
442483
end
443484

444485
---@param output Output Output object to write to
@@ -713,16 +754,24 @@ end
713754
---@param output Output Output object to write to
714755
---@param start_line number
715756
---@param end_line number
716-
---@param hl_group string
757+
---@param hl_group string Highlight group for the border character
717758
---@param win_col number
718-
function M._add_vertical_border(output, start_line, end_line, hl_group, win_col)
759+
---@param text_hl_group? string Optional highlight group for the background/foreground of text lines
760+
function M._add_vertical_border(output, start_line, end_line, hl_group, win_col, text_hl_group)
719761
for line = start_line, end_line do
720-
output:add_extmark(line - 1, {
762+
local extmark_opts = {
721763
virt_text = { { require('opencode.ui.icons').get('border'), hl_group } },
722764
virt_text_pos = 'overlay',
723765
virt_text_win_col = win_col,
724766
virt_text_repeat_linebreak = true,
725-
} --[[@as OutputExtmark]])
767+
}
768+
769+
-- Add line highlight if text_hl_group is provided
770+
if text_hl_group then
771+
extmark_opts.line_hl_group = text_hl_group
772+
end
773+
774+
output:add_extmark(line - 1, extmark_opts --[[@as OutputExtmark]])
726775
end
727776
end
728777

@@ -762,6 +811,9 @@ function M.format_part(part, message, is_last_part)
762811
if part.type == 'text' and part.text then
763812
M._format_assistant_message(output, vim.trim(part.text))
764813
content_added = true
814+
elseif part.type == 'reasoning' and part.text then
815+
M._format_reasoning(output, part)
816+
content_added = true
765817
elseif part.type == 'tool' then
766818
M._format_tool(output, part)
767819
content_added = true

lua/opencode/ui/highlight.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ function M.setup()
3434
vim.api.nvim_set_hl(0, 'OpencodeContextSwitchOn', { link = '@label', default = true })
3535
vim.api.nvim_set_hl(0, 'OpencodePickerTime', { link = 'Comment', default = true })
3636
vim.api.nvim_set_hl(0, 'OpencodeDebugText', { link = 'Comment', default = true })
37+
vim.api.nvim_set_hl(0, 'OpencodeReasoningText', { link = 'Comment', default = true })
3738
else
3839
vim.api.nvim_set_hl(0, 'OpencodeBorder', { fg = '#616161', default = true })
3940
vim.api.nvim_set_hl(0, 'OpencodeBackground', { link = 'Normal', default = true })
@@ -64,6 +65,7 @@ function M.setup()
6465
vim.api.nvim_set_hl(0, 'OpencodeContextSwitchOn', { link = '@label', default = true })
6566
vim.api.nvim_set_hl(0, 'OpencodePickerTime', { link = 'Comment', default = true })
6667
vim.api.nvim_set_hl(0, 'OpencodeDebugText', { link = 'Comment', default = true })
68+
vim.api.nvim_set_hl(0, 'OpencodeReasoningText', { link = 'Comment', default = true })
6769
end
6870
end
6971

lua/opencode/ui/icons.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ local presets = {
2525
folder = '',
2626
attached_file = '󰌷 ',
2727
agent = '󰚩 ',
28+
reasoning = '󰧑 ',
2829
-- statuses
2930
status_on = '',
3031
status_off = '',

tests/data/reasoning.expected.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"lines":["----","","","Explain your plan, then provide the final title.","","----","","","**󰧑 Reasoning (2025-12-17 19:33:20 - 2025-12-17 19:33:21)** ","","The user is asking me to explain my plan and then provide a final title. I need to think through what they're asking for. It seems like they want me to demonstrate reasoning capabilities - showing my thought process before giving an answer. Let me break this down into steps.","","1. First, I should understand what kind of title they want - is this for a project, document, or something else?","2. Since they didn't specify the context, I should provide a general approach","3. I'll demonstrate my reasoning process transparently","4. Then I'll give them a concrete example title","","Given that this appears to be related to the OpenCode project (based on the file structure I can see), I should provide a title that reflects the nature of this AI coding assistant tool.","","Based on my reasoning above, here's my plan:","","1. Acknowledge the request for demonstrating reasoning","2. Show transparency in my thought process","3. Provide context-appropriate suggestions","","For the title, considering this is the OpenCode Neovim plugin - an AI-powered coding assistant - a good title would be:","","**\"OpenCode.nvim: AI-Powered Coding Assistant for Neovim\"**","","This title clearly identifies what the tool is (OpenCode), the platform it works with (Neovim), and its primary function (AI-powered coding assistance).","",""],"actions":[],"extmarks":[[1,1,0,{"virt_text":[["▌󰭻 ","OpencodeMessageRoleUser"],[" "],["USER","OpencodeMessageRoleUser"],["","OpencodeHint"],[" (2025-12-17 19:33:20)","OpencodeHint"],[" [msg_reason_user1]","OpencodeHint"]],"virt_text_pos":"win_col","virt_text_hide":false,"priority":10,"virt_text_repeat_linebreak":false,"right_gravity":true,"virt_text_win_col":-3,"ns_id":3}],[2,2,0,{"virt_text":[["▌","OpencodeMessageRoleUser"]],"virt_text_pos":"win_col","virt_text_hide":false,"priority":4096,"virt_text_repeat_linebreak":true,"right_gravity":true,"virt_text_win_col":-3,"ns_id":3}],[3,3,0,{"virt_text":[["▌","OpencodeMessageRoleUser"]],"virt_text_pos":"win_col","virt_text_hide":false,"priority":4096,"virt_text_repeat_linebreak":true,"right_gravity":true,"virt_text_win_col":-3,"ns_id":3}],[4,6,0,{"virt_text":[[" ","OpencodeMessageRoleAssistant"],[" "],["BUILD","OpencodeMessageRoleAssistant"],[" claude-sonnet-4","OpencodeHint"],[" (2025-12-17 19:33:20)","OpencodeHint"],[" [msg_reason_asst1]","OpencodeHint"]],"virt_text_pos":"win_col","virt_text_hide":false,"priority":10,"virt_text_repeat_linebreak":false,"right_gravity":true,"virt_text_win_col":-3,"ns_id":3}],[5,8,0,{"line_hl_group":"OpencodeReasoningText","right_gravity":true,"ns_id":3,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_hide":false,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_win_col":-1}],[6,9,0,{"line_hl_group":"OpencodeReasoningText","right_gravity":true,"ns_id":3,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_hide":false,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_win_col":-1}],[7,10,0,{"line_hl_group":"OpencodeReasoningText","right_gravity":true,"ns_id":3,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_hide":false,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_win_col":-1}],[8,11,0,{"line_hl_group":"OpencodeReasoningText","right_gravity":true,"ns_id":3,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_hide":false,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_win_col":-1}],[9,12,0,{"line_hl_group":"OpencodeReasoningText","right_gravity":true,"ns_id":3,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_hide":false,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_win_col":-1}],[10,13,0,{"line_hl_group":"OpencodeReasoningText","right_gravity":true,"ns_id":3,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_hide":false,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_win_col":-1}],[11,14,0,{"line_hl_group":"OpencodeReasoningText","right_gravity":true,"ns_id":3,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_hide":false,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_win_col":-1}],[12,15,0,{"line_hl_group":"OpencodeReasoningText","right_gravity":true,"ns_id":3,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_hide":false,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_win_col":-1}],[13,16,0,{"line_hl_group":"OpencodeReasoningText","right_gravity":true,"ns_id":3,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_hide":false,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_win_col":-1}],[14,17,0,{"line_hl_group":"OpencodeReasoningText","right_gravity":true,"ns_id":3,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_hide":false,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_win_col":-1}],[15,18,0,{"line_hl_group":"OpencodeReasoningText","right_gravity":true,"ns_id":3,"virt_text":[["▌","OpencodeToolBorder"]],"virt_text_pos":"win_col","virt_text_hide":false,"priority":4096,"virt_text_repeat_linebreak":true,"virt_text_win_col":-1}]],"timestamp":1766413340}

0 commit comments

Comments
 (0)