Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ require('opencode').setup({
['<leader>opa'] = { 'permission_accept' }, -- Accept permission request once
['<leader>opA'] = { 'permission_accept_all' }, -- Accept all (for current tool)
['<leader>opd'] = { 'permission_deny' }, -- Deny permission request once
['<leader>ott'] = { 'toggle_tool_output' }, -- Toggle tools output (diffs, cmd output, etc.)
['<leader>otr'] = { 'toggle_reasoning_output' }, -- Toggle reasoning output (thinking steps)
},
input_window = {
['<cr>'] = { 'submit_input_prompt', mode = { 'n', 'i' } }, -- Submit prompt (normal mode and insert mode)
Expand Down Expand Up @@ -191,6 +193,7 @@ require('opencode').setup({
output = {
tools = {
show_output = true, -- Show tools output [diffs, cmd output, etc.] (default: true)
show_reasoning_output = true, -- Show reasoning/thinking steps output (default: true)
},
rendering = {
markdown_debounce_ms = 250, -- Debounce time for markdown rendering on new data (default: 250ms)
Expand Down Expand Up @@ -389,7 +392,8 @@ The plugin provides the following actions that can be triggered via keymaps, com
| Navigate to next prompt in history | `<down>` | - | `require('opencode.api').next_history()` |
| Toggle input/output panes | `<tab>` | - | - |
| Swap Opencode pane left/right | `<leader>ox` | `:Opencode swap position` | `require('opencode.api').swap_position()` |
| Toggle tools output (diffs, cmd output, etc.) | - | `:Opencode toggle_tools_output` | `require('opencode.api').toggle_tools_output()` |
| Toggle tools output (diffs, cmd output, etc.) | `<leader>ott` | `:Opencode toggle_tool_output` | `require('opencode.api').toggle_tool_output()` |
| Toggle reasoning output (thinking steps) | `<leader>otr` | `:Opencode toggle_reasoning_output` | `require('opencode.api').toggle_reasoning_output()` |

---

Expand Down
16 changes: 16 additions & 0 deletions lua/opencode/api.lua
Original file line number Diff line number Diff line change
Expand Up @@ -904,10 +904,19 @@ function M.permission_deny()
end

function M.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()
end

function M.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()
end

---@type table<string, OpencodeUICommand>
M.commands = {
open = {
Expand Down Expand Up @@ -1210,6 +1219,11 @@ M.commands = {
desc = 'Toggle tool output visibility in the output window',
fn = M.toggle_tool_output,
},

toggle_reasoning_output = {
desc = 'Toggle reasoning output visibility in the output window',
fn = M.toggle_reasoning_output,
},
paste_image = {
desc = 'Paste image from clipboard and add to context',
fn = M.paste_image,
Expand All @@ -1234,6 +1248,8 @@ M.slash_commands_map = {
['/undo'] = { fn = M.undo, desc = 'Undo last action' },
['/unshare'] = { fn = M.unshare, desc = 'Unshare current session' },
['/rename'] = { fn = M.rename_session, desc = 'Rename current session' },
['/thinking'] = { fn = M.toggle_reasoning_output, desc = 'Toggle reasoning output' },
['/reasoning'] = { fn = M.toggle_reasoning_output, desc = 'Toggle reasoning output' },
}

M.legacy_command_map = {
Expand Down
3 changes: 3 additions & 0 deletions lua/opencode/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ M.defaults = {
['<leader>oPa'] = { 'permission_accept', desc = 'Accept permission' },
['<leader>oPA'] = { 'permission_accept_all', desc = 'Accept all permissions' },
['<leader>oPd'] = { 'permission_deny', desc = 'Deny permission' },
['<leader>otr'] = { 'toggle_reasoning_output', desc = 'Toggle reasoning output' },
['<leader>ott'] = { 'toggle_tool_output', desc = 'Toggle tool output' },
},
output_window = {
['<esc>'] = { 'close' },
Expand Down Expand Up @@ -118,6 +120,7 @@ M.defaults = {
},
tools = {
show_output = true,
show_reasoning_output = true,
},
always_scroll_to_bottom = false,
},
Expand Down
9 changes: 7 additions & 2 deletions lua/opencode/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@
---@field output OpencodeUIOutputConfig
---@field input { text: { wrap: boolean } }
---@field completion OpencodeCompletionConfig
---@field highlights? OpencodeHighlightConfig

---@class OpencodeHighlightConfig
---@field vertical_borders? { tool?: { fg?: string, bg?: string }, user?: { fg?: string, bg?: string }, assistant?: { fg?: string, bg?: string } }

---@class OpencodeUIOutputRenderingConfig
---@field markdown_debounce_ms number
Expand All @@ -137,7 +141,7 @@
---@field event_collapsing boolean

---@class OpencodeUIOutputConfig
---@field tools { show_output: boolean }
---@field tools { show_output: boolean, show_reasoning_output: boolean }
---@field rendering OpencodeUIOutputRenderingConfig
---@field always_scroll_to_bottom boolean

Expand Down Expand Up @@ -382,7 +386,7 @@
---@field value string|nil

---@class OpencodeMessagePart
---@field type 'text'|'file'|'agent'|'tool'|'step-start'|'patch'|string
---@field type 'text'|'file'|'agent'|'tool'|'step-start'|'patch'|'reasoning'|string
---@field id string|nil Unique identifier for tool use parts
---@field text string|nil
---@field tool string|nil Name of the tool being used
Expand All @@ -399,6 +403,7 @@
---@field callID string|nil Call identifier (used for tools)
---@field hash string|nil Hash identifier for patch parts
---@field files string[]|nil List of file paths for patch parts
---@field time { start: number, end?: number }|nil Timestamps for the part

---@class OpencodeModelModalities
---@field input ('text'|'image'|'audio'|'video')[] Supported input modalities
Expand Down
68 changes: 60 additions & 8 deletions lua/opencode/ui/formatter.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,46 @@ M.separator = {
'',
}

---@param output Output
---@param part OpencodeMessagePart
function M._format_reasoning(output, part)
local text = vim.trim(part.text or '')
if text == '' then
return
end

local start_line = output:get_line_count() + 1

local title = 'Reasoning'
local time = part.time
if time and type(time) == 'table' and time.start then
local start_text = util.format_time(time.start) or ''
local end_text = (time['end'] and util.format_time(time['end'])) or nil
if end_text and end_text ~= '' then
title = string.format('%s (%s - %s)', title, start_text, end_text)
elseif start_text ~= '' then
title = string.format('%s (%s)', title, start_text)
end
end

M._format_action(output, icons.get('reasoning') .. ' ' .. title, '')

if config.ui.output.tools.show_reasoning_output then
output:add_empty_line()
output:add_lines(vim.split(text, '\n'))
output:add_empty_line()
end

local end_line = output:get_line_count()
if end_line - start_line > 1 then
M._add_vertical_border(output, start_line, end_line, 'OpencodeToolBorder', -1, 'OpencodeReasoningText')
else
output:add_extmark(start_line - 1, {
line_hl_group = 'OpencodeReasoningText',
} --[[@as OutputExtmark]])
end
end

function M._handle_permission_request(output, part)
if part.state and part.state.status == 'error' and part.state.error then
if part.state.error:match('rejected permission') then
Expand Down Expand Up @@ -431,14 +471,15 @@ function M._format_assistant_message(output, text)
end

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

output:add_line('**' .. type .. '** `' .. value .. '`')
output:add_line(line)
end

---@param output Output Output object to write to
Expand Down Expand Up @@ -713,16 +754,24 @@ end
---@param output Output Output object to write to
---@param start_line number
---@param end_line number
---@param hl_group string
---@param hl_group string Highlight group for the border character
---@param win_col number
function M._add_vertical_border(output, start_line, end_line, hl_group, win_col)
---@param text_hl_group? string Optional highlight group for the background/foreground of text lines
function M._add_vertical_border(output, start_line, end_line, hl_group, win_col, text_hl_group)
for line = start_line, end_line do
output:add_extmark(line - 1, {
local extmark_opts = {
virt_text = { { require('opencode.ui.icons').get('border'), hl_group } },
virt_text_pos = 'overlay',
virt_text_win_col = win_col,
virt_text_repeat_linebreak = true,
} --[[@as OutputExtmark]])
}

-- Add line highlight if text_hl_group is provided
if text_hl_group then
extmark_opts.line_hl_group = text_hl_group
end

output:add_extmark(line - 1, extmark_opts --[[@as OutputExtmark]])
end
end

Expand Down Expand Up @@ -762,6 +811,9 @@ function M.format_part(part, message, is_last_part)
if part.type == 'text' and part.text then
M._format_assistant_message(output, vim.trim(part.text))
content_added = true
elseif part.type == 'reasoning' and part.text then
M._format_reasoning(output, part)
content_added = true
elseif part.type == 'tool' then
M._format_tool(output, part)
content_added = true
Expand Down
2 changes: 2 additions & 0 deletions lua/opencode/ui/highlight.lua
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ function M.setup()
vim.api.nvim_set_hl(0, 'OpencodeContextSwitchOn', { link = '@label', default = true })
vim.api.nvim_set_hl(0, 'OpencodePickerTime', { link = 'Comment', default = true })
vim.api.nvim_set_hl(0, 'OpencodeDebugText', { link = 'Comment', default = true })
vim.api.nvim_set_hl(0, 'OpencodeReasoningText', { link = 'Comment', default = true })
else
vim.api.nvim_set_hl(0, 'OpencodeBorder', { fg = '#616161', default = true })
vim.api.nvim_set_hl(0, 'OpencodeBackground', { link = 'Normal', default = true })
Expand Down Expand Up @@ -64,6 +65,7 @@ function M.setup()
vim.api.nvim_set_hl(0, 'OpencodeContextSwitchOn', { link = '@label', default = true })
vim.api.nvim_set_hl(0, 'OpencodePickerTime', { link = 'Comment', default = true })
vim.api.nvim_set_hl(0, 'OpencodeDebugText', { link = 'Comment', default = true })
vim.api.nvim_set_hl(0, 'OpencodeReasoningText', { link = 'Comment', default = true })
end
end

Expand Down
1 change: 1 addition & 0 deletions lua/opencode/ui/icons.lua
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ local presets = {
folder = ' ',
attached_file = '󰌷 ',
agent = '󰚩 ',
reasoning = '󰧑 ',
-- statuses
status_on = ' ',
status_off = ' ',
Expand Down
1 change: 1 addition & 0 deletions tests/data/reasoning.expected.json
Original file line number Diff line number Diff line change
@@ -0,0 +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}
Loading