33--- @field on_select function (index : integer ) Called when an option is selected
44--- @field on_dismiss ? function () Called when dialog is dismissed
55--- @field on_navigate ? function () Called when selection changes
6+ --- @field on_navigate_group ? function (index : integer ) Called when group selection changes
67--- @field get_option_count function (): integer Returns the total number of options
8+ --- @field get_group_count ? function (): integer Returns the total number of groups
79--- @field check_focused ? function (): boolean Returns whether dialog should be active
810--- @field keymaps ? DialogKeymaps Custom keymap configuration
911--- @field namespace_prefix ? string Prefix for vim.on_key namespace (default : ' opencode_dialog' )
1214--- @class DialogKeymaps
1315--- @field up ? string[] Keys for navigating up (default : { ' k' , ' <Up>' } )
1416--- @field down ? string[] Keys for navigating down (default : { ' j' , ' <Down>' } )
17+ --- @field left ? string[] Keys for navigating left between groups
18+ --- @field right ? string[] Keys for navigating right between groups
1519--- @field select ? string Key for selecting current option (default : ' <CR>' )
1620--- @field dismiss ? string Key for dismissing dialog (default : ' <Esc>' )
1721--- @field number_shortcuts ? boolean Enable 1-9 number shortcuts (default : true )
2226--- @field private _key_capture_ns integer ? Namespace for vim.on_key
2327--- @field private _selected_index integer Currently selected option index
2428--- @field private _active boolean Whether dialog is currently active
29+ --- @field private _group_index integer Currently selected group index
2530local Dialog = {}
2631Dialog .__index = Dialog
2732
@@ -35,6 +40,8 @@ function Dialog.new(config)
3540 local default_keymaps = {
3641 up = { ' k' , ' <Up>' },
3742 down = { ' j' , ' <Down>' },
43+ left = {},
44+ right = {},
3845 select = ' <CR>' ,
3946 dismiss = ' <Esc>' ,
4047 number_shortcuts = true ,
@@ -52,6 +59,7 @@ function Dialog.new(config)
5259 self ._keymaps = {}
5360 self ._key_capture_ns = nil
5461 self ._selected_index = 1
62+ self ._group_index = 1
5563 self ._active = false
5664
5765 return self
@@ -72,6 +80,24 @@ function Dialog:set_selection(index)
7280 end
7381end
7482
83+ --- @return integer
84+ function Dialog :get_group_selection ()
85+ return self ._group_index
86+ end
87+
88+ --- @param index integer
89+ function Dialog :set_group_selection (index )
90+ local group_count = self ._config .get_group_count and self ._config .get_group_count () or 0
91+ if group_count == 0 then
92+ self ._group_index = 1
93+ return
94+ end
95+
96+ if index >= 1 and index <= group_count then
97+ self ._group_index = index
98+ end
99+ end
100+
75101--- Navigate selection by delta (positive for down, negative for up)
76102--- @param delta integer Amount to move selection
77103function Dialog :navigate (delta )
@@ -98,6 +124,28 @@ function Dialog:navigate(delta)
98124 end
99125end
100126
127+ --- @param delta integer
128+ function Dialog :navigate_group (delta )
129+ if not self ._active or not self ._config .check_focused () or not self ._config .on_navigate_group then
130+ return
131+ end
132+
133+ local group_count = self ._config .get_group_count and self ._config .get_group_count () or 0
134+ if group_count <= 1 then
135+ return
136+ end
137+
138+ self ._group_index = self ._group_index + delta
139+
140+ if self ._group_index < 1 then
141+ self ._group_index = group_count
142+ elseif self ._group_index > group_count then
143+ self ._group_index = 1
144+ end
145+
146+ self ._config .on_navigate_group (self ._group_index )
147+ end
148+
101149--- Select the current option
102150function Dialog :select ()
103151 if not self ._active or not self ._config .check_focused () then
@@ -178,31 +226,42 @@ function Dialog:format_legend(output, options)
178226 end
179227
180228 if ui .is_opencode_focused () then
181- local legend_parts = {}
182229 local keymaps = self ._config .keymaps
183230 if not keymaps then
184231 return
185232 end
186233
187234 if keymaps .up and # keymaps .up > 0 and keymaps .down and # keymaps .down > 0 then
188- table.insert (legend_parts , string.format (' Navigate: `%s`/`%s` or `↑`/`↓`' , keymaps .down [1 ], keymaps .up [1 ]))
235+ local line = output :add_line (string.format (' Move: j/k or %s/%s' , ' ↑' , ' ↓' ))
236+ output :add_extmark (line - 1 , { start_col = 6 , end_col = 9 , hl_group = ' OpencodeQuestionKeyHint' } --[[ @as OutputExtmark]] )
237+ output :add_extmark (line - 1 , { start_col = 13 , end_col = 16 , hl_group = ' OpencodeQuestionKeyHint' } --[[ @as OutputExtmark]] )
238+ end
239+
240+ if keymaps .left and # keymaps .left > 0 and keymaps .right and # keymaps .right > 0 then
241+ local line = output :add_line (' Question: h/l or <-/->' )
242+ output :add_extmark (line - 1 , { start_col = 10 , end_col = 13 , hl_group = ' OpencodeQuestionKeyHint' } --[[ @as OutputExtmark]] )
243+ output :add_extmark (line - 1 , { start_col = 17 , end_col = 23 , hl_group = ' OpencodeQuestionKeyHint' } --[[ @as OutputExtmark]] )
189244 end
190245
191246 if keymaps .select and keymaps .select ~= ' ' then
192- local select_text = string.format (' Select: `%s`' , keymaps .select )
247+ local select_text = ' Select: <CR>'
248+ if keymaps .number_shortcuts and option_count > 0 then
249+ local max_shortcut = math.min (option_count , 9 )
250+ select_text = select_text .. string.format (' or 1-%d' , max_shortcut )
251+ end
252+ local line = output :add_line (select_text )
253+ output :add_extmark (line - 1 , { start_col = 8 , end_col = 12 , hl_group = ' OpencodeQuestionKeyHint' } --[[ @as OutputExtmark]] )
193254 if keymaps .number_shortcuts and option_count > 0 then
194255 local max_shortcut = math.min (option_count , 9 )
195- select_text = select_text .. string.format (' or `1-%d`' , max_shortcut )
256+ local suffix = string.format (' 1-%d' , max_shortcut )
257+ local start_col = # select_text - # suffix
258+ output :add_extmark (line - 1 , { start_col = start_col , end_col = # select_text , hl_group = ' OpencodeQuestionKeyHint' } --[[ @as OutputExtmark]] )
196259 end
197- table.insert (legend_parts , select_text )
198260 end
199261
200262 if keymaps .dismiss and keymaps .dismiss ~= ' ' then
201- table.insert (legend_parts , string.format (' Dismiss: `%s`' , keymaps .dismiss ))
202- end
203-
204- if # legend_parts > 0 then
205- output :add_line (table.concat (legend_parts , ' ' ))
263+ local line = output :add_line (' Close: <Esc>' )
264+ output :add_extmark (line - 1 , { start_col = 7 , end_col = 12 , hl_group = ' OpencodeQuestionKeyHint' } --[[ @as OutputExtmark]] )
206265 end
207266 else
208267 local message = options .unfocused_message or ' Focus Opencode window to interact'
@@ -353,6 +412,42 @@ function Dialog:_setup_keymaps()
353412 end
354413 end
355414
415+ if keymaps .left then
416+ for _ , key in ipairs (keymaps .left ) do
417+ if key and key ~= ' ' then
418+ vim .keymap .set (
419+ ' n' ,
420+ key ,
421+ function ()
422+ self :navigate_group (- 1 )
423+ end ,
424+ vim .tbl_extend (' force' , keymap_opts , {
425+ desc = ' Dialog: navigate left' ,
426+ })
427+ )
428+ table.insert (self ._keymaps , key )
429+ end
430+ end
431+ end
432+
433+ if keymaps .right then
434+ for _ , key in ipairs (keymaps .right ) do
435+ if key and key ~= ' ' then
436+ vim .keymap .set (
437+ ' n' ,
438+ key ,
439+ function ()
440+ self :navigate_group (1 )
441+ end ,
442+ vim .tbl_extend (' force' , keymap_opts , {
443+ desc = ' Dialog: navigate right' ,
444+ })
445+ )
446+ table.insert (self ._keymaps , key )
447+ end
448+ end
449+ end
450+
356451 if keymaps .select and keymaps .select ~= ' ' then
357452 vim .keymap .set (
358453 ' n' ,
0 commit comments