diff --git a/lua/opencode/commands/handlers/session.lua b/lua/opencode/commands/handlers/session.lua index 9dc5d172..a2150c1c 100644 --- a/lua/opencode/commands/handlers/session.lua +++ b/lua/opencode/commands/handlers/session.lua @@ -10,7 +10,8 @@ local M = { actions = {}, } -local session_subcommands = { 'new', 'select', 'navigate', 'compact', 'share', 'unshare', 'agents_init', 'rename', 'toggle_lock' } +local session_subcommands = + { 'new', 'select', 'navigate', 'compact', 'share', 'unshare', 'agents_init', 'rename', 'toggle_lock' } ---@param message string local function invalid_arguments(message) @@ -522,15 +523,6 @@ function M.actions.timeline() end) end ----@param source? string -function M.actions.message_actions(source) - local message_actions = require('opencode.ui.message_actions') - if source == 'mouse' then - return message_actions.open_from_mouse() - end - return message_actions.open_at_cursor() -end - ---@param message_id? string function M.actions.fork_session(message_id) return with_active_session('No active session to fork', function(state_obj) @@ -677,12 +669,6 @@ M.command_defs = { desc = 'Open timeline picker to navigate/undo/redo/fork to message', execute = M.actions.timeline, }, - message_actions = { - desc = 'Open message actions', - execute = function(args) - return M.actions.message_actions(args[1]) - end, - }, } return M diff --git a/lua/opencode/config.lua b/lua/opencode/config.lua index 416f5d0a..cc1b4b46 100644 --- a/lua/opencode/config.lua +++ b/lua/opencode/config.lua @@ -76,8 +76,6 @@ M.defaults = { ['gr'] = { 'references', desc = 'Browse code references' }, ['gf'] = { 'jump_to_file', desc = 'Jump to file at cursor' }, [''] = { 'jump_to_target_at_cursor', desc = 'Jump to target at cursor' }, - [''] = { 'message_actions', { 'mouse' }, desc = 'Open message actions' }, - ['o'] = { 'message_actions', desc = 'Open message actions' }, [''] = { 'toggle_input', mode = { 'n' }, desc = 'Toggle input window' }, [''] = { 'cycle_variant', mode = { 'n' }, desc = 'Cycle model variants' }, ['oS'] = { 'navigate_session_tree', { 'child', 'picker' }, desc = 'Select child session' }, diff --git a/lua/opencode/types.lua b/lua/opencode/types.lua index 62783ea9..7c01caec 100644 --- a/lua/opencode/types.lua +++ b/lua/opencode/types.lua @@ -148,7 +148,6 @@ ---@class OpencodeKeymapEntry ---@field [1] string # Function name ----@field [2]? string|string[] # Preset command arguments ---@field mode? string|string[] # Mode(s) for the keymap ---@field desc? string # Keymap description ---@field defer_to_completion? boolean # Whether to defer the keymap when completion menu is open diff --git a/lua/opencode/ui/dialog.lua b/lua/opencode/ui/dialog.lua index 7085e634..78cfc238 100644 --- a/lua/opencode/ui/dialog.lua +++ b/lua/opencode/ui/dialog.lua @@ -10,8 +10,6 @@ ---@field keymaps? DialogKeymaps Custom keymap configuration ---@field namespace_prefix? string Prefix for vim.on_key namespace (default: 'opencode_dialog') ---@field hide_input? boolean Whether to hide the input window when dialog is active (default: true) ----@field render_part_id? string Rendered part ID used to resolve mouse clicks against output lines ----@field mouse_select? boolean Whether Dialog should register for option selection (default: false) ---@class DialogKeymaps ---@field up? string[] Keys for navigating up (default: {'k', ''}) @@ -19,7 +17,7 @@ ---@field left? string[] Keys for navigating left between groups ---@field right? string[] Keys for navigating right between groups ---@field select? string Key for selecting current option (default: '') ----@field dismiss? string|string[] Keys for dismissing dialog (default: {'', ''}) +---@field dismiss? string Key for dismissing dialog (default: '') ---@field number_shortcuts? boolean Enable 1-9 number shortcuts (default: true) ---@class Dialog @@ -29,38 +27,9 @@ ---@field private _selected_index integer Currently selected option index ---@field private _active boolean Whether dialog is currently active ---@field private _group_index integer Currently selected group index ----@field private _option_local_lines integer[] 0-based output-local lines for each option local Dialog = {} Dialog.__index = Dialog ----@param keymap string|string[]|nil ----@return string[] -local function keymap_list(keymap) - local keymaps = {} - if type(keymap) == 'string' then - if keymap ~= '' then - table.insert(keymaps, keymap) - end - elseif type(keymap) == 'table' then - for _, key in ipairs(keymap) do - if key and key ~= '' then - table.insert(keymaps, key) - end - end - end - return keymaps -end - ----@param keymaps string[] ----@return string -local function keymap_legend(keymaps) - local labels = {} - for _, key in ipairs(keymaps) do - table.insert(labels, '`' .. key .. '`') - end - return table.concat(labels, ' or ') -end - ---Create a new dialog instance ---@param config DialogConfig Dialog configuration ---@return Dialog @@ -74,7 +43,7 @@ function Dialog.new(config) left = {}, right = {}, select = '', - dismiss = { '', '' }, + dismiss = '', number_shortcuts = true, } @@ -85,7 +54,6 @@ function Dialog.new(config) return true end, hide_input = true, - mouse_select = false, } --[[@as DialogConfig]], config) self._keymaps = {} @@ -93,7 +61,6 @@ function Dialog.new(config) self._selected_index = 1 self._group_index = 1 self._active = false - self._option_local_lines = {} return self end @@ -281,9 +248,8 @@ function Dialog:format_legend(output, options) local line = output:add_line(select_text) end - local dismiss_keymaps = keymap_list(keymaps.dismiss) - if #dismiss_keymaps > 0 then - local line = output:add_line('Close: ' .. keymap_legend(dismiss_keymaps)) + if keymaps.dismiss and keymaps.dismiss ~= '' then + local line = output:add_line('Close: ``') end else local message = options.unfocused_message or 'Focus Opencode window to interact' @@ -302,7 +268,6 @@ end --- - progress?: string - Progress indicator (e.g., "(1/3)") --- - content?: string[] - Array of lines to render before options --- - render_content?: function(output: Output) - Custom function to render content before options ---- - hide_legend?: boolean - Whether to hide movement/select/dismiss instructions function Dialog:format_dialog(output, config) if not self._active then return @@ -336,10 +301,9 @@ function Dialog:format_dialog(output, config) self:format_options(output, config.options or {}) - if not config.hide_legend then - output:add_line('') - self:format_legend(output, { unfocused_message = config.unfocused_message }) - end + output:add_line('') + + self:format_legend(output, { unfocused_message = config.unfocused_message }) local end_line = output:get_line_count() @@ -358,8 +322,6 @@ end ---@param output Output Output object to write to ---@param options table[] Array of option objects with {label: string, description?: string} function Dialog:format_options(output, options) - self._option_local_lines = {} - for i, option in ipairs(options) do local label = option.label if option.description and option.description ~= '' then @@ -377,12 +339,11 @@ function Dialog:format_options(output, options) -- add_line returns a 1-based line index; Output extmarks use 0-based -- keys, so subtract 1 to get the correct extmark key. local added_idx = output:add_line(line_text) - local option_local_line = added_idx - 1 - self._option_local_lines[i] = option_local_line if is_selected then - output:add_extmark(option_local_line, { line_hl_group = 'OpencodeDialogOptionHover' } --[[@as OutputExtmark]]) - output:add_extmark(option_local_line, { + local extmark_idx = added_idx - 1 + output:add_extmark(extmark_idx, { line_hl_group = 'OpencodeDialogOptionHover' } --[[@as OutputExtmark]]) + output:add_extmark(extmark_idx, { start_col = 2, virt_text = { { '› ', 'OpencodeDialogOptionHover' } }, virt_text_pos = 'overlay', @@ -391,58 +352,6 @@ function Dialog:format_options(output, options) end end ----@return integer|nil -function Dialog:_mouse_option_index() - local render_part_id = self._config.render_part_id - if not render_part_id or render_part_id == '' then - return nil - end - - local mouse = vim.fn.getmousepos() - local winid = mouse and mouse.winid - if not winid or winid == 0 or not vim.api.nvim_win_is_valid(winid) then - return nil - end - - local buf = self._config.buffer - if not buf or vim.api.nvim_win_get_buf(winid) ~= buf then - return nil - end - - local rendered_part = require('opencode.ui.renderer.ctx').render_state:get_part(render_part_id) - if not rendered_part or rendered_part.line_start == nil then - return nil - end - - local mouse_line = mouse.line - if not mouse_line or mouse_line <= 0 then - return nil - end - - local clicked_output_line = mouse_line - 1 - for option_index, option_local_line in ipairs(self._option_local_lines) do - if clicked_output_line == rendered_part.line_start + option_local_line then - return option_index - end - end -end - ----@return boolean selected -function Dialog:select_mouse_option() - if not self._active or not self._config.check_focused() then - return false - end - - local option_index = self:_mouse_option_index() - if not option_index then - return false - end - - self._selected_index = option_index - self._config.on_select(option_index) - return true -end - ---Set up buffer-scoped keymaps function Dialog:_setup_keymaps() self:_clear_keymaps() @@ -541,34 +450,18 @@ function Dialog:_setup_keymaps() table.insert(self._keymaps, keymaps.select) end - if self._config.mouse_select ~= false and self._config.render_part_id and self._config.render_part_id ~= '' then + if keymaps.dismiss and keymaps.dismiss ~= '' then vim.keymap.set( 'n', - '', + keymaps.dismiss, function() - self:select_mouse_option() + self:dismiss() end, vim.tbl_extend('force', keymap_opts, { - desc = 'Dialog: select clicked option', + desc = 'Dialog: dismiss', }) ) - table.insert(self._keymaps, '') - end - - for _, key in ipairs(keymap_list(keymaps.dismiss)) do - if key and key ~= '' then - vim.keymap.set( - 'n', - key, - function() - self:dismiss() - end, - vim.tbl_extend('force', keymap_opts, { - desc = 'Dialog: dismiss', - }) - ) - table.insert(self._keymaps, key) - end + table.insert(self._keymaps, keymaps.dismiss) end if keymaps.number_shortcuts then diff --git a/lua/opencode/ui/formatter.lua b/lua/opencode/ui/formatter.lua index bf93c283..1a20f821 100644 --- a/lua/opencode/ui/formatter.lua +++ b/lua/opencode/ui/formatter.lua @@ -719,11 +719,7 @@ function M.format_part(part, message, is_last_part, get_child_parts) local role = message.info.role - if part.type == 'message-actions-display' then - local message_actions = require('opencode.ui.message_actions') - message_actions.format_display(output) - content_added = true - elseif role == 'user' then + if role == 'user' then if is_compaction_part(part) then format_compaction_divider(output) content_added = true diff --git a/lua/opencode/ui/message_actions.lua b/lua/opencode/ui/message_actions.lua deleted file mode 100644 index 453d249e..00000000 --- a/lua/opencode/ui/message_actions.lua +++ /dev/null @@ -1,316 +0,0 @@ -local Dialog = require('opencode.ui.dialog') -local state = require('opencode.state') -local ctx = require('opencode.ui.renderer.ctx') - -local M = {} - -local DISPLAY_PART_ID_PREFIX = 'message-actions-display-part:' - -local ACTIONS = { - { - label = 'Revert', - run = function(message_id) - require('opencode.api').undo(message_id) - end, - }, - { - label = 'Copy', - run = function(_, message) - local text = M.collect_user_text(message) - if text == '' then - vim.notify('No message text to copy', vim.log.levels.WARN) - return - end - vim.fn.setreg('+', text) - end, - }, - { - label = 'Fork', - run = function(message_id) - require('opencode.api').fork_session(message_id) - end, - }, -} - -M._target_message = nil -M._dialog = nil -M._display_part_id = nil - ----@param message_id string ----@return string -local function display_part_id(message_id) - return DISPLAY_PART_ID_PREFIX .. message_id -end - ----@return integer|nil output_win ----@return integer|nil output_buf -local function output_window() - local windows = state.windows - local output_win = windows and windows.output_win - local output_buf = windows and windows.output_buf - - if not output_win or not output_buf then - return nil, nil - end - if not vim.api.nvim_win_is_valid(output_win) or not vim.api.nvim_buf_is_valid(output_buf) then - return nil, nil - end - if vim.api.nvim_win_get_buf(output_win) ~= output_buf then - return nil, nil - end - - return output_win, output_buf -end - ----@param output_win integer ----@param output_buf integer ----@param mouse table -local function move_cursor_to_mouse(output_win, output_buf, mouse) - local line_count = vim.api.nvim_buf_line_count(output_buf) - local line = math.max(1, math.min(mouse.line or 1, line_count)) - local col = math.max(0, (mouse.column or 1) - 1) - pcall(vim.api.nvim_win_set_cursor, output_win, { line, col }) -end - ----@return boolean -local function can_open_message_actions() - local question_window = require('opencode.ui.question_window') - local permission_window = require('opencode.ui.permission_window') - - if question_window.has_question() or permission_window.has_permissions() then - vim.notify('Finish the active dialog first', vim.log.levels.WARN) - return false - end - - return true -end - ----@return boolean handled -local function select_active_dialog_from_mouse() - local question_window = require('opencode.ui.question_window') - if question_window.has_question() then - question_window.select_mouse_option() - return true - end - - local permission_window = require('opencode.ui.permission_window') - if permission_window.has_permissions() then - permission_window.select_mouse_option() - return true - end - - return false -end - -local function render_display() - local target_id = M._target_message and M._target_message.info and M._target_message.info.id or nil - require('opencode.ui.renderer.events').render_message_actions_display( - M._dialog ~= nil and target_id or nil, - M._display_part_id - ) -end - ----@param message OpencodeMessage|nil ----@return boolean -function M.is_actionable_user_message(message) - if not message or not message.info or message.info.role ~= 'user' then - return false - end - - local message_id = message.info.id - if not message_id or message_id == '' then - return false - end - - local parts = message.parts or {} - if #parts == 0 then - return true - end - - for _, part in ipairs(parts) do - if part.synthetic ~= true then - return true - end - end - - return false -end - ----@param message OpencodeMessage|nil ----@return string -function M.collect_user_text(message) - local chunks = {} - - for _, part in ipairs((message and message.parts) or {}) do - if part.type == 'text' and part.synthetic ~= true and type(part.text) == 'string' and vim.trim(part.text) ~= '' then - chunks[#chunks + 1] = part.text - end - end - - return table.concat(chunks, '\n\n') -end - ----@param line integer ----@return OpencodeMessage|nil -local function actionable_message_at_line(line) - local rendered_message = ctx.render_state:get_message_at_line(line) - if not rendered_message then - local rendered_part = ctx.render_state:get_part_at_line(line) - if rendered_part and rendered_part.part and rendered_part.part.type ~= 'message-actions-display' then - rendered_message = ctx.render_state:get_message(rendered_part.message_id) - end - end - - local message = rendered_message and rendered_message.message - if not M.is_actionable_user_message(message) then - return nil - end - return message -end - ----@param message OpencodeMessage ----@param output_buf integer -local function open_for_message(message, output_buf) - if not can_open_message_actions() then - return - end - - M.clear() - M._target_message = message - M._display_part_id = display_part_id(message.info.id) - - local function is_active_target() - local ui = require('opencode.ui.ui') - return ui.is_opencode_focused() and M._target_message ~= nil - end - - M._dialog = Dialog.new({ - buffer = output_buf, - render_part_id = M._display_part_id, - mouse_select = false, - namespace_prefix = 'opencode_message_actions', - check_focused = is_active_target, - get_option_count = function() - return M._target_message and #ACTIONS or 0 - end, - on_navigate = render_display, - on_dismiss = M.clear, - on_select = function(index) - if not M._target_message then - return - end - - local action = ACTIONS[index] - if not action then - return - end - - local selected_message = M._target_message - local message_id = selected_message.info.id - M.clear() - action.run(message_id, selected_message) - end, - }) - - M._dialog:setup() - render_display() -end - -function M.open_at_cursor() - local output_win, output_buf = output_window() - if not output_win or not output_buf then - return - end - - local cursor_line = vim.api.nvim_win_get_cursor(output_win)[1] - 1 - local message = actionable_message_at_line(cursor_line) - if not message then - return - end - - open_for_message(message, output_buf) -end - -local function pass_through_left_mouse() - vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('', true, true, true), 'n', false) -end - -function M.open_from_mouse() - local output_win, output_buf = output_window() - if not output_win or not output_buf then - pass_through_left_mouse() - return - end - - local mouse = vim.fn.getmousepos() - if not mouse or mouse.winid ~= output_win then - pass_through_left_mouse() - return - end - if not vim.api.nvim_win_is_valid(mouse.winid) or vim.api.nvim_win_get_buf(mouse.winid) ~= output_buf then - pass_through_left_mouse() - return - end - if not mouse.line or mouse.line <= 0 then - return - end - - move_cursor_to_mouse(output_win, output_buf, mouse) - - if select_active_dialog_from_mouse() then - return - end - - if M._dialog then - if M._dialog:select_mouse_option() then - return - end - M.clear() - return - end - - local message = actionable_message_at_line(mouse.line - 1) - if not message then - return - end - - open_for_message(message, output_buf) -end - -function M.clear() - if M._dialog then - M._dialog:teardown() - M._dialog = nil - end - local display_id = M._display_part_id - M._target_message = nil - M._display_part_id = nil - require('opencode.ui.renderer.events').render_message_actions_display(nil, display_id) -end - -function M.teardown() - if M._dialog then - M._dialog:teardown() - end - M._dialog = nil - M._target_message = nil - M._display_part_id = nil -end - ----@param output Output -function M.format_display(output) - if not M._target_message or not M._dialog then - return - end - - M._dialog:format_dialog(output, { - title = 'Message Actions', - title_hl = 'OpencodeQuestionTitle', - border_hl = 'OpencodeQuestionBorder', - options = ACTIONS, - hide_legend = true, - unfocused_message = 'Focus Opencode window to choose message action', - }) -end - -return M diff --git a/lua/opencode/ui/permission_window.lua b/lua/opencode/ui/permission_window.lua index e5364950..70f2f2bb 100644 --- a/lua/opencode/ui/permission_window.lua +++ b/lua/opencode/ui/permission_window.lua @@ -322,8 +322,6 @@ function M._setup_dialog() get_option_count = get_option_count, check_focused = check_focused, namespace_prefix = 'opencode_permission', - render_part_id = 'permission-display-part', - mouse_select = false, keymaps = { dismiss = '', -- Disable dismiss keymap and legend }, @@ -343,11 +341,6 @@ function M._clear_dialog() end end ----@return boolean selected -function M.select_mouse_option() - return M._dialog ~= nil and M._dialog:select_mouse_option() or false -end - ---Query the server for pending permissions and restore any that belong ---to the active session. Mirrors question_window.restore_pending_question. ---@param session_id string|nil diff --git a/lua/opencode/ui/question_window.lua b/lua/opencode/ui/question_window.lua index 7b09f0a4..2727ab48 100644 --- a/lua/opencode/ui/question_window.lua +++ b/lua/opencode/ui/question_window.lua @@ -209,11 +209,11 @@ function M.restore_pending_question(session_id) for _, request in ipairs(requests) do if - request - and request.questions - and #request.questions > 0 - and session_scope.belongs_to_active_session(request) - and not is_resolved_question_request(request) + request + and request.questions + and #request.questions > 0 + and session_scope.belongs_to_active_session(request) + and not is_resolved_question_request(request) then if M.matches_active_question(request) then return @@ -504,10 +504,7 @@ function M._setup_dialog() end, check_focused = check_focused, namespace_prefix = 'opencode_question', - render_part_id = 'question-display-part', - mouse_select = false, keymaps = { - dismiss = '', left = { 'h', '' }, right = { 'l', '' }, }, @@ -517,11 +514,6 @@ function M._setup_dialog() M._dialog:setup() end ----@return boolean selected -function M.select_mouse_option() - return M._dialog ~= nil and M._dialog:select_mouse_option() or false -end - ---Tear down the active question dialog, if any. function M._clear_dialog() if M._dialog then diff --git a/lua/opencode/ui/renderer.lua b/lua/opencode/ui/renderer.lua index 7b4aee04..986f66e1 100644 --- a/lua/opencode/ui/renderer.lua +++ b/lua/opencode/ui/renderer.lua @@ -283,7 +283,6 @@ end ---Reset all renderer state and clear the output buffer function M.reset() - require('opencode.ui.message_actions').teardown() ctx:reset() output_window.clear() permission_window.clear_all() diff --git a/lua/opencode/ui/renderer/buffer.lua b/lua/opencode/ui/renderer/buffer.lua index e5451bbf..67f40618 100644 --- a/lua/opencode/ui/renderer/buffer.lua +++ b/lua/opencode/ui/renderer/buffer.lua @@ -9,11 +9,6 @@ local pinned_bottom_message_ids = { ['question-display-message'] = true, } -local pinned_bottom_message_order = { - 'permission-display-message', - 'question-display-message', -} - local pinned_top_message_ids = { ['__opencode_hidden_messages_notice__'] = true, } @@ -269,7 +264,8 @@ end ---@param new_line_end integer ---@param skip_clear? boolean local function apply_extmarks(previous_formatted, formatted_data, line_start, old_line_end, new_line_end, skip_clear) - local clear_start, clear_end = extmark_clear_range(previous_formatted, formatted_data, line_start, old_line_end, new_line_end) + local clear_start, clear_end = + extmark_clear_range(previous_formatted, formatted_data, line_start, old_line_end, new_line_end) if not skip_clear then output_window.clear_extmarks(clear_start, clear_end) end @@ -287,7 +283,8 @@ end ---@param old_line_end integer ---@param new_line_end integer local function apply_appended_extmarks(previous_formatted, formatted_data, line_start, old_line_end, new_line_end) - local clear_start, clear_end = extmark_clear_range(previous_formatted, formatted_data, line_start, old_line_end, new_line_end) + local clear_start, clear_end = + extmark_clear_range(previous_formatted, formatted_data, line_start, old_line_end, new_line_end) clear_start = math.max(clear_start, old_line_end + 1) if clear_start >= clear_end then return @@ -341,7 +338,7 @@ local function get_message_insert_line(message_id) return append_at end - for _, pinned_message_id in ipairs(pinned_bottom_message_order) do + for _, pinned_message_id in ipairs({ 'permission-display-message', 'question-display-message' }) do local pinned_rendered = ctx.render_state:get_message(pinned_message_id) if pinned_rendered and pinned_rendered.line_start then return pinned_rendered.line_start @@ -372,7 +369,7 @@ local function get_message_insert_line(message_id) end end - for _, pinned_message_id in ipairs(pinned_bottom_message_order) do + for _, pinned_message_id in ipairs({ 'permission-display-message', 'question-display-message' }) do local pinned_rendered = ctx.render_state:get_message(pinned_message_id) if pinned_rendered and pinned_rendered.line_start then return pinned_rendered.line_start @@ -502,7 +499,7 @@ function M.upsert_message_now(message_id, formatted_data, previous_formatted) if ctx.bulk_mode then local line_start = #ctx.bulk_buffer_lines local line_end = line_start + #formatted_data.lines - 1 - + for _, line in ipairs(formatted_data.lines) do ctx.bulk_buffer_lines[#ctx.bulk_buffer_lines + 1] = line end @@ -512,15 +509,15 @@ function M.upsert_message_now(message_id, formatted_data, previous_formatted) if formatted_data.fold_ranges then accumulate_bulk_folds(formatted_data.fold_ranges, line_start) end - + local message_data = ctx.render_state:get_message(message_id) if message_data then ctx.render_state:set_message(message_data.message, line_start, line_end) end - + return true end - + local cached = ctx.render_state:get_message(message_id) if cached and cached.line_start and cached.line_end then local old_line_end = cached.line_end @@ -534,15 +531,15 @@ function M.upsert_message_now(message_id, formatted_data, previous_formatted) old_line_end, cached.line_start + #formatted_data.lines - 1 ) - + output_window.clear_extmarks(clear_start, clear_end) output_window.set_lines(lines_to_write, write_start, cached.line_end + 1) highlight_written_lines(write_start, lines_to_write) - + local new_line_end = cached.line_start + #formatted_data.lines - 1 apply_extmarks(previous_formatted, formatted_data, cached.line_start, old_line_end, new_line_end, true) ctx.render_state:set_message(cached.message, cached.line_start, new_line_end) - + local delta = new_line_end - old_line_end if delta ~= 0 then ctx.render_state:shift_all(old_line_end + 1, delta) @@ -550,7 +547,7 @@ function M.upsert_message_now(message_id, formatted_data, previous_formatted) end return true end - + local insert_at = get_message_insert_line(message_id) local message_data = ctx.render_state:get_message(message_id) if message_data and message_data.message then @@ -558,17 +555,16 @@ function M.upsert_message_now(message_id, formatted_data, previous_formatted) if has_extmarks(formatted_data.extmarks) then output_window.set_extmarks(formatted_data.extmarks, insert_at) end - + ctx.render_state:shift_all(insert_at, #formatted_data.lines) output_window.shift_folds(insert_at, #formatted_data.lines) ctx.render_state:set_message(message_data.message, range.line_start, range.line_end) return true end - + return false end - ---@param part_id string ---@param message_id string ---@param formatted_data Output @@ -578,7 +574,7 @@ function M.upsert_part_now(part_id, message_id, formatted_data, previous_formatt if ctx.bulk_mode then local line_start = #ctx.bulk_buffer_lines local line_end = line_start + #formatted_data.lines - 1 - + for _, line in ipairs(formatted_data.lines) do ctx.bulk_buffer_lines[#ctx.bulk_buffer_lines + 1] = line end @@ -588,16 +584,16 @@ function M.upsert_part_now(part_id, message_id, formatted_data, previous_formatt if formatted_data.fold_ranges then accumulate_bulk_folds(formatted_data.fold_ranges, line_start) end - + local part_data = ctx.render_state:get_part(part_id) if part_data then ctx.render_state:set_part(part_data.part, line_start, line_end) apply_part_actions(part_id, formatted_data, line_start) end - + return true end - + local cached = ctx.render_state:get_part(part_id) if cached and cached.line_start and cached.line_end then local old_line_end = cached.line_end @@ -611,14 +607,14 @@ function M.upsert_part_now(part_id, message_id, formatted_data, previous_formatt old_line_end, cached.line_start + #formatted_data.lines - 1 ) - + output_window.clear_extmarks(clear_start, clear_end) output_window.set_lines(lines_to_write, write_start, cached.line_end + 1) highlight_written_lines(write_start, lines_to_write) - + local new_line_end = cached.line_start + #formatted_data.lines - 1 apply_part_actions(part_id, formatted_data, cached.line_start) - + if new_line_end ~= cached.line_end then ctx.render_state:update_part_lines(part_id, cached.line_start, new_line_end) end @@ -632,7 +628,7 @@ function M.upsert_part_now(part_id, message_id, formatted_data, previous_formatt return true end -local insert_at = get_part_insertion_line(part_id, message_id) + local insert_at = get_part_insertion_line(part_id, message_id) if not insert_at then return false end @@ -684,10 +680,16 @@ function M.set_all_folds() end local function folds_equal(a, b) - if not a or not b then return false end - if #a ~= #b then return false end + if not a or not b then + return false + end + 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 + if a[i].from ~= b[i].from or a[i].to ~= b[i].to then + return false + end end return true end @@ -725,12 +727,13 @@ function M.update_part_folds(part_id) table.insert(new_global, f) end end - table.sort(new_global, function(a, b) return a.from < b.from end) + table.sort(new_global, function(a, b) + return a.from < b.from + end) ctx.global_folds = new_global output_window.set_folds(new_global) end - ---@param part_id string ---@param extra_lines string[] ---@param extra_extmarks table|nil @@ -773,13 +776,13 @@ function M.remove_part_now(part_id) ctx.render_state:remove_part(part_id) return end - + local cached = ctx.render_state:get_part(part_id) if not cached or not cached.line_start or not cached.line_end then ctx.render_state:remove_part(part_id) return end - + output_window.clear_extmarks(cached.line_start - 1, cached.line_end + 1) output_window.set_lines({}, cached.line_start, cached.line_end + 1) local delta = -(cached.line_end - cached.line_start + 1) @@ -787,7 +790,6 @@ function M.remove_part_now(part_id) ctx.render_state:remove_part(part_id) end - ---@param message_id string function M.remove_message_now(message_id) if ctx.bulk_mode then @@ -796,13 +798,13 @@ function M.remove_message_now(message_id) ctx.render_state:remove_message(message_id) return end - + local cached = ctx.render_state:get_message(message_id) if not cached or not cached.line_start or not cached.line_end then ctx.render_state:remove_message(message_id) return end - + output_window.clear_extmarks(cached.line_start, cached.line_end + 1) output_window.set_lines({}, cached.line_start, cached.line_end + 1) local delta = -(cached.line_end - cached.line_start + 1) @@ -810,5 +812,4 @@ function M.remove_message_now(message_id) ctx.render_state:remove_message(message_id) end - return M diff --git a/lua/opencode/ui/renderer/events.lua b/lua/opencode/ui/renderer/events.lua index 0587f410..46e7bd0d 100644 --- a/lua/opencode/ui/renderer/events.lua +++ b/lua/opencode/ui/renderer/events.lua @@ -157,39 +157,6 @@ function M.render_question_display() end end ----@param part_id string -local function remove_rendered_part(part_id) - local cached = ctx.render_state:get_part(part_id) - M.on_part_removed({ - sessionID = state.active_session and state.active_session.id or '', - messageID = cached and cached.message_id or nil, - partID = part_id, - }) -end - ----Render the active message actions dialog as a synthetic part under its target message ----@param target_message_id string|nil ----@param part_id string|nil -function M.render_message_actions_display(target_message_id, part_id) - if not part_id or part_id == '' then - return - end - - if not target_message_id or target_message_id == '' then - remove_rendered_part(part_id) - return - end - - local fake_part = { - id = part_id, - messageID = target_message_id, - sessionID = state.active_session and state.active_session.id or '', - type = 'message-actions-display', - synthetic = true, - } - M.on_part_updated({ part = fake_part }) -end - ---Remove the question display from the buffer function M.clear_question_display() local use_vim_ui = config.ui.questions and config.ui.questions.use_vim_ui_select diff --git a/lua/opencode/ui/ui.lua b/lua/opencode/ui/ui.lua index 1d0f7aa3..52c04f64 100644 --- a/lua/opencode/ui/ui.lua +++ b/lua/opencode/ui/ui.lua @@ -137,9 +137,6 @@ function M.hide_visible_windows(windows) return M.teardown_visible_windows(windows) end - require('opencode.ui.message_actions').clear() - require('opencode.ui.renderer.flush').flush() - local snapshot = capture_hidden_snapshot(windows) -- Only save width ratio for split modes (not dialog/current mode) diff --git a/tests/unit/commands_handlers_spec.lua b/tests/unit/commands_handlers_spec.lua index 74b4285c..cd23f36b 100644 --- a/tests/unit/commands_handlers_spec.lua +++ b/tests/unit/commands_handlers_spec.lua @@ -16,7 +16,6 @@ describe('opencode.commands.handlers', function() 'opencode.commands.handlers.session', 'opencode.commands.handlers.diff', 'opencode.commands.handlers.permission', - 'opencode.ui.message_actions', } local original_loaded = {} @@ -95,13 +94,14 @@ describe('opencode.commands.handlers', function() assert.same({ 'accept', 'accept_all', 'deny' }, defs.permission.completions) assert.same({ allow_empty = false }, defs.permission.nested_subcommand) - assert.same({ 'new', 'select', 'navigate', 'compact', 'share', 'unshare', 'agents_init', 'rename', 'toggle_lock' }, defs.session.completions) + assert.same( + { 'new', 'select', 'navigate', 'compact', 'share', 'unshare', 'agents_init', 'rename', 'toggle_lock' }, + defs.session.completions + ) assert.same({ allow_empty = false }, defs.session.nested_subcommand) - assert.equal('Open message actions', defs.message_actions.desc) assert.same({ 'input', 'output' }, defs.open.completions) assert.equal('user_commands', defs.command.completion_provider_id) - end) it('keeps command semantic validation in window handler (open target)', function() @@ -143,44 +143,6 @@ describe('opencode.commands.handlers', function() }, err_diff) end) - it('routes message_actions mouse to the mouse opener without requiring it at module load', function() - local session = require('opencode.commands.handlers.session') - local calls = {} - - assert.is_nil(package.loaded['opencode.ui.message_actions']) - - package.loaded['opencode.ui.message_actions'] = { - open_from_mouse = function() - calls[#calls + 1] = 'mouse' - end, - open_at_cursor = function() - calls[#calls + 1] = 'cursor' - end, - } - - session.command_defs.message_actions.execute({ 'mouse' }) - - assert.same({ 'mouse' }, calls) - end) - - it('routes message_actions without mouse arg to the cursor opener', function() - local session = require('opencode.commands.handlers.session') - local calls = {} - - package.loaded['opencode.ui.message_actions'] = { - open_from_mouse = function() - calls[#calls + 1] = 'mouse' - end, - open_at_cursor = function() - calls[#calls + 1] = 'cursor' - end, - } - - session.command_defs.message_actions.execute({}) - - assert.same({ 'cursor' }, calls) - end) - it('keeps help rendering stable in narrow output windows', function() local surface = require('opencode.commands.handlers.surface') local window = require('opencode.commands.handlers.window') @@ -584,7 +546,8 @@ describe('opencode.commands.handlers', function() it('normalize_navigate_args rejects invalid empty_policy', function() local session_handler = require('opencode.commands.handlers.session') - local ok, err = pcall(session_handler.command_defs.navigate_session_tree.execute, { 'forward', 'direct', 'false', 'silent' }) + local ok, err = + pcall(session_handler.command_defs.navigate_session_tree.execute, { 'forward', 'direct', 'false', 'silent' }) assert.is_false(ok) assert.equal('invalid_arguments', err.code) end) diff --git a/tests/unit/config_spec.lua b/tests/unit/config_spec.lua index 999e81f3..96a2ce18 100644 --- a/tests/unit/config_spec.lua +++ b/tests/unit/config_spec.lua @@ -45,30 +45,6 @@ describe('opencode.config', function() assert.equal('jump_to_target_at_cursor', output_keymap[''][1]) end) - it('maps output message actions to the prefixed enter key by default', function() - local output_keymap = config.defaults.keymap.output_window - - assert.equal('message_actions', output_keymap['o'][1]) - end) - - it('maps output single click to message actions by default', function() - local output_keymap = config.defaults.keymap.output_window - - assert.equal('message_actions', output_keymap[''][1]) - assert.same({ 'mouse' }, output_keymap[''][2]) - assert.is_nil(output_keymap['<2-LeftMouse>']) - end) - - it('moves output message actions with keymap_prefix while leaving bare enter unchanged', function() - config.setup({ keymap_prefix = 'x' }) - - local output_keymap = config.values.keymap.output_window - - assert.equal('message_actions', output_keymap['x'][1]) - assert.is_nil(output_keymap['o']) - assert.equal('jump_to_target_at_cursor', output_keymap[''][1]) - end) - describe('update_keymap_prefix', function() local function test_prefix_update(opts) config.values.keymap = vim.deepcopy(opts.given) diff --git a/tests/unit/dialog_spec.lua b/tests/unit/dialog_spec.lua index a95434e9..a1f186b2 100644 --- a/tests/unit/dialog_spec.lua +++ b/tests/unit/dialog_spec.lua @@ -7,28 +7,6 @@ describe('Dialog', function() local input_buf, output_buf, input_win, output_win local original_auto_hide - local function keymap_callback(buf, lhs) - for _, keymap in ipairs(vim.api.nvim_buf_get_keymap(buf, 'n')) do - if keymap.lhs:lower() == lhs:lower() then - return keymap.callback - end - end - end - - local function with_mousepos(mousepos, callback) - local original_getmousepos = vim.fn.getmousepos - ---@diagnostic disable-next-line: duplicate-set-field - vim.fn.getmousepos = function() - return mousepos - end - - local ok, err = pcall(callback) - vim.fn.getmousepos = original_getmousepos - if not ok then - error(err) - end - end - before_each(function() -- Save original config original_auto_hide = config.ui.input.auto_hide @@ -73,159 +51,9 @@ describe('Dialog', function() pcall(vim.api.nvim_buf_delete, output_buf, { force = true }) state.ui.clear_windows() - require('opencode.ui.renderer.ctx'):reset() package.loaded['opencode.ui.input_window'] = nil end) - describe('mouse option selection', function() - it('selects an option by rendered part line plus tracked option line', function() - local selected = nil - local dialog = Dialog.new({ - buffer = output_buf, - render_part_id = 'dialog-test-part', - mouse_select = true, - on_select = function(index) - selected = index - end, - get_option_count = function() - return 3 - end, - hide_input = false, - }) - - dialog:setup() - - local output = Output.new() - dialog:format_options(output, { - { label = 'First' }, - { label = 'Second' }, - { label = 'Third' }, - }) - - require('opencode.ui.renderer.ctx').render_state:set_part({ id = 'dialog-test-part' }, 20, 30) - - with_mousepos({ - winid = output_win, - line = 20 + dialog._option_local_lines[2] + 1, - column = 1, - }, function() - keymap_callback(output_buf, '')() - end) - - assert.are.equal(2, selected) - assert.are.equal(2, dialog:get_selection()) - - dialog:teardown() - end) - - it('ignores clicks outside tracked option lines', function() - local selected = nil - local dialog = Dialog.new({ - buffer = output_buf, - render_part_id = 'dialog-test-part', - mouse_select = true, - on_select = function(index) - selected = index - end, - get_option_count = function() - return 2 - end, - hide_input = false, - }) - - dialog:setup() - - local output = Output.new() - dialog:format_options(output, { - { label = 'First' }, - { label = 'Second' }, - }) - - require('opencode.ui.renderer.ctx').render_state:set_part({ id = 'dialog-test-part' }, 20, 30) - - with_mousepos({ - winid = output_win, - line = 20 + 9 + 1, - column = 1, - }, function() - keymap_callback(output_buf, '')() - end) - - assert.is_nil(selected) - assert.are.equal(1, dialog:get_selection()) - - dialog:teardown() - end) - end) - - describe('dismiss keymaps', function() - it('maps both default dismiss keys', function() - local dismiss_count = 0 - local dialog = Dialog.new({ - buffer = output_buf, - on_select = function() end, - on_dismiss = function() - dismiss_count = dismiss_count + 1 - end, - get_option_count = function() - return 1 - end, - hide_input = false, - }) - - dialog:setup() - - keymap_callback(output_buf, '')() - keymap_callback(output_buf, '')() - - assert.are.equal(2, dismiss_count) - - dialog:teardown() - end) - - it('does not map dismiss when dismiss is an empty string', function() - local dialog = Dialog.new({ - buffer = output_buf, - on_select = function() end, - get_option_count = function() - return 1 - end, - hide_input = false, - keymaps = { - dismiss = '', - }, - }) - - dialog:setup() - - assert.is_nil(keymap_callback(output_buf, '')) - assert.is_nil(keymap_callback(output_buf, '')) - - dialog:teardown() - end) - - it('does not map dismiss when dismiss is an empty list', function() - local dialog = Dialog.new({ - buffer = output_buf, - on_select = function() end, - get_option_count = function() - return 1 - end, - hide_input = false, - keymaps = { - dismiss = {}, - }, - }) - - dialog:setup() - - assert.is_nil(keymap_callback(output_buf, '')) - assert.is_nil(keymap_callback(output_buf, '')) - - dialog:teardown() - end) - end) - describe('teardown with hide_input enabled', function() it('should show input window when auto_hide is disabled', function() config.ui.input.auto_hide = false @@ -466,26 +294,6 @@ describe('Dialog', function() end) describe('Dialog formatting', function() - it('tracks option lines from the line returned by add_line', function() - local dialog = Dialog.new({ - buffer = 0, - on_select = function() end, - get_option_count = function() - return 2 - end, - }) - - local output = Output.new() - output:add_line('content before options') - - dialog:format_options(output, { - { label = 'First' }, - { label = 'Second' }, - }) - - assert.are.same({ 1, 2 }, dialog._option_local_lines) - end) - it('places selection extmarks on the selected option line', function() local dialog = Dialog.new({ buffer = 0, diff --git a/tests/unit/formatter_spec.lua b/tests/unit/formatter_spec.lua index 6bb8fe83..5a7620e6 100644 --- a/tests/unit/formatter_spec.lua +++ b/tests/unit/formatter_spec.lua @@ -1,5 +1,4 @@ local assert = require('luassert') -local stub = require('luassert.stub') local config = require('opencode.config') local formatter = require('opencode.ui.formatter') local Output = require('opencode.ui.output') @@ -476,29 +475,6 @@ describe('formatter', function() assert.is_nil(output.extmarks[0]) end) - it('dispatches message-actions-display system parts to message_actions.format_display', function() - local message_actions = require('opencode.ui.message_actions') - local format_stub = stub(message_actions, 'format_display') - - local output = formatter.format_part({ - id = 'message-actions-display-part:msg_user', - type = 'message-actions-display', - messageID = 'msg_user', - sessionID = 'ses_1', - }, { - info = { - id = 'msg_user', - role = 'user', - sessionID = 'ses_1', - }, - parts = {}, - }, true) - - assert.stub(format_stub).was_called() - assert.are.same({ '' }, output.lines) - format_stub:revert() - end) - it('does not add a spacing-only block for hidden same-mode assistant messages', function() config.setup({ ui = { diff --git a/tests/unit/keymap_spec.lua b/tests/unit/keymap_spec.lua index 2976d013..bff34163 100644 --- a/tests/unit/keymap_spec.lua +++ b/tests/unit/keymap_spec.lua @@ -62,8 +62,6 @@ describe('opencode.keymap', function() open_input = { desc = 'Open input window', execute = function() end }, toggle = { desc = 'Toggle opencode windows', execute = function() end }, submit_input_prompt = { desc = 'Submit input prompt', execute = function() end }, - message_actions = { desc = 'Open message actions', execute = function() end }, - jump_to_target_at_cursor = { desc = 'Jump to target at cursor', execute = function() end }, } end, build_parsed_intent = function(name, args) @@ -136,7 +134,9 @@ describe('opencode.keymap', function() assert.equal(2, #set_keymaps) local by_key = {} - for _, km in ipairs(set_keymaps) do by_key[km.key] = km end + for _, km in ipairs(set_keymaps) do + by_key[km.key] = km + end assert.equal('Custom description for open input', by_key['test'].opts.desc) assert.equal('Custom function description', by_key['func'].opts.desc) @@ -200,51 +200,6 @@ describe('opencode.keymap', function() vim.api.nvim_buf_delete(bufnr, { force = true }) end) - it('routes output single-click to message actions from mouse', function() - local bufnr = vim.api.nvim_create_buf(false, true) - keymap.setup_window_keymaps({ [''] = { 'message_actions', { 'mouse' } } }, bufnr) - - assert.equal(1, #set_keymaps) - set_keymaps[1].callback() - - assert.equal(1, #built_parsed) - assert.same('message_actions', built_parsed[1].intent.name) - assert.same({ 'mouse' }, built_parsed[1].intent.args) - assert.equal(1, #executed_parsed) - - vim.api.nvim_buf_delete(bufnr, { force = true }) - end) - - it('routes output prefixed enter to message actions without args', function() - local bufnr = vim.api.nvim_create_buf(false, true) - keymap.setup_window_keymaps({ ['o'] = { 'message_actions' } }, bufnr) - - assert.equal(1, #set_keymaps) - set_keymaps[1].callback() - - assert.equal(1, #built_parsed) - assert.same('message_actions', built_parsed[1].intent.name) - assert.same({}, built_parsed[1].intent.args) - assert.equal(1, #executed_parsed) - - vim.api.nvim_buf_delete(bufnr, { force = true }) - end) - - it('keeps bare output enter on jump_to_target_at_cursor', function() - local bufnr = vim.api.nvim_create_buf(false, true) - keymap.setup_window_keymaps({ [''] = { 'jump_to_target_at_cursor' } }, bufnr) - - assert.equal(1, #set_keymaps) - set_keymaps[1].callback() - - assert.equal(1, #built_parsed) - assert.same('jump_to_target_at_cursor', built_parsed[1].intent.name) - assert.same({}, built_parsed[1].intent.args) - assert.equal(1, #executed_parsed) - - vim.api.nvim_buf_delete(bufnr, { force = true }) - end) - it('does not mutate configured table args across repeated key presses', function() local original_execute_parsed_intent = mock_commands.execute_parsed_intent mock_commands.execute_parsed_intent = function(parsed) diff --git a/tests/unit/message_actions_spec.lua b/tests/unit/message_actions_spec.lua deleted file mode 100644 index 7e61d96c..00000000 --- a/tests/unit/message_actions_spec.lua +++ /dev/null @@ -1,752 +0,0 @@ -local assert = require('luassert') -local stub = require('luassert.stub') -local helpers = require('tests.helpers') -local state = require('opencode.state') -local ctx = require('opencode.ui.renderer.ctx') - -local function user_message(id, parts) - return { - info = { - id = id, - role = 'user', - sessionID = 'ses_1', - }, - parts = parts or { - { id = id .. '_text', type = 'text', text = 'hello', messageID = id, sessionID = 'ses_1' }, - }, - } -end - -local function message(role, id, parts) - return { - info = { - id = id, - role = role, - sessionID = 'ses_1', - }, - parts = parts or {}, - } -end - -describe('message_actions', function() - local message_actions - local input_buf, output_buf, other_buf - local input_win, output_win, other_win - local render_display_stub - local notify_mock - - local function with_mousepos(mousepos, callback) - local original_getmousepos = vim.fn.getmousepos - ---@diagnostic disable-next-line: duplicate-set-field - vim.fn.getmousepos = function() - return mousepos - end - - local ok, err = pcall(callback) - vim.fn.getmousepos = original_getmousepos - if not ok then - error(err) - end - end - - local function set_output_cursor(line) - vim.api.nvim_win_set_cursor(output_win, { line, 0 }) - end - - local function has_buffer_key(lhs) - for _, map in ipairs(vim.api.nvim_buf_get_keymap(output_buf, 'n')) do - if map.lhs == lhs then - return true - end - end - return false - end - - local function render_message_at(message_to_render, line_start, line_end) - ctx.render_state:set_message(message_to_render, line_start, line_end) - end - - local function render_part_at(part, line_start, line_end) - ctx.render_state:set_part(part, line_start, line_end) - end - - local function attach_rendered_actions_part(line_start) - local Output = require('opencode.ui.output') - local output = Output.new() - message_actions.format_display(output) - render_part_at({ - id = 'message-actions-display-part:msg_user', - type = 'message-actions-display', - messageID = 'msg_user', - sessionID = 'ses_1', - synthetic = true, - }, line_start, line_start + #output.lines - 1) - end - - before_each(function() - package.loaded['opencode.ui.message_actions'] = nil - package.loaded['opencode.api'] = nil - message_actions = require('opencode.ui.message_actions') - - input_buf = vim.api.nvim_create_buf(false, true) - output_buf = vim.api.nvim_create_buf(false, true) - other_buf = vim.api.nvim_create_buf(false, true) - input_win = vim.api.nvim_open_win(input_buf, false, { - relative = 'editor', - width = 80, - height = 5, - row = 0, - col = 0, - }) - output_win = vim.api.nvim_open_win(output_buf, true, { - relative = 'editor', - width = 80, - height = 10, - row = 6, - col = 0, - }) - other_win = vim.api.nvim_open_win(other_buf, false, { - relative = 'editor', - width = 80, - height = 5, - row = 17, - col = 0, - }) - - vim.api.nvim_buf_set_lines(output_buf, 0, -1, false, { 'one', 'two', 'three', 'four' }) - state.ui.set_windows({ - input_buf = input_buf, - input_win = input_win, - output_buf = output_buf, - output_win = output_win, - }) - state.session.set_active({ id = 'ses_1' }) - state.renderer.set_messages({}) - ctx:reset() - - package.loaded['opencode.ui.input_window'] = { - _hide = function() end, - _show = function() end, - } - - render_display_stub = stub(require('opencode.ui.renderer.events'), 'render_message_actions_display') - notify_mock = helpers.mock_notify() - end) - - after_each(function() - notify_mock.reset() - if render_display_stub then - render_display_stub:revert() - end - pcall(message_actions.clear) - ctx:reset() - state.renderer.set_messages({}) - state.session.set_active(nil) - state.ui.clear_windows() - package.loaded['opencode.ui.input_window'] = nil - package.loaded['opencode.api'] = nil - require('opencode.ui.question_window')._current_question = nil - require('opencode.ui.question_window')._current_question_index = 1 - require('opencode.ui.permission_window')._permission_queue = {} - require('opencode.ui.permission_window')._dialog = nil - pcall(vim.api.nvim_win_close, input_win, true) - pcall(vim.api.nvim_win_close, output_win, true) - pcall(vim.api.nvim_win_close, other_win, true) - pcall(vim.api.nvim_buf_delete, input_buf, { force = true }) - pcall(vim.api.nvim_buf_delete, output_buf, { force = true }) - pcall(vim.api.nvim_buf_delete, other_buf, { force = true }) - end) - - it('does not require opencode.api at module load time', function() - package.loaded['opencode.ui.message_actions'] = nil - package.loaded['opencode.api'] = nil - - require('opencode.ui.message_actions') - - assert.is_nil(package.loaded['opencode.api']) - end) - - it('open_at_cursor finds the user message from the output cursor line', function() - local target = user_message('msg_user') - render_message_at(target, 0, 0) - set_output_cursor(1) - - message_actions.open_at_cursor() - - assert.are.equal(target, message_actions._target_message) - assert.is_not_nil(message_actions._dialog) - assert.stub(render_display_stub).was_called_with('msg_user', 'message-actions-display-part:msg_user') - end) - - it('open_at_cursor finds the user message from a rendered user text part line', function() - local target = user_message('msg_user') - local text_part = target.parts[1] - render_message_at(target, 0, 0) - render_part_at(text_part, 1, 1) - set_output_cursor(2) - - message_actions.open_at_cursor() - - assert.are.equal(target, message_actions._target_message) - assert.stub(render_display_stub).was_called_with('msg_user', 'message-actions-display-part:msg_user') - end) - - it('open_at_cursor does not open from assistant part or message actions part lines', function() - local assistant = message('assistant', 'msg_assistant', { - { id = 'part_assistant', type = 'text', text = 'assistant', messageID = 'msg_assistant', sessionID = 'ses_1' }, - }) - render_message_at(assistant, 0, 0) - render_part_at(assistant.parts[1], 1, 1) - set_output_cursor(2) - - message_actions.open_at_cursor() - - assert.is_nil(message_actions._target_message) - assert.stub(render_display_stub).was_not_called() - - local target = user_message('msg_user') - render_message_at(target, 2, 2) - render_part_at({ - id = 'message-actions-display-part:msg_user', - type = 'message-actions-display', - messageID = 'msg_user', - sessionID = 'ses_1', - synthetic = true, - }, 3, 3) - set_output_cursor(4) - - message_actions.open_at_cursor() - - assert.is_nil(message_actions._target_message) - assert.stub(render_display_stub).was_not_called() - end) - - it('open_from_mouse uses the mouse line instead of the cursor line', function() - local assistant = message('assistant', 'msg_assistant') - local target = user_message('msg_user') - render_message_at(assistant, 0, 0) - render_message_at(target, 1, 1) - set_output_cursor(1) - - with_mousepos({ winid = output_win, line = 2, column = 1 }, function() - message_actions.open_from_mouse() - end) - - assert.are.equal(target, message_actions._target_message) - end) - - it('open_from_mouse finds the user message from a rendered user text part line', function() - local target = user_message('msg_user') - render_message_at(target, 0, 0) - render_part_at(target.parts[1], 1, 1) - - with_mousepos({ winid = output_win, line = 2, column = 1 }, function() - message_actions.open_from_mouse() - end) - - assert.are.equal(target, message_actions._target_message) - assert.stub(render_display_stub).was_called_with('msg_user', 'message-actions-display-part:msg_user') - end) - - it('normalizes Neovim 1-based cursor and mouse lines to render-state 0-based lines', function() - local first = user_message('msg_first') - local second = user_message('msg_second') - render_message_at(first, 0, 0) - render_message_at(second, 1, 1) - - set_output_cursor(1) - message_actions.open_at_cursor() - assert.are.equal(first, message_actions._target_message) - - with_mousepos({ winid = output_win, line = 2, column = 1 }, function() - message_actions.open_from_mouse() - end) - assert.is_nil(message_actions._target_message) - - with_mousepos({ winid = output_win, line = 2, column = 1 }, function() - message_actions.open_from_mouse() - end) - assert.are.equal(second, message_actions._target_message) - end) - - it('open_from_mouse silently ignores mouse positions outside the output window', function() - render_message_at(user_message('msg_user'), 0, 0) - - with_mousepos({ winid = other_win, line = 1, column = 1 }, function() - message_actions.open_from_mouse() - end) - - assert.is_nil(message_actions._target_message) - assert.stub(render_display_stub).was_not_called() - end) - - it('open_from_mouse silently ignores output-window ids whose buffer does not match output_buf', function() - render_message_at(user_message('msg_user'), 0, 0) - vim.api.nvim_win_set_buf(output_win, other_buf) - - with_mousepos({ winid = output_win, line = 1, column = 1 }, function() - message_actions.open_from_mouse() - end) - - assert.is_nil(message_actions._target_message) - assert.stub(render_display_stub).was_not_called() - end) - - it('open_from_mouse routes active question clicks before message actions', function() - local question_window = require('opencode.ui.question_window') - local permission_window = require('opencode.ui.permission_window') - local original_has_question = question_window.has_question - local original_select_question = question_window.select_mouse_option - local original_has_permissions = permission_window.has_permissions - local selected = false - - question_window.has_question = function() - return true - end - question_window.select_mouse_option = function() - selected = true - return true - end - permission_window.has_permissions = function() - return false - end - render_message_at(user_message('msg_user'), 0, 0) - - with_mousepos({ winid = output_win, line = 1, column = 1 }, function() - message_actions.open_from_mouse() - end) - - question_window.has_question = original_has_question - question_window.select_mouse_option = original_select_question - permission_window.has_permissions = original_has_permissions - - assert.is_true(selected) - assert.is_nil(message_actions._target_message) - assert.stub(render_display_stub).was_not_called() - end) - - it('open_from_mouse routes active permission clicks before message actions', function() - local question_window = require('opencode.ui.question_window') - local permission_window = require('opencode.ui.permission_window') - local original_has_question = question_window.has_question - local original_has_permissions = permission_window.has_permissions - local original_select_permission = permission_window.select_mouse_option - local selected = false - - question_window.has_question = function() - return false - end - permission_window.has_permissions = function() - return true - end - permission_window.select_mouse_option = function() - selected = true - return true - end - render_message_at(user_message('msg_user'), 0, 0) - - with_mousepos({ winid = output_win, line = 1, column = 1 }, function() - message_actions.open_from_mouse() - end) - - question_window.has_question = original_has_question - permission_window.has_permissions = original_has_permissions - permission_window.select_mouse_option = original_select_permission - - assert.is_true(selected) - assert.is_nil(message_actions._target_message) - assert.stub(render_display_stub).was_not_called() - end) - - it('only allows non-synthetic user messages with ids', function() - assert.is_true(message_actions.is_actionable_user_message(user_message('msg_user'))) - assert.is_false(message_actions.is_actionable_user_message(message('assistant', 'msg_assistant'))) - assert.is_false(message_actions.is_actionable_user_message(message('tool', 'msg_tool'))) - assert.is_false(message_actions.is_actionable_user_message(message('system', 'msg_system'))) - assert.is_false(message_actions.is_actionable_user_message(user_message('', {}))) - assert.is_false(message_actions.is_actionable_user_message(user_message('msg_synthetic', { - { type = 'text', text = 'synthetic', synthetic = true }, - }))) - end) - - it('refuses to open while a question dialog is active', function() - local question_window = require('opencode.ui.question_window') - question_window._current_question = { - id = 'question_1', - questions = { - { question = 'Choose', options = {} }, - }, - } - question_window._current_question_index = 1 - render_message_at(user_message('msg_user'), 0, 0) - set_output_cursor(1) - - message_actions.open_at_cursor() - - assert.is_nil(message_actions._target_message) - assert.are.equal('Finish the active dialog first', notify_mock.get_notifications()[1].msg) - end) - - it('refuses to open while a permission dialog is active', function() - require('opencode.ui.permission_window')._permission_queue = { { id = 'permission_1' } } - render_message_at(user_message('msg_user'), 0, 0) - set_output_cursor(1) - - message_actions.open_at_cursor() - - assert.is_nil(message_actions._target_message) - assert.are.equal('Finish the active dialog first', notify_mock.get_notifications()[1].msg) - end) - - it('clear tears down the active dialog and requests synthetic display removal', function() - render_message_at(user_message('msg_user'), 0, 0) - set_output_cursor(1) - message_actions.open_at_cursor() - render_display_stub:clear() - - message_actions.clear() - - assert.is_nil(message_actions._target_message) - assert.is_nil(message_actions._dialog) - assert.stub(render_display_stub).was_called_with(nil, 'message-actions-display-part:msg_user') - end) - - it('does not let Dialog override the output single-click mapping while active', function() - render_message_at(user_message('msg_user'), 0, 0) - set_output_cursor(1) - message_actions.open_at_cursor() - - assert.is_false(has_buffer_key('')) - end) - - it('active mouse click on an action option selects that option', function() - local setreg_name, setreg_value - local original_setreg = vim.fn.setreg - ---@diagnostic disable-next-line: duplicate-set-field - vim.fn.setreg = function(name, value) - setreg_name = name - setreg_value = value - end - - render_message_at( - user_message('msg_user', { - { id = 'part_text', type = 'text', text = 'copy me', messageID = 'msg_user', sessionID = 'ses_1' }, - }), - 0, - 0 - ) - set_output_cursor(1) - message_actions.open_at_cursor() - attach_rendered_actions_part(20) - local copy_line = 20 + message_actions._dialog._option_local_lines[2] + 1 - - with_mousepos({ winid = output_win, line = copy_line, column = 1 }, function() - message_actions.open_from_mouse() - end) - vim.fn.setreg = original_setreg - - assert.are.equal('+', setreg_name) - assert.are.equal('copy me', setreg_value) - assert.is_nil(message_actions._target_message) - end) - - it('active mouse click outside action options clears the dialog', function() - render_message_at(user_message('msg_user'), 0, 0) - set_output_cursor(1) - message_actions.open_at_cursor() - attach_rendered_actions_part(20) - render_display_stub:clear() - - with_mousepos({ winid = output_win, line = 1, column = 1 }, function() - message_actions.open_from_mouse() - end) - - assert.is_nil(message_actions._target_message) - assert.is_nil(message_actions._dialog) - assert.stub(render_display_stub).was_called_with(nil, 'message-actions-display-part:msg_user') - end) - - it('active mouse click on another message clears instead of switching targets', function() - render_message_at(user_message('msg_user'), 0, 0) - render_message_at(user_message('msg_other'), 2, 2) - set_output_cursor(1) - message_actions.open_at_cursor() - attach_rendered_actions_part(20) - - with_mousepos({ winid = output_win, line = 3, column = 1 }, function() - message_actions.open_from_mouse() - end) - - assert.is_nil(message_actions._target_message) - assert.is_nil(message_actions._dialog) - end) - - it('teardown clears state and keymaps without requesting renderer removal', function() - local target = user_message('msg_user') - render_message_at(target, 0, 0) - set_output_cursor(1) - message_actions.open_at_cursor() - assert.is_true(has_buffer_key('1')) - - render_display_stub:clear() - message_actions.teardown() - - assert.is_nil(message_actions._target_message) - assert.is_nil(message_actions._dialog) - assert.is_nil(message_actions._display_part_id) - assert.is_false(has_buffer_key('1')) - assert.stub(render_display_stub).was_not_called() - end) - - it('renderer reset tears down the active message actions dialog', function() - local target = user_message('msg_user') - render_message_at(target, 0, 0) - set_output_cursor(1) - message_actions.open_at_cursor() - assert.is_true(has_buffer_key('1')) - - require('opencode.ui.renderer').reset() - - assert.is_nil(message_actions._target_message) - assert.is_nil(message_actions._dialog) - assert.is_nil(message_actions._display_part_id) - assert.is_false(has_buffer_key('1')) - end) - - it('persist hide tears down the active message actions dialog', function() - local config = require('opencode.config') - config.values.ui.persist_state = true - local target = user_message('msg_user') - render_message_at(target, 0, 0) - set_output_cursor(1) - message_actions.open_at_cursor() - assert.is_true(has_buffer_key('1')) - render_display_stub:clear() - - require('opencode.ui.ui').hide_visible_windows(state.windows) - - assert.is_nil(message_actions._target_message) - assert.is_nil(message_actions._dialog) - assert.is_nil(message_actions._display_part_id) - assert.is_false(has_buffer_key('1')) - assert.stub(render_display_stub).was_called_with(nil, 'message-actions-display-part:msg_user') - end) - - it('selecting Revert calls opencode.api.undo with the target message id', function() - local undo_id = nil - package.loaded['opencode.api'] = { - undo = function(message_id) - undo_id = message_id - end, - } - render_message_at(user_message('msg_user'), 0, 0) - set_output_cursor(1) - message_actions.open_at_cursor() - - message_actions._dialog:set_selection(1) - message_actions._dialog:select() - - assert.are.equal('msg_user', undo_id) - end) - - it('selecting Fork calls opencode.api.fork_session with the target message id', function() - local fork_id = nil - package.loaded['opencode.api'] = { - fork_session = function(message_id) - fork_id = message_id - end, - } - render_message_at(user_message('msg_user'), 0, 0) - set_output_cursor(1) - message_actions.open_at_cursor() - - message_actions._dialog:set_selection(3) - message_actions._dialog:select() - - assert.are.equal('msg_user', fork_id) - end) - - it('collect_user_text joins only non-synthetic text parts without trimming copied text', function() - local text = message_actions.collect_user_text(user_message('msg_user', { - { type = 'text', text = ' first ' }, - { type = 'tool', text = 'tool output' }, - { type = 'text', text = 'synthetic', synthetic = true }, - { type = 'text', text = ' ' }, - { type = 'text', text = 'second\nline' }, - })) - - assert.are.equal(' first \n\nsecond\nline', text) - end) - - it('selecting Copy writes only the + register with the collected user text', function() - local setreg_name, setreg_value - local original_setreg = vim.fn.setreg - ---@diagnostic disable-next-line: duplicate-set-field - vim.fn.setreg = function(name, value) - setreg_name = name - setreg_value = value - end - - render_message_at( - user_message('msg_user', { - { type = 'text', text = 'first' }, - { type = 'text', text = 'synthetic', synthetic = true }, - { type = 'text', text = 'second' }, - }), - 0, - 0 - ) - set_output_cursor(1) - message_actions.open_at_cursor() - - message_actions._dialog:set_selection(2) - message_actions._dialog:select() - vim.fn.setreg = original_setreg - - assert.are.equal('+', setreg_name) - assert.are.equal('first\n\nsecond', setreg_value) - end) - - it('selecting Copy notifies and does not write a register when there is no text', function() - local setreg_called = false - local original_setreg = vim.fn.setreg - ---@diagnostic disable-next-line: duplicate-set-field - vim.fn.setreg = function() - setreg_called = true - end - - render_message_at( - user_message('msg_user', { - { type = 'text', text = ' ' }, - { type = 'text', text = 'synthetic', synthetic = true }, - }), - 0, - 0 - ) - set_output_cursor(1) - message_actions.open_at_cursor() - - message_actions._dialog:set_selection(2) - message_actions._dialog:select() - vim.fn.setreg = original_setreg - - assert.is_false(setreg_called) - assert.are.equal('No message text to copy', notify_mock.get_notifications()[1].msg) - end) - - it('formats only the action choices without the generic dialog legend', function() - local Output = require('opencode.ui.output') - render_message_at(user_message('msg_user'), 0, 0) - set_output_cursor(1) - message_actions.open_at_cursor() - - local output = Output.new() - message_actions.format_display(output) - local lines = table.concat(output.lines, '\n') - - assert.is_not_nil(lines:find('Message Actions', 1, true)) - assert.is_not_nil(lines:find('Revert', 1, true)) - assert.is_not_nil(lines:find('Copy', 1, true)) - assert.is_not_nil(lines:find('Fork', 1, true)) - assert.is_nil(lines:find('Move:', 1, true)) - assert.is_nil(lines:find('Select:', 1, true)) - assert.is_nil(lines:find('Close:', 1, true)) - end) -end) - -describe('renderer.events message actions display', function() - local events = require('opencode.ui.renderer.events') - local message_actions - local part_stub - local remove_part_stub - - before_each(function() - package.loaded['opencode.ui.message_actions'] = nil - message_actions = require('opencode.ui.message_actions') - state.session.set_active({ id = 'ses_1' }) - state.renderer.set_messages({ - user_message('msg_user'), - user_message('msg_other'), - }) - ctx:reset() - part_stub = stub(events, 'on_part_updated') - remove_part_stub = stub(events, 'on_part_removed') - end) - - after_each(function() - part_stub:revert() - remove_part_stub:revert() - state.session.set_active(nil) - state.renderer.set_messages({}) - ctx:reset() - end) - - it('creates the synthetic part under the target message', function() - events.render_message_actions_display('msg_user', 'message-actions-display-part:msg_user') - - assert.stub(part_stub).was_called_with({ - part = { - id = 'message-actions-display-part:msg_user', - messageID = 'msg_user', - sessionID = 'ses_1', - type = 'message-actions-display', - synthetic = true, - }, - }) - end) - - it('removes the synthetic part when asked not to show it', function() - events.render_message_actions_display(nil, 'message-actions-display-part:msg_user') - - assert.stub(remove_part_stub).was_called_with({ - sessionID = 'ses_1', - messageID = nil, - partID = 'message-actions-display-part:msg_user', - }) - end) - - it('uses separate synthetic part ids when the target message changes', function() - ctx.render_state:set_part({ - id = 'message-actions-display-part:msg_other', - messageID = 'msg_other', - sessionID = 'ses_1', - type = 'message-actions-display', - synthetic = true, - }, 10, 12) - - events.render_message_actions_display(nil, 'message-actions-display-part:msg_other') - events.render_message_actions_display('msg_user', 'message-actions-display-part:msg_user') - - assert.stub(remove_part_stub).was_called_with({ - sessionID = 'ses_1', - messageID = 'msg_other', - partID = 'message-actions-display-part:msg_other', - }) - assert.stub(part_stub).was_called_with({ - part = { - id = 'message-actions-display-part:msg_user', - messageID = 'msg_user', - sessionID = 'ses_1', - type = 'message-actions-display', - synthetic = true, - }, - }) - end) - - it('renders synthetic parts without loading message_actions', function() - package.loaded['opencode.ui.message_actions'] = nil - - events.render_message_actions_display('msg_user', 'message-actions-display-part:msg_user') - - assert.is_nil(package.loaded['opencode.ui.message_actions']) - assert.stub(part_stub).was_called_with({ - part = { - id = 'message-actions-display-part:msg_user', - messageID = 'msg_user', - sessionID = 'ses_1', - type = 'message-actions-display', - synthetic = true, - }, - }) - end) -end) diff --git a/tests/unit/permission_window_spec.lua b/tests/unit/permission_window_spec.lua index 3aefe41a..bf6d569f 100644 --- a/tests/unit/permission_window_spec.lua +++ b/tests/unit/permission_window_spec.lua @@ -1,44 +1,12 @@ local permission_window = require('opencode.ui.permission_window') local Output = require('opencode.ui.output') local stub = require('luassert.stub') -local helpers = require('tests.helpers') -local state = require('opencode.state') -local config = require('opencode.config') describe('permission_window', function() - local original_auto_hide - - before_each(function() - original_auto_hide = config.ui.input.auto_hide - end) - - local function with_mousepos(mousepos, callback) - local original_getmousepos = vim.fn.getmousepos - ---@diagnostic disable-next-line: duplicate-set-field - vim.fn.getmousepos = function() - return mousepos - end - - local ok, err = pcall(callback) - vim.fn.getmousepos = original_getmousepos - if not ok then - error(err) - end - end - after_each(function() - config.ui.input.auto_hide = true - pcall(permission_window.clear_all) - config.ui.input.auto_hide = original_auto_hide permission_window._permission_queue = {} permission_window._dialog = nil permission_window._processing = false - state.jobs.set_api_client(nil) - state.renderer.set_messages({}) - state.session.set_active(nil) - if state.windows then - pcall(require('opencode.ui.ui').close_windows, state.windows) - end end) describe('format_display', function() @@ -667,77 +635,4 @@ describe('permission_window', function() assert.is_nil(permission_window._permission_queue[1]._call_id) end) end) - - describe('Dialog integration', function() - it('passes permission display part to Dialog and selects clicked option', function() - helpers.replay_setup() - config.ui.input.auto_hide = true - state.session.set_active({ id = 'sess1' }) - vim.api.nvim_set_current_win(state.windows.output_win) - - local original_schedule = vim.schedule - ---@diagnostic disable-next-line: duplicate-set-field - vim.schedule = function(fn) - fn() - end - - local api = require('opencode.api') - local original_permission_deny = api.permission_deny - local denied = {} - api.permission_deny = function(permission) - table.insert(denied, permission.id) - end - - local ok, err = pcall(function() - permission_window.add_permission({ - id = 'perm-mouse', - permission = 'bash', - title = 'Run command', - }) - - local dialog = permission_window._dialog - assert.are.equal('permission-display-part', dialog._config.render_part_id) - assert.is_false(dialog._config.mouse_select) - - require('opencode.ui.renderer.events').render_permissions_display() - require('opencode.ui.renderer.flush').flush() - - local rendered_part = require('opencode.ui.renderer.ctx').render_state:get_part('permission-display-part') - assert.is_not_nil(rendered_part) - - with_mousepos({ - winid = state.windows.output_win, - line = rendered_part.line_start + dialog._option_local_lines[2] + 1, - column = 1, - }, function() - dialog:select_mouse_option() - end) - - assert.are.same({ 'perm-mouse' }, denied) - assert.are.equal(0, permission_window.get_permission_count()) - end) - - api.permission_deny = original_permission_deny - vim.schedule = original_schedule - if not ok then - error(err) - end - end) - - it('keeps Esc and Ctrl-C unavailable as dismiss keys', function() - helpers.replay_setup() - state.session.set_active({ id = 'sess1' }) - vim.api.nvim_set_current_win(state.windows.output_win) - - permission_window.add_permission({ - id = 'perm-dismiss', - permission = 'bash', - title = 'Run command', - }) - - assert.is_false(vim.tbl_contains(permission_window._dialog._keymaps, '')) - assert.is_false(vim.tbl_contains(permission_window._dialog._keymaps, '')) - assert.are.equal(1, permission_window.get_permission_count()) - end) - end) end) diff --git a/tests/unit/question_window_spec.lua b/tests/unit/question_window_spec.lua index 21e3e7e9..bfa6ed2f 100644 --- a/tests/unit/question_window_spec.lua +++ b/tests/unit/question_window_spec.lua @@ -6,20 +6,6 @@ local stub = require('luassert.stub') local helpers = require('tests.helpers') describe('question_window', function() - local function with_mousepos(mousepos, callback) - local original_getmousepos = vim.fn.getmousepos - ---@diagnostic disable-next-line: duplicate-set-field - vim.fn.getmousepos = function() - return mousepos - end - - local ok, err = pcall(callback) - vim.fn.getmousepos = original_getmousepos - if not ok then - error(err) - end - end - after_each(function() question_window._current_question = nil question_window._current_question_index = 1 @@ -29,9 +15,6 @@ describe('question_window', function() state.renderer.set_messages({}) state.session.set_active(nil) state.jobs.set_api_client(nil) - if state.windows then - pcall(require('opencode.ui.ui').close_windows, state.windows) - end end) it('tracks answers by question index and waits until all are answered', function() @@ -343,73 +326,4 @@ describe('question_window', function() require('opencode.ui.ui').close_windows(state.windows) end end) - - it('passes question display part to Dialog and answers clicked option', function() - helpers.replay_setup() - state.session.set_active({ id = 'sess1' }) - vim.api.nvim_set_current_win(state.windows.output_win) - - local original_defer_fn = vim.defer_fn - ---@diagnostic disable-next-line: duplicate-set-field - vim.defer_fn = function(fn, _delay) - fn() - end - - local ok, err = pcall(function() - local replies = {} - state.jobs.set_api_client({ - reply_question = function(_, request_id, answers) - table.insert(replies, { request_id = request_id, answers = answers }) - return Promise.new():resolve({}) - end, - reject_question = function() - return Promise.new():resolve({}) - end, - }) - - question_window.show_question({ - id = 'q-mouse', - sessionID = 'sess1', - questions = { - { - question = 'Pick one', - options = { - { label = 'One' }, - { label = 'Two' }, - }, - }, - }, - }) - - local dialog = question_window._dialog - assert.are.equal('question-display-part', dialog._config.render_part_id) - assert.is_false(dialog._config.mouse_select) - assert.are.equal('', dialog._config.keymaps.dismiss) - - require('opencode.ui.renderer.flush').flush() - local rendered_part = require('opencode.ui.renderer.ctx').render_state:get_part('question-display-part') - assert.is_not_nil(rendered_part) - - with_mousepos({ - winid = state.windows.output_win, - line = rendered_part.line_start + dialog._option_local_lines[2] + 1, - column = 1, - }, function() - dialog:select_mouse_option() - end) - - assert.are.same({ - { - request_id = 'q-mouse', - answers = { { 'Two' } }, - }, - }, replies) - assert.is_nil(question_window._current_question) - end) - - vim.defer_fn = original_defer_fn - if not ok then - error(err) - end - end) end) diff --git a/tests/unit/renderer_buffer_spec.lua b/tests/unit/renderer_buffer_spec.lua index 7f809270..6dcebbb3 100644 --- a/tests/unit/renderer_buffer_spec.lua +++ b/tests/unit/renderer_buffer_spec.lua @@ -1,7 +1,6 @@ local buffer = require('opencode.ui.renderer.buffer') local ctx = require('opencode.ui.renderer.ctx') local output_window = require('opencode.ui.output_window') -local state = require('opencode.state') local stub = require('luassert.stub') local function assert_called_before(call_order, first_name, second_name) @@ -47,7 +46,6 @@ describe('renderer.buffer extmarks', function() clear_extmarks_stub:revert() set_extmarks_stub:revert() highlight_changed_lines_stub:revert() - state.renderer.set_messages({}) ctx:reset() end) @@ -128,29 +126,6 @@ describe('renderer.buffer extmarks', function() assert_called_before(call_order, 'clear_extmarks', 'set_lines') end) - it('inserts regular messages above pinned bottom displays', function() - local get_line_count_stub = stub(output_window, 'get_buf_line_count').returns(14) - local shift_folds_stub = stub(output_window, 'shift_folds') - state.renderer.set_messages({ - { info = { id = 'msg_1' } }, - }) - ctx.render_state:set_message({ info = { id = 'question-display-message' } }, 10, 12) - ctx.render_state:set_message({ info = { id = 'msg_1' } }) - - buffer.upsert_message_now('msg_1', { - lines = { 'regular message' }, - extmarks = {}, - actions = {}, - fold_ranges = {}, - }) - - assert.stub(set_lines_stub).was_called_with({ 'regular message' }, 10, 10) - - get_line_count_stub:revert() - shift_folds_stub:revert() - state.renderer.set_messages({}) - end) - it('only clears and reapplies appended extmarks during append-only updates', function() ctx.render_state:set_part({ id = 'part_1', messageID = 'msg_1', type = 'text' }, 10, 11) ctx.formatted_parts['part_1'] = {