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
54 changes: 46 additions & 8 deletions lua/opencode/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@
---@field diff_close string
---@field diff_revert_all_last_prompt string
---@field diff_revert_this_last_prompt string
---@field diff_revert_all string
---@field diff_revert_this string
---@field diff_restore_snapshot_file string
---@field diff_restore_snapshot_all string
---@field open_configuration_file string
---@field swap_position string # Swap Opencode pane left/right

---@class OpencodeKeymapWindow
Expand All @@ -72,12 +77,9 @@
---@field toggle_pane string
---@field prev_prompt_history string
---@field next_prompt_history string
---@field focus_input string
---@field debug_message string
---@field debug_session string
---@field debug_output string
---@field switch_mode string
---@field select_child_session string
---@field focus_input string
---@field select_child_session string\n---@field debug_message string\n---@field debug_output string\n---@field debug_session string
---@class OpencodeKeymap
---@field global OpencodeKeymapGlobal
---@field window OpencodeKeymapWindow
Expand Down Expand Up @@ -109,17 +111,52 @@

---@class OpencodeContextConfig
---@field enabled boolean
---@field plugin_versions { enabled: boolean, limit: number }
---@field cursor_data { enabled: boolean }
---@field diagnostics { info: boolean, warning: boolean, error: boolean }
---@field current_file { enabled: boolean }
---@field current_file { enabled: boolean, show_full_path: boolean }
---@field selection { enabled: boolean }
---@field marks { enabled: boolean, limit: number }
---@field jumplist { enabled: boolean, limit: number }
---@field recent_buffers { enabled: boolean, limit: number, symbols_only: boolean }
---@field undo_history { enabled: boolean, limit: number }
---@field windows_tabs { enabled: boolean }
---@field highlights { enabled: boolean }
---@field session_info { enabled: boolean }
---@field registers { enabled: boolean, include: string[] }
---@field command_history { enabled: boolean, limit: number }
---@field search_history { enabled: boolean, limit: number }
---@field debug_data { enabled: boolean }
---@field lsp_context { enabled: boolean, diagnostics_limit: number, code_actions: boolean }
---@field git_info { enabled: boolean, diff_limit: number, changes_limit: number }
---@field fold_info { enabled: boolean }
---@field cursor_surrounding { enabled: boolean, lines_above: number, lines_below: number }
---@field quickfix_loclist { enabled: boolean, limit: number }
---@field macros { enabled: boolean, register: string }
---@field terminal_buffers { enabled: boolean }
---@field session_duration { enabled: boolean }

---@class OpencodeDebugConfig
---@field enabled boolean

--- @class OpencodeProviders
--- @field [string] string[]

---@class OpencodeConfigModule
---@field defaults OpencodeConfig
---@field values OpencodeConfig
---@field setup fun(opts?: OpencodeConfig): nil
---@overload fun(key: nil): OpencodeConfig
---@overload fun(key: "preferred_picker"): 'mini.pick' | 'telescope' | 'fzf' | 'snacks' | nil
---@overload fun(key: "preferred_completion"): 'blink' | 'nvim-cmp' | 'vim_complete' | nil
---@overload fun(key: "default_mode"): 'build' | 'plan'
---@overload fun(key: "default_global_keymaps"): boolean
---@overload fun(key: "keymap"): OpencodeKeymap
---@overload fun(key: "ui"): OpencodeUIConfig
---@overload fun(key: "providers"): OpencodeProviders
---@overload fun(key: "context"): OpencodeContextConfig
---@overload fun(key: "debug"): OpencodeDebugConfig

---@class OpencodeConfig
---@field preferred_picker 'telescope' | 'fzf' | 'mini.pick' | 'snacks' | nil
---@field preferred_completion 'blink' | 'nvim-cmp' | 'vim_complete' | nil -- Preferred completion strategy for mentons and commands
Expand Down Expand Up @@ -230,7 +267,7 @@
---@field msg_idx number|nil Message index in session
---@field part_idx number|nil Part index in message
---@field role 'user'|'assistant'|'system'|nil Message role
---@field type 'text'|'tool'|'header'|nil Message part type
---@field type 'text'|'tool'|'header'|'patch'|'step-start'|nil Message part type
---@field snapshot? string|nil snapshot commit hash

---@class OutputAction
Expand All @@ -241,7 +278,7 @@
---@field display_line number Line number to display the action
---@field range? { from: number, to: number } Optional range for the action

---@alias OutputExtmark vim.api.keyset.set_extmark
---@alias OutputExtmark vim.api.keyset.set_extmark|fun():vim.api.keyset.set_extmark

---@class Message
---@field id string Unique message identifier
Expand All @@ -256,6 +293,7 @@
---@field providerID string Provider identifier
---@field role 'user'|'assistant'|'system' Role of the message sender
---@field system_role string|nil Role defined in system messages
---@field mode string|nil Agent or mode identifier
---@field error table

---@class RestorePoint
Expand Down
69 changes: 57 additions & 12 deletions lua/opencode/ui/session_formatter.lua
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ function M.format_session(session)
end

if session.revert and session.revert.messageID == msg.id then
---@type {messages: number, tool_calls: number, files: table<string, {additions: number, deletions: number}>}
local revert_stats = M._calculate_revert_stats(state.messages, i, session.revert)
M._format_revert_message(revert_stats)
break
Expand All @@ -61,7 +62,7 @@ function M.format_session(session)
M.output:add_metadata(M._current)

if part.type == 'text' and part.text then
if msg.role == 'user' and not part.synthetic == true then
if msg.role == 'user' and part.synthetic ~= true then
state.last_user_message = msg
M._format_user_message(vim.trim(part.text), msg)
elseif msg.role == 'assistant' then
Expand All @@ -85,12 +86,18 @@ function M.format_session(session)
end

---@param line number Buffer line number
---@return {message: Message, part: MessagePart, type: string, msg_idx: number, part_idx: number}|nil
---@return {message: Message, part: MessagePart, msg_idx: number, part_idx: number}|nil
function M.get_message_at_line(line)
local metadata = M.output:get_nearest_metadata(line)
if metadata and metadata.msg_idx and metadata.part_idx then
local msg = state.messages[metadata.msg_idx]
local msg = state.messages and state.messages[metadata.msg_idx]
if not msg or not msg.parts then
return nil
end
local part = msg.parts[metadata.part_idx]
if not part then
return nil
end
return {
message = msg,
part = part,
Expand All @@ -109,7 +116,7 @@ end
---@param messages Message[] All messages in the session
---@param revert_index number Index of the message where revert occurred
---@param revert_info SessionRevertInfo Revert information
---@return {messages: number, tool_calls: number, files: {additions: number, deletions: number}[]}
---@return {messages: number, tool_calls: number, files: {additions: number, deletions: number}}
function M._calculate_revert_stats(messages, revert_index, revert_info)
local stats = {
messages = 0,
Expand Down Expand Up @@ -206,23 +213,32 @@ function M._format_patch(part)
local restore_points = snapshot.get_restore_points_by_parent(part.hash)
M.output:add_empty_line()
M._format_action(icons.get('snapshot') .. ' **Created Snapshot**', vim.trim(part.hash:sub(1, 8)))
local snapshot_header_line = M.output:get_line_count()

-- Anchor all snapshot-level actions to the snapshot header line
M.output:add_action({
text = '[R]evert file',
type = 'diff_revert_selected_file',
args = { part.hash },
key = 'R',
display_line = snapshot_header_line,
range = { from = snapshot_header_line, to = snapshot_header_line },
})
M.output:add_action({
text = 'Revert [A]ll',
type = 'diff_revert_all',
args = { part.hash },
key = 'A',
display_line = snapshot_header_line,
range = { from = snapshot_header_line, to = snapshot_header_line },
})
M.output:add_action({
text = '[D]iff',
type = 'diff_open',
args = { part.hash },
key = 'D',
display_line = snapshot_header_line,
range = { from = snapshot_header_line, to = snapshot_header_line },
})

if #restore_points > 0 then
Expand All @@ -235,17 +251,22 @@ function M._format_patch(part)
util.time_ago(restore_point.created_at)
)
)
local restore_line = M.output:get_line_count()
M.output:add_action({
text = 'Restore [A]ll',
type = 'diff_restore_snapshot_all',
args = { part.hash },
key = 'A',
display_line = restore_line,
range = { from = restore_line, to = restore_line },
})
M.output:add_action({
text = '[R]estore file',
type = 'diff_restore_snapshot_file',
args = { part.hash },
key = 'R',
display_line = restore_line,
range = { from = restore_line, to = restore_line },
})
end
end
Expand All @@ -258,6 +279,7 @@ function M._format_error(message)
end

---@param message Message
---@param msg_idx number Message index in the session
function M._format_message_header(message, msg_idx)
local role = message.role or 'unknown'
local icon = message.role == 'user' and icons.get('header_user') or icons.get('header_assistant')
Expand All @@ -270,14 +292,34 @@ function M._format_message_header(message, msg_idx)

M.output:add_empty_line()
M.output:add_metadata({ msg_idx = msg_idx, part_idx = 1, role = role, type = 'header' })

local display_name
if role == 'assistant' then
local mode = message.mode
if mode and mode ~= '' then
display_name = mode:upper()
else
-- For the most recent assistant message, show current_mode if mode is missing
-- This handles new messages that haven't been stamped yet
local is_last_message = msg_idx == #state.messages
if is_last_message and state.current_mode and state.current_mode ~= '' then
display_name = state.current_mode:upper()
else
display_name = 'ASSISTANT'
end
end
else
display_name = role:upper()
end

M.output:add_extmark(M.output:get_line_count(), {
virt_text = {
{ icon, role_hl },
{ ' ' },
{ role:upper(), role_hl },
{ display_name, role_hl },
{ model_text, 'OpencodeHint' },
{ time_text, 'OpenCodeHint' },
{ debug_text, 'OpenCodeHint' },
{ time_text, 'OpencodeHint' },
{ debug_text, 'OpencodeHint' },
},
virt_text_win_col = -3,
priority = 10,
Expand All @@ -287,9 +329,11 @@ function M._format_message_header(message, msg_idx)
end

---@param callout string Callout type (e.g., 'ERROR', 'TODO')
---@param text string Callout text content
---@param title? string Optional title for the callout
function M._format_callout(callout, text, title)
title = title and title .. ' ' or ''
local win_width = vim.api.nvim_win_get_width(state.windows.output_win)
local win_width = (state.windows and state.windows.output_win and vim.api.nvim_win_is_valid(state.windows.output_win)) and vim.api.nvim_win_get_width(state.windows.output_win) or config.ui.window_width or 80
if #text > win_width - 4 then
local ok, substituted = pcall(vim.fn.substitute, text, '\v(.{' .. (win_width - 8) .. '})', '\1\n', 'g')
text = ok and substituted or text
Expand Down Expand Up @@ -345,6 +389,7 @@ function M._format_assistant_message(text)
end

---@param 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(type, value)
if not type or not value then
return
Expand Down Expand Up @@ -473,9 +518,9 @@ function M._format_tool(part)
end

local start_line = M.output:get_line_count() + 1
local input = part.state and part.state.input or {}
local metadata = part.state.metadata or {}
local output = part.state and part.state.output or ''
local input = (part.state and part.state.input) or {}
local metadata = (part.state and part.state.metadata) or {}
local output = (part.state and part.state.output) or ''

if tool == 'bash' then
M._format_bash_tool(input --[[@as BashToolInput]], metadata --[[@as BashToolMetadata]])
Expand Down Expand Up @@ -569,7 +614,7 @@ function M._format_diff(code, file_type)
return {
end_col = 0,
end_row = line_idx,
virt_text = { { first_char, { hl_group } } },
virt_text = { { first_char, hl_group } },
hl_group = hl_group,
hl_eol = true,
priority = 5000,
Expand Down