1010--- @field keymaps ? DialogKeymaps Custom keymap configuration
1111--- @field namespace_prefix ? string Prefix for vim.on_key namespace (default : ' opencode_dialog' )
1212--- @field hide_input ? boolean Whether to hide the input window when dialog is active (default : true )
13+ --- @field render_part_id ? string Rendered part ID used to resolve mouse clicks against output lines
14+ --- @field mouse_select ? boolean Whether Dialog should register <LeftMouse> for option selection (default : false )
1315
1416--- @class DialogKeymaps
1517--- @field up ? string[] Keys for navigating up (default : { ' k' , ' <Up>' } )
1618--- @field down ? string[] Keys for navigating down (default : { ' j' , ' <Down>' } )
1719--- @field left ? string[] Keys for navigating left between groups
1820--- @field right ? string[] Keys for navigating right between groups
1921--- @field select ? string Key for selecting current option (default : ' <CR>' )
20- --- @field dismiss ? string Key for dismissing dialog (default : ' <Esc>' )
22+ --- @field dismiss ? string | string[] Keys for dismissing dialog (default : { ' <Esc>' , ' <C-c> ' } )
2123--- @field number_shortcuts ? boolean Enable 1-9 number shortcuts (default : true )
2224
2325--- @class Dialog
2729--- @field private _selected_index integer Currently selected option index
2830--- @field private _active boolean Whether dialog is currently active
2931--- @field private _group_index integer Currently selected group index
32+ --- @field private _option_local_lines integer[] 0-based output-local lines for each option
3033local Dialog = {}
3134Dialog .__index = Dialog
3235
36+ --- @param keymap string | string[] | nil
37+ --- @return string[]
38+ local function keymap_list (keymap )
39+ local keymaps = {}
40+ if type (keymap ) == ' string' then
41+ if keymap ~= ' ' then
42+ table.insert (keymaps , keymap )
43+ end
44+ elseif type (keymap ) == ' table' then
45+ for _ , key in ipairs (keymap ) do
46+ if key and key ~= ' ' then
47+ table.insert (keymaps , key )
48+ end
49+ end
50+ end
51+ return keymaps
52+ end
53+
54+ --- @param keymaps string[]
55+ --- @return string
56+ local function keymap_legend (keymaps )
57+ local labels = {}
58+ for _ , key in ipairs (keymaps ) do
59+ table.insert (labels , ' `' .. key .. ' `' )
60+ end
61+ return table.concat (labels , ' or ' )
62+ end
63+
3364--- Create a new dialog instance
3465--- @param config DialogConfig Dialog configuration
3566--- @return Dialog
@@ -43,7 +74,7 @@ function Dialog.new(config)
4374 left = {},
4475 right = {},
4576 select = ' <CR>' ,
46- dismiss = ' <Esc>' ,
77+ dismiss = { ' <Esc>' , ' <C-c> ' } ,
4778 number_shortcuts = true ,
4879 }
4980
@@ -54,13 +85,15 @@ function Dialog.new(config)
5485 return true
5586 end ,
5687 hide_input = true ,
88+ mouse_select = false ,
5789 } --[[ @as DialogConfig]] , config )
5890
5991 self ._keymaps = {}
6092 self ._key_capture_ns = nil
6193 self ._selected_index = 1
6294 self ._group_index = 1
6395 self ._active = false
96+ self ._option_local_lines = {}
6497
6598 return self
6699end
@@ -248,8 +281,9 @@ function Dialog:format_legend(output, options)
248281 local line = output :add_line (select_text )
249282 end
250283
251- if keymaps .dismiss and keymaps .dismiss ~= ' ' then
252- local line = output :add_line (' Close: `<Esc>`' )
284+ local dismiss_keymaps = keymap_list (keymaps .dismiss )
285+ if # dismiss_keymaps > 0 then
286+ local line = output :add_line (' Close: ' .. keymap_legend (dismiss_keymaps ))
253287 end
254288 else
255289 local message = options .unfocused_message or ' Focus Opencode window to interact'
268302--- - progress?: string - Progress indicator (e.g., "(1/3)")
269303--- - content?: string[] - Array of lines to render before options
270304--- - render_content?: function(output: Output) - Custom function to render content before options
305+ --- - hide_legend?: boolean - Whether to hide movement/select/dismiss instructions
271306function Dialog :format_dialog (output , config )
272307 if not self ._active then
273308 return
@@ -301,9 +336,10 @@ function Dialog:format_dialog(output, config)
301336
302337 self :format_options (output , config .options or {})
303338
304- output :add_line (' ' )
305-
306- self :format_legend (output , { unfocused_message = config .unfocused_message })
339+ if not config .hide_legend then
340+ output :add_line (' ' )
341+ self :format_legend (output , { unfocused_message = config .unfocused_message })
342+ end
307343
308344 local end_line = output :get_line_count ()
309345
322358--- @param output Output Output object to write to
323359--- @param options table[] Array of option objects with {label : string , description ?: string }
324360function Dialog :format_options (output , options )
361+ self ._option_local_lines = {}
362+
325363 for i , option in ipairs (options ) do
326364 local label = option .label
327365 if option .description and option .description ~= ' ' then
@@ -339,11 +377,12 @@ function Dialog:format_options(output, options)
339377 -- add_line returns a 1-based line index; Output extmarks use 0-based
340378 -- keys, so subtract 1 to get the correct extmark key.
341379 local added_idx = output :add_line (line_text )
380+ local option_local_line = added_idx - 1
381+ self ._option_local_lines [i ] = option_local_line
342382
343383 if is_selected then
344- local extmark_idx = added_idx - 1
345- output :add_extmark (extmark_idx , { line_hl_group = ' OpencodeDialogOptionHover' } --[[ @as OutputExtmark]] )
346- output :add_extmark (extmark_idx , {
384+ output :add_extmark (option_local_line , { line_hl_group = ' OpencodeDialogOptionHover' } --[[ @as OutputExtmark]] )
385+ output :add_extmark (option_local_line , {
347386 start_col = 2 ,
348387 virt_text = { { ' › ' , ' OpencodeDialogOptionHover' } },
349388 virt_text_pos = ' overlay' ,
@@ -352,6 +391,58 @@ function Dialog:format_options(output, options)
352391 end
353392end
354393
394+ --- @return integer | nil
395+ function Dialog :_mouse_option_index ()
396+ local render_part_id = self ._config .render_part_id
397+ if not render_part_id or render_part_id == ' ' then
398+ return nil
399+ end
400+
401+ local mouse = vim .fn .getmousepos ()
402+ local winid = mouse and mouse .winid
403+ if not winid or winid == 0 or not vim .api .nvim_win_is_valid (winid ) then
404+ return nil
405+ end
406+
407+ local buf = self ._config .buffer
408+ if not buf or vim .api .nvim_win_get_buf (winid ) ~= buf then
409+ return nil
410+ end
411+
412+ local rendered_part = require (' opencode.ui.renderer.ctx' ).render_state :get_part (render_part_id )
413+ if not rendered_part or rendered_part .line_start == nil then
414+ return nil
415+ end
416+
417+ local mouse_line = mouse .line
418+ if not mouse_line or mouse_line <= 0 then
419+ return nil
420+ end
421+
422+ local clicked_output_line = mouse_line - 1
423+ for option_index , option_local_line in ipairs (self ._option_local_lines ) do
424+ if clicked_output_line == rendered_part .line_start + option_local_line then
425+ return option_index
426+ end
427+ end
428+ end
429+
430+ --- @return boolean selected
431+ function Dialog :select_mouse_option ()
432+ if not self ._active or not self ._config .check_focused () then
433+ return false
434+ end
435+
436+ local option_index = self :_mouse_option_index ()
437+ if not option_index then
438+ return false
439+ end
440+
441+ self ._selected_index = option_index
442+ self ._config .on_select (option_index )
443+ return true
444+ end
445+
355446--- Set up buffer-scoped keymaps
356447function Dialog :_setup_keymaps ()
357448 self :_clear_keymaps ()
@@ -450,18 +541,34 @@ function Dialog:_setup_keymaps()
450541 table.insert (self ._keymaps , keymaps .select )
451542 end
452543
453- if keymaps . dismiss and keymaps . dismiss ~= ' ' then
544+ if self . _config . mouse_select ~= false and self . _config . render_part_id and self . _config . render_part_id ~= ' ' then
454545 vim .keymap .set (
455546 ' n' ,
456- keymaps . dismiss ,
547+ ' <LeftMouse> ' ,
457548 function ()
458- self :dismiss ()
549+ self :select_mouse_option ()
459550 end ,
460551 vim .tbl_extend (' force' , keymap_opts , {
461- desc = ' Dialog: dismiss ' ,
552+ desc = ' Dialog: select clicked option ' ,
462553 })
463554 )
464- table.insert (self ._keymaps , keymaps .dismiss )
555+ table.insert (self ._keymaps , ' <LeftMouse>' )
556+ end
557+
558+ for _ , key in ipairs (keymap_list (keymaps .dismiss )) do
559+ if key and key ~= ' ' then
560+ vim .keymap .set (
561+ ' n' ,
562+ key ,
563+ function ()
564+ self :dismiss ()
565+ end ,
566+ vim .tbl_extend (' force' , keymap_opts , {
567+ desc = ' Dialog: dismiss' ,
568+ })
569+ )
570+ table.insert (self ._keymaps , key )
571+ end
465572 end
466573
467574 if keymaps .number_shortcuts then
0 commit comments